From: "RubyBugs (A Nonymous)" <noreply@...>
Date: 2022-11-30T19:58:59+00:00
Subject: [ruby-core:111098] [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).
For comparison, you can plug in the following example code for a faster implementation of `#with` and it should work the same:
```ruby
Point = Data.define(:x, :y) do
# Example only, too slow. Real implementation should eliminate allocations.
# def with(**args)
# raise ArgumentError unless args.keys.all? { |k| members.include? k }
# self.class.new(**(to_h.merge(args)))
# end
# A trick to avoid `nil` for defaults so that `nil` can be valid
DEFAULT = class.new.new.freeze
# An example of a faster implementation that could inspire auto-generation
def with(x: DEFAULT, y: DEFAULT)
self.class.new(
x: x.equal?(DEFAULT) ? self.x : x),
y: y.equal?(DEFAULT) ? self.y : y),
)
end
end
```
----------------------------------------
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-100364
* 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/
______________________________________________
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/