From: "RubyBugs (A Nonymous)" Date: 2022-12-07T20:44:36+00:00 Subject: [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) # => # ``` > 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-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/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/