ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
* [ruby-core:109850] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
@ 2022-09-08 18:19 RubyBugs (A Nonymous)
  2022-09-08 19:47 ` [ruby-core:109856] " bkuhlmann (Brooke Kuhlmann)
                   ` (40 more replies)
  0 siblings, 41 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-09-08 18:19 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been reported by RubyBugs (A Nonymous).

----------------------------------------
Feature #19000: Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
https://bugs.ruby-lang.org/issues/19000

* 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/

^ permalink raw reply	[flat|nested] 42+ messages in thread

end of thread, other threads:[~2022-12-22  0:47 UTC | newest]

Thread overview: 42+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-09-08 18:19 [ruby-core:109850] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object] RubyBugs (A Nonymous)
2022-09-08 19:47 ` [ruby-core:109856] " bkuhlmann (Brooke Kuhlmann)
2022-10-21 15:05 ` [ruby-core:110466] " bdewater (Bart de Water)
2022-10-21 17:22 ` [ruby-core:110468] " RubyBugs (A Nonymous)
2022-10-21 21:35 ` [ruby-core:110471] " ufuk (Ufuk Kayserilioglu)
2022-10-21 22:13 ` [ruby-core:110472] " jeremyevans0 (Jeremy Evans)
2022-10-22  1:46 ` [ruby-core:110474] " Dan0042 (Daniel DeLorme)
2022-10-24 19:42 ` [ruby-core:110503] " ufuk (Ufuk Kayserilioglu)
2022-10-26 18:16 ` [ruby-core:110517] " RubyBugs (A Nonymous)
2022-11-16 10:02 ` [ruby-core:110774] " tomstuart (Tom Stuart)
2022-11-17  7:33 ` [ruby-core:110791] " p8 (Petrik de Heus)
2022-11-17 12:05 ` [ruby-core:110796] " nobu (Nobuyoshi Nakada)
2022-11-17 12:28 ` [ruby-core:110797] " tomstuart (Tom Stuart)
2022-11-18  3:09 ` [ruby-core:110804] " mame (Yusuke Endoh)
2022-11-18 20:19 ` [ruby-core:110820] " ufuk (Ufuk Kayserilioglu)
2022-11-28 17:04 ` [ruby-core:111038] " bdewater (Bart de Water)
2022-11-28 19:15 ` [ruby-core:111039] " RubyBugs (A Nonymous)
2022-11-29  4:09 ` [ruby-core:111046] " mame (Yusuke Endoh)
2022-11-29 14:46 ` [ruby-core:111064] " p8 (Petrik de Heus)
2022-11-30  3:13 ` [ruby-core:111080] " Eregon (Benoit Daloze)
2022-11-30  3:20 ` [ruby-core:111081] " Dan0042 (Daniel DeLorme)
2022-11-30 19:52 ` [ruby-core:111097] " RubyBugs (A Nonymous)
2022-11-30 19:58 ` [ruby-core:111098] " RubyBugs (A Nonymous)
2022-12-02  2:54 ` [ruby-core:111141] " mame (Yusuke Endoh)
2022-12-02  3:13 ` [ruby-core:111142] " mame (Yusuke Endoh)
2022-12-02 17:05 ` [ruby-core:111162] " ufuk (Ufuk Kayserilioglu)
2022-12-03  2:12 ` [ruby-core:111169] " matz (Yukihiro Matsumoto)
2022-12-03  2:56 ` [ruby-core:111170] " ufuk (Ufuk Kayserilioglu)
2022-12-03 14:09 ` [ruby-core:111173] " RubyBugs (A Nonymous)
2022-12-03 14:26 ` [ruby-core:111174] " tomstuart (Tom Stuart)
2022-12-03 14:44 ` [ruby-core:111175] " ufuk (Ufuk Kayserilioglu)
2022-12-03 14:49 ` [ruby-core:111176] " austin (Austin Ziegler)
2022-12-03 16:15 ` [ruby-core:111178] " ufuk (Ufuk Kayserilioglu)
2022-12-03 16:29 ` [ruby-core:111180] " RubyBugs (A Nonymous)
2022-12-07 20:44 ` [ruby-core:111235] " RubyBugs (A Nonymous)
2022-12-08 14:41 ` [ruby-core:111243] " Eregon (Benoit Daloze)
2022-12-20  7:13 ` [ruby-core:111338] " k0kubun (Takashi Kokubun) via ruby-core
2022-12-20 16:27 ` [ruby-core:111352] " Eregon (Benoit Daloze) via ruby-core
2022-12-20 22:02 ` [ruby-core:111358] " ufuk (Ufuk Kayserilioglu) via ruby-core
2022-12-21 23:37 ` [ruby-core:111371] " matz (Yukihiro Matsumoto) via ruby-core
2022-12-22  0:23 ` [ruby-core:111373] " naruse (Yui NARUSE) via ruby-core
2022-12-22  0:46 ` [ruby-core:111374] " ufuk (Ufuk Kayserilioglu) via ruby-core

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).