From: "ufuk (Ufuk Kayserilioglu)" Date: 2022-11-18T20:19:14+00:00 Subject: [ruby-core:110820] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object] Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu). I just put up a PR implementing this using the `dup` proposal: https://github.com/ruby/ruby/pull/6766 Here are my objections to other proposals: 1. `point = Point.new(**Origin.to_h, **change)` works but is very verbose and not intention revealing, in my opinion. For the reader of this code, it is very hard to understand what the intended operation is. 2. `with_attrs` (from https://bugs.ruby-lang.org/issues/18951): That proposal is explicitly about mutating the receiver of `with_attrs`, which is the exact opposite of what this "copy with changes" operation is trying to do. For that reason, I don't think we should be conflating that with this proposal. 3. `with`: While I understand that there is precedent for using `with` from other languages, I have always found the name to not make it clear that a copy is being made. Since `with` implies doing something "with" original receiver, it leads less experienced folks to think that the receiver might be modified in the process. I think we have an opportunity to make the operation more clear by giving it a better name. My reasoning for `dup` is to make it explicit that a copy is being created (just as how a string copy is created when `"foo".dup` is called) but that the copy has updated values for the supplied member fields. `dup` is already an existing protocol for creating shallow copies of objects, and can already be used on `Data` class instance even without this change, of course without being able to affect the copy in any way. Extending the existing copying protocol of Ruby to optionally add the ability to affect the copy feels like the best way forward to me. I would be happy to hear feedback from the community on my PR and if a different name than `dup` is picked in the end, also would be happy to update the PR to conform. ---------------------------------------- Feature #19000: Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object] https://bugs.ruby-lang.org/issues/19000#change-100177 * Author: RubyBugs (A Nonymous) * Status: Open * Priority: Normal ---------------------------------------- *As requested: extracted a follow-up to #16122 Data: simple immutable value object from [this comment](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/109815)* # Proposal: Add a "Copy with changes" method to Data Assume the proposed `Data.define` exists. Seeing examples from the [[Values gem]](https://github.com/ms-ati/Values): ```ruby # A new class Point = Data.def(:x, :y) # An immutable instance Origin = Point.with(x: 0, y: 0) # Q: How do we make copies that change 1 or more values? right = Origin.with(x: 1.0) up = Origin.with(y: 1.0) up_and_right = right.with(y: up.y) # In loops movements = [ { x: +0.5 }, { x: +0.5 }, { y: -1.0 }, { x: +0.5 }, ] # position = Point(x: 1.5, y: -1.0) position = movements.inject(Origin) { |p, move| p.with(**move) } ``` ## Proposed detail: Call this method: `#with` ```ruby Money = Data.define(:amount, :currency) account = Money.new(amount: 100, currency: 'USD') transactions = [+10, -5, +15] account = transactions.inject(account) { |a, t| a.with(amount: a.amount + t) } #=> Money(amount: 120, currency: "USD") ``` ## Why add this "Copy with changes" method to the Data simple immutable value class? Called on an instance, it returns a new instance with only the provided parameters changed. This API affordance is now **widely adopted across many languages** for its usefulness. Why is it so useful? Because copying immutable value object instances, with 1 or more discrete changes to specific fields, is the proper and ubiquitous pattern that takes the place of mutation when working with immutable value objects. **Other languages** C# Records: ���immutable record structs ��� Non-destructive mutation��� ��� is called `with { ... }` https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#nondestructive-mutation Scala Case Classes ��� is called `#copy` https://docs.scala-lang.org/tour/case-classes.html Java 14+ Records ��� Brian Goetz at Oracle is working on adding a with copy constructor inspired by C# above as we speak, likely to be called `#with` https://mail.openjdk.org/pipermail/amber-spec-experts/2022-June/003461.html Rust ���Struct Update Syntax��� via `..` syntax in constructor https://doc.rust-lang.org/book/ch05-01-defining-structs.html#creating-instances-from-other-instances-with-struct-update-syntax ## Alternatives Without a copy-with-changes method, one must construct entirely new instances using the constructor. This can either be (a) fully spelled out as boilerplate code, or (b) use a symmetrical `#to_h` to feed the keyword-args constructor. **(a) Boilerplate using constructor** ```ruby Point = Data.define(:x, :y, :z) Origin = Point.new(x: 0.0, y: 0.0, z: 0.0) change = { z: -1.5 } # Have to use full constructor -- does this even work? point = Point.new(x: Origin.x, y: Origin.y, **change) ``` **(b) Using a separately proposed `#to_h` method and constructor symmetry** ```ruby Point = Data.define(:x, :y, :z) Origin = Point.new(x: 0.0, y: 0.0, z: 0.0) change = { z: -1.5 } # Have to use full constructor -- does this even work? point = Point.new(**(Origin.to_h.merge(change))) ``` Notice that the above are not ergonomic -- leading so many of our peer language communities to adopt the `#with` method to copy an instance with discrete changes. -- https://bugs.ruby-lang.org/ Unsubscribe: