From: "Dan0042 (Daniel DeLorme)" <noreply@ruby-lang.org>
To: ruby-core@neon.ruby-lang.org
Subject: [ruby-core:110474] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
Date: Sat, 22 Oct 2022 01:46:16 +0000 (UTC) [thread overview]
Message-ID: <redmine.journal-99792.20221022014616.1343@ruby-lang.org> (raw)
In-Reply-To: redmine.issue-19000.20220908181900.1343@ruby-lang.org
Issue #19000 has been updated by Dan0042 (Daniel DeLorme).
ufuk (Ufuk Kayserilioglu) wrote in #note-4:
> I think allowing the `dup` method on `Data` instances to take optional keyword arguments would be the ideal API for this
I had a strong visceral "no" reaction to this. To a certain extent it's a logical idea, we are after all creating a kind of copy. But the dup/clone API is so old, so well established and so consistent throughout ruby, changing/extending it would be a mistake.
----------------------------------------
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-99792
* 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/
next prev parent reply other threads:[~2022-10-22 1:46 UTC|newest]
Thread overview: 42+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Dan0042 (Daniel DeLorme) [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-list from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.ruby-lang.org/en/community/mailing-lists/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=redmine.journal-99792.20221022014616.1343@ruby-lang.org \
--to=ruby-core@ruby-lang.org \
--cc=ruby-core@neon.ruby-lang.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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).