[ruby-core:111235] [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 RubyBugs (A Nonymous). Hi @mame! Thank you for your questions. mame (Yusuke Endoh) wrote in #note-24:
Thanks for the update.
Now I have a question. Do you really want to write `p.with(field => p.send(field) + delta)`? I don't think it is very elegant. It is not very convincing (at least, to me) as a first motivation example.
The challenge is that this operation "make a copy of an immutable value object with 0 or more fields changed" is so fundamental, it's hard to find examples that strip away every other concern. Staying with the `Point` example, would **translate** and **invert** work better as examples that change multiple fields? ```ruby ## # Example of proposed Data#with # # To try this out: # # ruby-install ruby-3.2.0-preview3 # ## Point3d = Data.define(:x, :y, :z) do # Example only, too slow due to allocations def with(**args) raise ArgumentError unless args.keys.all? { |k| members.include? k } self.class.new(**(to_h.merge(args))) end def translate_2d(dx: 0, dy: 0) with(x: x + dx, y: y + dy) end def invert_2d with(x: -x, y: -y) end end Origin3d = Point3d.new(x: 0, y: 0, z:0) north = Origin3d.translate_2d(dy: 1.0) south = north.invert_2d east = Origin3d.translate_2d(dx: 1.0) west = east.invert_2d west == Point3d.new(x: -1.0, y: 0, z: 0) # => true mountain_height = 2.0 western_mountain = west.with(z: mountain_height) # => #<data Point3d x=-1.0, y=0, z=2.0> ```
Also, do you need the ability to update multiple fields at once? Both motivation examples only update a single field. This may be the result of simplifying the motivation example, though.
Yes! Definitely need to update multiple fields at once
Looking at these motivation examples, there may be room to consider an API like `p.with(field) {|old_value| old_value + delta }` or something.
Agreed that this probably comes out of a motivating example which is trying to calculate the new value for a **single** field based on **only** the previous value of that field. ---------------------------------------- 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-100524 * 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 require "values" # A new class Point = Value.new(: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) do |p, (field, delta)| p.with(field => p.send(field) + delta) end ``` ## 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-ty... 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-instan... ## 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/
participants (1)
-
RubyBugs (A Nonymous)