Issue #19000 has been updated by RubyBugs (A Nonymous).
bdewater (Bart de Water) wrote in #note-15:
I like `dup` as the method name đź‘Ť
Is there a way we could get more active Rubyists to weigh in? My sense is that there is a
real tension in that:
* Nearly every major value objects gem use `#with`
* Most other language with value objects use `#with`
On the other hand, there seem to be a number of voices on this Bug thread, who while they
don't necessarily currently work with code that uses this exact pattern, feel strongly
that overloading the meaning of `#dup` is a better choice for Ruby. Even the amazing and
incomparable @jeremyevans, whose Sequel gem our team depends on as well :)
While it might not generally be the practice of the Ruby community, would we consider a
way to get more "working Rubyist" eyes on this question? Perhaps by getting this
thread in to the Ruby Weekly News for example?
In the end, having the method is more important than the name. But it does seem important
to let voices be heard?
----------------------------------------
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-100292
* 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-t…
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-insta…
## 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/