Project

General

Profile

Feature #19000

Updated by RubyBugs (A Nonymous) about 2 years ago

*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, 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, { x: +0.5 ], }, 
   [ :x, { x: +0.5 ], }, 
   [ :y, { y: -1.0 ], }, 
   [ :x, { 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 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.

Back