From: "k0kubun (Takashi Kokubun) via ruby-core" Date: 2022-12-20T07:13:32+00:00 Subject: [ruby-core:111338] [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 k0kubun (Takashi Kokubun). Summary of the discussion so far: * Interface * `data.xxx(foo: bar)`, making `data.xxx` (no argument) an error * Matz #note-27 is in favor of this. * This might make dynamic use of this API harder #note-29 * `data.xxx(foo: bar)`, making `data.xxx` (no argument) not an error * `data.xxx(foo) { bar }` #note-27 * Method name * `with` * Proposal: [Feature #19000] by @RubyBugs * Other languages: C#, Java (WIP), * Existing gems: sorbet, value_semantics * Pull request: https://github.com/ruby/ruby/pull/6766 * Matz #note-25 said it's acceptable. * `update` * Proposal: #note-25 * Matz #note-25 liked this, but ko1 didn't. * `dup` * Proposal: #note-4 by @ufuk * Matz #note-25 said it's not acceptable. * `copy` * Proposal: #note-29 by @RubyBugs * Other languages: Scala, Kotlin * `clone` * Proposal: #note-5 by @jeremyevans0 * Existing gems: sequel * Alternatives * `Point.new(foo: Origin.foo, **change)` * `Point.new(**(Origin.to_h.merge(change)))` * `Point.new(**Origin.to_h, **change)` * Suggested at the Ruby developers' meeting in November --- My personal notes: I'm late to the party, but in my experience of using data classes in other languages, this feels like a must-have for data classes. It'd be unfortunate if Data were released without it. > > Also, do you need the ability to update multiple fields at once? Both motivation examples only update a single field. > > Yes! Definitely need to update multiple fields at once I second that. In my previous company, we had an immutable class that represents a state/snapshot for stream-processing multiple events at once (with Amazon Kinesis). It groups multiple events and dispatches a batch operation when a certain threshold is met. Because we had multiple thresholds (e.g. max size, max duration), we naturally needed to create a copy with multiple updates when we create the next state. > I also agree the 0-kwargs case should be supported for general use like data.with(**changes), whether changes is empty or not. And that reads fine of course. > It's also like Hash#merge with no arguments. +1. I'm not sure if it's possible to distinguish them in Ruby, but ideally `data.with` or `data.with()` should be rejected even if we accept `data.with(**changes)`. If it's feasible, then it might clear Matz's concern at #note-27. ---------------------------------------- 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-100714 * 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/