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

* [ruby-core:109856] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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 ` bkuhlmann (Brooke Kuhlmann)
  2022-10-21 15:05 ` [ruby-core:110466] " bdewater (Bart de Water)
                   ` (39 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: bkuhlmann (Brooke Kuhlmann) @ 2022-09-08 19:47 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by bkuhlmann (Brooke Kuhlmann).


💡 In case it's of interest, I've partially solved this problem in the [Refinements](https://www.alchemists.io/projects/refinements) gem several years back by adding the `#merge` and `#merge!` methods to `Struct` as a refinement. Here's the [source code](https://github.com/bkuhlmann/refinements/blob/main/lib/refinements/structs.rb#L18-L23). I'm mostly posting this here in case it helps provide more example solutions to the problem mentioned in Alternative B above:

``` ruby
point = Point.new(**(Origin.to_h.merge(change)))
```

I've found that using keyword arguments, regardless of whether the struct was constructed using positionals or keywords, has been very effective in practice.

----------------------------------------
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-99093

* 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

* [ruby-core:110466] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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 ` bdewater (Bart de Water)
  2022-10-21 17:22 ` [ruby-core:110468] " RubyBugs (A Nonymous)
                   ` (38 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: bdewater (Bart de Water) @ 2022-10-21 15:05 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by bdewater (Bart de Water).


Example from Sorbet which uses `with` as well:
``` ruby
class Point < T::Struct
  const :x, Numeric
  const :y, Numeric
end

# An immutable instance
Origin = Point.new(x: 0, y: 0)

right = Origin.with(x: 1.0)
up = Origin.with(y: 1.0)
up_and_right = right.with(y: up.y)
```


----------------------------------------
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-99785

* 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

* [ruby-core:110468] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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 ` RubyBugs (A Nonymous)
  2022-10-21 21:35 ` [ruby-core:110471] " ufuk (Ufuk Kayserilioglu)
                   ` (37 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-10-21 17:22 UTC (permalink / raw)
  To: ruby-core

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


Example from [value_semantics](https://github.com/tomdalling/value_semantics) gem which uses `with` as well:
```ruby
Point = ValueSemantics::Struct.new do
  x Numeric
  y Numeric
end

# An immutable instance
Origin = Point.new(x: 0, y: 0)

right = Origin.with(x: 1.0)
up = Origin.with(y: 1.0)
up_and_right = right.with(y: up.y)
```

----------------------------------------
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-99787

* 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

* [ruby-core:110471] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (2 preceding siblings ...)
  2022-10-21 17:22 ` [ruby-core:110468] " RubyBugs (A Nonymous)
@ 2022-10-21 21:35 ` ufuk (Ufuk Kayserilioglu)
  2022-10-21 22:13 ` [ruby-core:110472] " jeremyevans0 (Jeremy Evans)
                   ` (36 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-10-21 21:35 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


Since Ruby already has the [`Object#dup` method](https://ruby-doc.org/core-3.1.0/Object.html#dup-method), I think we don't need to create a new method to duplicate a `Data` instance with potentially slightly different values.

I think allowing the `dup` method on `Data` instances to take optional keyword arguments would be the ideal API for this:
```ruby
Point = Data.define(:x, :y, :z)
Origin = Point.new(x: 0.0, y: 0.0, z: 0.0)

right = Origin.dup(x: 1.0)
up = Origin.dup(y: 1.0)
up_and_right = right.with(y: up.y)
```

----------------------------------------
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-99790

* 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

* [ruby-core:110472] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (3 preceding siblings ...)
  2022-10-21 21:35 ` [ruby-core:110471] " ufuk (Ufuk Kayserilioglu)
@ 2022-10-21 22:13 ` jeremyevans0 (Jeremy Evans)
  2022-10-22  1:46 ` [ruby-core:110474] " Dan0042 (Daniel DeLorme)
                   ` (35 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: jeremyevans0 (Jeremy Evans) @ 2022-10-21 22:13 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by jeremyevans0 (Jeremy Evans).


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 agree. This is the pattern that Sequel uses for datasets (which are always frozen, as `Data` instances are), except Sequel uses `clone` instead of `dup` (for historical reasons and because it also needs to copy singleton classes).

----------------------------------------
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-99791

* 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

* [ruby-core:110474] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (4 preceding siblings ...)
  2022-10-21 22:13 ` [ruby-core:110472] " jeremyevans0 (Jeremy Evans)
@ 2022-10-22  1:46 ` Dan0042 (Daniel DeLorme)
  2022-10-24 19:42 ` [ruby-core:110503] " ufuk (Ufuk Kayserilioglu)
                   ` (34 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: Dan0042 (Daniel DeLorme) @ 2022-10-22  1:46 UTC (permalink / raw)
  To: ruby-core

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/

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

* [ruby-core:110503] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (5 preceding siblings ...)
  2022-10-22  1:46 ` [ruby-core:110474] " Dan0042 (Daniel DeLorme)
@ 2022-10-24 19:42 ` ufuk (Ufuk Kayserilioglu)
  2022-10-26 18:16 ` [ruby-core:110517] " RubyBugs (A Nonymous)
                   ` (33 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-10-24 19:42 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


> ... Sequel uses `clone` instead of `dup` (for historical reasons and because it also needs to copy singleton classes).

I think copying singleton classes into such copies might NOT be desired behaviour for `Data` objects. So, it might make more sense to use `dup` for this. That means, it should work in the following way:
```ruby
Point = Data.define(:x, :y, :z)
ORIGIN = Point.new(x: 0.0, y: 0.0, z: 0.0)
ORIGIN.singleton_class.define_method(:distance_from_origin) { 0 }

right = ORIGIN.dup(x: 1.0)
up = ORIGIN.dup(y: 1.0)
up_and_right = right.dup(y: up.y)

ORIGIN.distance_from_origin # => 0
right.distance_from_origin # => undefined method `distance_from_origin' for #<data Point x=1.0, y=0.0, z=0.0> (NoMethodError)
up.distance_from_origin # => undefined method `distance_from_origin' for #<data Point x=0.0, y=1.0, z=0.0> (NoMethodError)
up_and_right.distance_from_origin # => undefined method `distance_from_origin' for #<data Point x=1.0, y=1.0, z=0.0> (NoMethodError)
```

Dan0042 (Daniel DeLorme) wrote in #note-6:
> ufuk (Ufuk Kayserilioglu) wrote in #note-4:
> ... But the dup/clone API is so old, so well established and so consistent throughout ruby, changing/extending it would be a mistake.

I am not sure we would be extending the API in any incompatible way. Calling `dup` with no arguments would still work as it should, by creating a copy of the original object. If one supplies, optional, keyword arguments to `dup`, though, then one would get the special copying that is suitable for `Data` objects. BTW, it should be an error to supply a keyword argument that the Data class does not have as members.

```ruby
Point = Data.define(:x, :y, :z)
ORIGIN = Point.new(x: 0.0, y: 0.0, z: 0.0)

cloned_origin = ORIGIN.dup # => works as expected
illegal = ORIGIN.dup(w: 1.0) # => ArgumentError
```


----------------------------------------
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-99825

* 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

* [ruby-core:110517] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (6 preceding siblings ...)
  2022-10-24 19:42 ` [ruby-core:110503] " ufuk (Ufuk Kayserilioglu)
@ 2022-10-26 18:16 ` RubyBugs (A Nonymous)
  2022-11-16 10:02 ` [ruby-core:110774] " tomstuart (Tom Stuart)
                   ` (32 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-10-26 18:16 UTC (permalink / raw)
  To: ruby-core

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


> it should be an error to supply a keyword argument that the Data class does not have as members

+1 Agree!

**Please note, performance work should be planned** to ensure that such checking of the keyword args is as fast as possible and avoids unnecessary allocations.

For example, here's a **real life** pull request which sped up this error checking the Values gem `#with` method by 2.29x:
https://github.com/ms-ati/Values/commit/c3f00e3e8d67ff5d6470315ad137166177f079e2

----------------------------------------
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-99841

* 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

* [ruby-core:110774] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (7 preceding siblings ...)
  2022-10-26 18:16 ` [ruby-core:110517] " RubyBugs (A Nonymous)
@ 2022-11-16 10:02 ` tomstuart (Tom Stuart)
  2022-11-17  7:33 ` [ruby-core:110791] " p8 (Petrik de Heus)
                   ` (31 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: tomstuart (Tom Stuart) @ 2022-11-16 10:02 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by tomstuart (Tom Stuart).


While it’s undeniable that this method duplicates its receiver in some sense, I’d like to add my support for the `#with` naming choice.

I don’t have any technical objection to using `#dup`, it’s just a stylistic preference: in the interests of programmer happiness, to me it’s more natural for the name to communicate the intent of the sender (“I want an object just like `ORIGIN`, except `#with` an `x` of `1.0`”) rather than the lower-level implementation detail of what that operation involves (“I want a shallow clone of `ORIGIN` via `#allocate` & `#initialize_copy`, except assigning `1.0` as the value of the `@x` instance variable”).

Perhaps another way of putting this is that I’d estimate most Ruby programmers rarely call `Object#dup` in the course of their work, whereas I would expect users of `Data` to need this new “copy with changes” operation somewhat frequently, especially if they’re migrating existing code from `Struct`. But of course I have no data to support that guess!

----------------------------------------
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-100119

* 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

* [ruby-core:110791] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (8 preceding siblings ...)
  2022-11-16 10:02 ` [ruby-core:110774] " tomstuart (Tom Stuart)
@ 2022-11-17  7:33 ` p8 (Petrik de Heus)
  2022-11-17 12:05 ` [ruby-core:110796] " nobu (Nobuyoshi Nakada)
                   ` (30 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: p8 (Petrik de Heus) @ 2022-11-17  7:33 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by p8 (Petrik de Heus).


I'm seeing some overlap with "Object#with to set and restore attributes around a block" https://bugs.ruby-lang.org/issues/18951

Should we have a single method `Object#with_attrs` that:
- returns a copy with the attributes set by default
- sets and restores the attributes when passed a block (although that doesn't make sense for Data)?

```ruby
Origin = Point.new(x: 0, y: 0)
right = Origin.with_attrs(x: 1.0)

SomeLibrary.with_attrs(enabled: true) do
  # test things
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-100143

* 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

* [ruby-core:110796] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (9 preceding siblings ...)
  2022-11-17  7:33 ` [ruby-core:110791] " p8 (Petrik de Heus)
@ 2022-11-17 12:05 ` nobu (Nobuyoshi Nakada)
  2022-11-17 12:28 ` [ruby-core:110797] " tomstuart (Tom Stuart)
                   ` (29 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: nobu (Nobuyoshi Nakada) @ 2022-11-17 12:05 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by nobu (Nobuyoshi Nakada).

Description updated

At the developers meeting today, someone proposed something like this.

> ```ruby
> point = Point.new(**(Origin.to_h.merge(change)))
> ```

```ruby
point = Point.new(**Origin.to_h, **change)
```


----------------------------------------
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-100152

* 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

* [ruby-core:110797] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (10 preceding siblings ...)
  2022-11-17 12:05 ` [ruby-core:110796] " nobu (Nobuyoshi Nakada)
@ 2022-11-17 12:28 ` tomstuart (Tom Stuart)
  2022-11-18  3:09 ` [ruby-core:110804] " mame (Yusuke Endoh)
                   ` (28 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: tomstuart (Tom Stuart) @ 2022-11-17 12:28 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by tomstuart (Tom Stuart).


nobu (Nobuyoshi Nakada) wrote in #note-11:
> At the developers meeting today, someone proposed something like this.
> 
> ```ruby
> point = Point.new(**Origin.to_h, **change)
> ```

Agreed — this is what I do at the moment. There’s no need to `#merge` explicitly because “last keyword wins”, and although repeating a literal keyword will generate a warning, the same isn’t true when double-splatting the result of `#to_h`.

----------------------------------------
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-100153

* 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

* [ruby-core:110804] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (11 preceding siblings ...)
  2022-11-17 12:28 ` [ruby-core:110797] " tomstuart (Tom Stuart)
@ 2022-11-18  3:09 ` mame (Yusuke Endoh)
  2022-11-18 20:19 ` [ruby-core:110820] " ufuk (Ufuk Kayserilioglu)
                   ` (27 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: mame (Yusuke Endoh) @ 2022-11-18  3:09 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by mame (Yusuke Endoh).


@RubyBugs I cannot understand the following parts of your first motivative example

```
# 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) }
```

According to the comment, do you really expect `Point.new(x: 0.0+0.5+0.5+0.5, y: 0.0-1.0)`? I don't think the proposed operation should mean addition implicitly.

If it is a mistake, could you update the code example?

----------------------------------------
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-100159

* 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

* [ruby-core:110820] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (12 preceding siblings ...)
  2022-11-18  3:09 ` [ruby-core:110804] " mame (Yusuke Endoh)
@ 2022-11-18 20:19 ` ufuk (Ufuk Kayserilioglu)
  2022-11-28 17:04 ` [ruby-core:111038] " bdewater (Bart de Water)
                   ` (26 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-11-18 20:19 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


I just put up a PR implementing this using the `dup` proposal: https://github.com/ruby/ruby/pull/6766

Here are my objections to other proposals:

1. `point = Point.new(**Origin.to_h, **change)` works but is very verbose and not intention revealing, in my opinion. For the reader of this code, it is very hard to understand what the intended operation is.
2. `with_attrs` (from https://bugs.ruby-lang.org/issues/18951): That proposal is explicitly about mutating the receiver of `with_attrs`, which is the exact opposite of what this "copy with changes" operation is trying to do. For that reason, I don't think we should be conflating that with this proposal.
3. `with`: While I understand that there is precedent for using `with` from other languages, I have always found the name to not make it clear that a copy is being made. Since `with` implies doing something "with" original receiver, it leads less experienced folks to think that the receiver might be modified in the process. I think we have an opportunity to make the operation more clear by giving it a better name.

My reasoning for `dup` is to make it explicit that a copy is being created (just as how a string copy is created when `"foo".dup` is called) but that the copy has updated values for the supplied member fields. `dup` is already an existing protocol for creating shallow copies of objects, and can already be used on `Data` class instance even without this change, of course without being able to affect the copy in any way. Extending the existing copying protocol of Ruby to optionally add the ability to affect the copy feels like the best way forward to me.

I would be happy to hear feedback from the community on my PR and if a different name than `dup` is picked in the end, also would be happy to update the PR to conform.

----------------------------------------
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-100177

* 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

* [ruby-core:111038] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (13 preceding siblings ...)
  2022-11-18 20:19 ` [ruby-core:110820] " ufuk (Ufuk Kayserilioglu)
@ 2022-11-28 17:04 ` bdewater (Bart de Water)
  2022-11-28 19:15 ` [ruby-core:111039] " RubyBugs (A Nonymous)
                   ` (25 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: bdewater (Bart de Water) @ 2022-11-28 17:04 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by bdewater (Bart de Water).


I like `dup` as the method name 👍

tomstuart (Tom Stuart) wrote in #note-9:
> Perhaps another way of putting this is that I’d estimate most Ruby programmers rarely call `Object#dup` in the course of their work, whereas I would expect users of `Data` to need this new “copy with changes” operation somewhat frequently, especially if they’re migrating existing code from `Struct`. But of course I have no data to support that guess!

I don't think `dup` is that rare, IME most Ruby programmers are familiar with it even if they don't use it daily. Some unscientific data points of usage in Rails apps:
- https://github.com/discourse/discourse/search?l=Ruby&q=dup
- https://github.com/mastodon/mastodon/search?l=Ruby&q=dup
- https://gitlab.com/search?search=dup&nav_source=navbar&project_id=278964&group_id=9970&search_code=true&repository_ref=master

----------------------------------------
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-100291

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

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

* [ruby-core:111039] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (14 preceding siblings ...)
  2022-11-28 17:04 ` [ruby-core:111038] " bdewater (Bart de Water)
@ 2022-11-28 19:15 ` RubyBugs (A Nonymous)
  2022-11-29  4:09 ` [ruby-core:111046] " mame (Yusuke Endoh)
                   ` (24 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-11-28 19:15 UTC (permalink / raw)
  To: ruby-core

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

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

* [ruby-core:111046] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (15 preceding siblings ...)
  2022-11-28 19:15 ` [ruby-core:111039] " RubyBugs (A Nonymous)
@ 2022-11-29  4:09 ` mame (Yusuke Endoh)
  2022-11-29 14:46 ` [ruby-core:111064] " p8 (Petrik de Heus)
                   ` (23 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: mame (Yusuke Endoh) @ 2022-11-29  4:09 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by mame (Yusuke Endoh).


@RubyBugs Please check my comment https://bugs.ruby-lang.org/issues/19000#note-13 . A wrong motivation example raises the suspicion that this API is actually confusing to users.

----------------------------------------
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-100307

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

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

* [ruby-core:111064] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (16 preceding siblings ...)
  2022-11-29  4:09 ` [ruby-core:111046] " mame (Yusuke Endoh)
@ 2022-11-29 14:46 ` p8 (Petrik de Heus)
  2022-11-30  3:13 ` [ruby-core:111080] " Eregon (Benoit Daloze)
                   ` (22 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: p8 (Petrik de Heus) @ 2022-11-29 14:46 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by p8 (Petrik de Heus).


If `dup` is chosen would it make sense to always allow `dup` methods to take arguments for consistency?

For example, it would allow the following code in Rails

```ruby
firm = Firm.first.dup
firm.account = Account.first
assert_queries(2) { firm.save! }
```
(https://github.com/rails/rails/blob/8e34831f97acd7448fd68a6ac130694079aea951/activerecord/test/cases/autosave_association_test.rb#L277-L279)

to be written as:
```ruby
firm = Firm.first.dup(account: Account.first)
assert_queries(2) { firm.save! }
```


----------------------------------------
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-100324

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

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

* [ruby-core:111080] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (17 preceding siblings ...)
  2022-11-29 14:46 ` [ruby-core:111064] " p8 (Petrik de Heus)
@ 2022-11-30  3:13 ` Eregon (Benoit Daloze)
  2022-11-30  3:20 ` [ruby-core:111081] " Dan0042 (Daniel DeLorme)
                   ` (21 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: Eregon (Benoit Daloze) @ 2022-11-30  3:13 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by Eregon (Benoit Daloze).


p8 (Petrik de Heus) wrote in #note-18:
> If `dup` is chosen would it make sense to always allow `dup` methods to take arguments for consistency?

I'm rather against that, it makes the standard `dup` so much more complicated, and it would most likely be pretty slow in CRuby.
If it's only for Data it's a much smaller concern.

----------------------------------------
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-100340

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

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

* [ruby-core:111081] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (18 preceding siblings ...)
  2022-11-30  3:13 ` [ruby-core:111080] " Eregon (Benoit Daloze)
@ 2022-11-30  3:20 ` Dan0042 (Daniel DeLorme)
  2022-11-30 19:52 ` [ruby-core:111097] " RubyBugs (A Nonymous)
                   ` (20 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: Dan0042 (Daniel DeLorme) @ 2022-11-30  3:20 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by Dan0042 (Daniel DeLorme).


p8 (Petrik de Heus) wrote in #note-18:
> If `dup` is chosen would it make sense to always allow `dup` methods to take arguments for consistency?

It would make sense, but it would require everyone who wrote a custom `#dup` to extend their version to support this new API pattern. Not likely to happen.

On the other hand with `#with` it would be possible to implement a general version for all objects
```ruby
def with(attr={})
  attr.each_with_object(self.dup) do |(k,v),obj|
    obj.send("#{k}=", v)
  end
end
```
(but let's keep in mind this proposal is about Data#with, not Object#with)

----------------------------------------
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-100341

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

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

* [ruby-core:111097] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (19 preceding siblings ...)
  2022-11-30  3:20 ` [ruby-core:111081] " Dan0042 (Daniel DeLorme)
@ 2022-11-30 19:52 ` RubyBugs (A Nonymous)
  2022-11-30 19:58 ` [ruby-core:111098] " RubyBugs (A Nonymous)
                   ` (19 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-11-30 19:52 UTC (permalink / raw)
  To: ruby-core

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


mame (Yusuke Endoh) wrote in #note-17:
> @RubyBugs Please check my comment https://bugs.ruby-lang.org/issues/19000#note-13 . A wrong motivation example raises the suspicion that this API is actually confusing to users.

Quite right @mame, thank you for catching that! I provided two examples. The first example is intended to use the previous values in creating the next values, and that part was broken. 

How is this?

```ruby
##
# Example of proposed Data#with
#
# To try this out:
#
#     ruby-install ruby-3.2.0-preview3
#
##

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  
end

Origin = Point.new(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)

begin
  Origin.with(x: 1, z: 0)
rescue ArgumentError
  puts "#with(z: 0) raised ArgumentError as expected"
end

# In loops
movements = [
  [ :x, +0.5 ],
  [ :x, +0.5 ],
  [ :y, -1.0 ],
  [ :x, +0.5 ],
]

position = movements.inject(Origin) do |p, (field, delta)|
  p.with(field => p.to_h[field] + delta) 
end

# position = Point(x: 1.5, y: -1.0)
```

----------------------------------------
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-100363

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

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

* [ruby-core:111098] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (20 preceding siblings ...)
  2022-11-30 19:52 ` [ruby-core:111097] " RubyBugs (A Nonymous)
@ 2022-11-30 19:58 ` RubyBugs (A Nonymous)
  2022-12-02  2:54 ` [ruby-core:111141] " mame (Yusuke Endoh)
                   ` (18 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-11-30 19:58 UTC (permalink / raw)
  To: ruby-core

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/

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

* [ruby-core:111141] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (21 preceding siblings ...)
  2022-11-30 19:58 ` [ruby-core:111098] " RubyBugs (A Nonymous)
@ 2022-12-02  2:54 ` mame (Yusuke Endoh)
  2022-12-02  3:13 ` [ruby-core:111142] " mame (Yusuke Endoh)
                   ` (17 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: mame (Yusuke Endoh) @ 2022-12-02  2:54 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by mame (Yusuke Endoh).


RubyBugs (A Nonymous) wrote in #note-21:
> How is this?

Thanks for the update.

Now I have a question. Do you really want to write `p.with(field => p.send(field) + delta)`? I don't think it is very elegant. It is not very convincing (at least, to me) as a first motivation example.

Also, do you need the ability to update multiple fields at once? Both motivation examples only update a single field. This may be the result of simplifying the motivation example, though.

Looking at these motivation examples, there may be room to consider an API like `p.with(field) {|old_value| old_value + delta }` or something.

----------------------------------------
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-100421

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111142] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (22 preceding siblings ...)
  2022-12-02  2:54 ` [ruby-core:111141] " mame (Yusuke Endoh)
@ 2022-12-02  3:13 ` mame (Yusuke Endoh)
  2022-12-02 17:05 ` [ruby-core:111162] " ufuk (Ufuk Kayserilioglu)
                   ` (16 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: mame (Yusuke Endoh) @ 2022-12-02  3:13 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by mame (Yusuke Endoh).


Discussed at the dev meeting.

Regarding method names, @matz likes `Data#update`. However, @ko1 objects because "update" is used as a destructive vocabulary in `Hash#update`. @matz said Data#with` is acceptable; Data#dup` is not acceptable.

----------------------------------------
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-100422

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111162] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (23 preceding siblings ...)
  2022-12-02  3:13 ` [ruby-core:111142] " mame (Yusuke Endoh)
@ 2022-12-02 17:05 ` ufuk (Ufuk Kayserilioglu)
  2022-12-03  2:12 ` [ruby-core:111169] " matz (Yukihiro Matsumoto)
                   ` (15 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-12-02 17:05 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


Thanks for the discussion and the name suggestion. I updated the PR https://github.com/ruby/ruby/pull/6766 to use `Data#with`. I would be grateful if it could get a review.

However, that raises a question: How should `Data#with` behave if it is passed no arguments? Semantically, that should mean "create a copy of the current data object with no fields changed", but then that ends up being identical to what `Kernel#dup` does. Also, `with` with no arguments does not read that well. 

@matz and @mame Should it be an error?

Example:
```ruby
Point = Data.define(:x, :y)
origin = Point.new(x: 0, y: 0)
new_origin = origin.with # should this be an error?
```


----------------------------------------
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-100445

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111169] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (24 preceding siblings ...)
  2022-12-02 17:05 ` [ruby-core:111162] " ufuk (Ufuk Kayserilioglu)
@ 2022-12-03  2:12 ` matz (Yukihiro Matsumoto)
  2022-12-03  2:56 ` [ruby-core:111170] " ufuk (Ufuk Kayserilioglu)
                   ` (14 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: matz (Yukihiro Matsumoto) @ 2022-12-03  2:12 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by matz (Yukihiro Matsumoto).


I think it should be an error. I am still not 100% sure `with` is the best name.

Matz.


----------------------------------------
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-100454

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111170] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (25 preceding siblings ...)
  2022-12-03  2:12 ` [ruby-core:111169] " matz (Yukihiro Matsumoto)
@ 2022-12-03  2:56 ` ufuk (Ufuk Kayserilioglu)
  2022-12-03 14:09 ` [ruby-core:111173] " RubyBugs (A Nonymous)
                   ` (13 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-12-03  2:56 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


Thank you @matz. I will update the implementation to make the no-args case an error, and point people to use `dup` to make identical shallow clones instead in the error message.

I read the dev meeting notes and saw that you made a point about the main of the operation not being about duplication. While I agree with that, I also feel like that is making people think that they are making a cheap operation, when they are actually creating a clone with modified values, isn't the best interface. I had suggested `dup` to make that explicit, but happy to go with another name that is chosen that gives a similar sense of an extra clone operation happening under the hood. Another name that was suggested was a `dup_with` that I also find acceptable personally.

Along the same lines, I feel like `update` gives the wrong impression that the receiver instance will be updated, so my vote would be against it.

----------------------------------------
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-100455

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111173] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (26 preceding siblings ...)
  2022-12-03  2:56 ` [ruby-core:111170] " ufuk (Ufuk Kayserilioglu)
@ 2022-12-03 14:09 ` RubyBugs (A Nonymous)
  2022-12-03 14:26 ` [ruby-core:111174] " tomstuart (Tom Stuart)
                   ` (12 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-12-03 14:09 UTC (permalink / raw)
  To: ruby-core

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


ufuk (Ufuk Kayserilioglu) wrote in #note-28:
> Thank you @matz. I will update the implementation to make the no-args case an error, and point people to use `dup` to make identical shallow clones instead in the error message.
> 

Hi Matz and Ufuk, please consider that making no-args an error makes programming around this API dynamically more complicated — to generate a set of changes as a Hash and pass then goes from:

```ruby
changes = {} # calculated somehow
p = Origin.with(**changes) # p == Origin is ok
```

to:

```ruby
changes = {} # calculated somehow
p = unless changes.empty
      Origin.with(**changes)
    else
      Origin
    end
```

Because this is a common way to use such an API, would we consider allowing calling `#with` with no arguments to return `self`?

> I read the dev meeting notes and saw that you made a point about the main of the operation not being about duplication. While I agree with that, I also feel that makes people think that they are making a cheap operation, when they are actually creating a clone with modified values. I had suggested `dup` to make that explicit, but happy to go with another name that is chosen that gives a similar sense of an extra clone operation happening under the hood. Another name that was suggested was a `dup_with` that I also find acceptable personally.
> 
> Along the same lines, I feel like `update` gives the wrong impression that the receiver instance will be updated, so my vote would be against it.



----------------------------------------
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-100457

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111174] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (27 preceding siblings ...)
  2022-12-03 14:09 ` [ruby-core:111173] " RubyBugs (A Nonymous)
@ 2022-12-03 14:26 ` tomstuart (Tom Stuart)
  2022-12-03 14:44 ` [ruby-core:111175] " ufuk (Ufuk Kayserilioglu)
                   ` (11 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: tomstuart (Tom Stuart) @ 2022-12-03 14:26 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by tomstuart (Tom Stuart).


RubyBugs (A Nonymous) wrote in #note-29:
> please consider that making no-args an error makes programming around this API dynamically more complicated

I second this request. In the interests of regularity, it should be fine to ask for a `Data` instance `#with` “no changes”, otherwise the programmer has to anticipate & handle that as an edge case unnecessarily if they intend to generically handle changes which originate from elsewhere rather than supplying keyword arguments inline.

Whether doing so returns `self` or a shallow copy shouldn’t be very important since the instances are immutable, but I agree that returning `self` would be more semantically predictable, slightly more efficient, and a nice way to differentiate it from the behaviour of `#dup`.

----------------------------------------
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-100458

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111175] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (28 preceding siblings ...)
  2022-12-03 14:26 ` [ruby-core:111174] " tomstuart (Tom Stuart)
@ 2022-12-03 14:44 ` ufuk (Ufuk Kayserilioglu)
  2022-12-03 14:49 ` [ruby-core:111176] " austin (Austin Ziegler)
                   ` (10 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-12-03 14:44 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


RubyBugs (A Nonymous) wrote in #note-29:
> Hi Matz and Ufuk, please consider that making no-args an error makes programming around this API dynamically more complicated

Indeed, that is true. However, personally, I find a `with` call with no arguments very hard to read and reason about. It just reads badly as an English construct. It is like saying, "can I have tea with".

> Semantically one could describe this API behavior as: “an immutable value object with no changes is itself”?

Indeed this is a good idea for what the behaviour should be but does not alleviate the  naming/readability concern that I've raised above.

> As another reference point: in Scala `case class` this operation is called `#copy`, and I think it returns the original object instance in the no-args case.
> https://docs.scala-lang.org/tour/case-classes.html

Indeed, it was along these lines that I'd suggested `dup` originally. Like I've said before, I am flexible on what the name should be and `copy` actually addresses the naming concern equally as well as `dup` in my opinion. It also makes sense to ask for a "copy" with no values changed and readability is not hurt in any way.


----------------------------------------
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-100459

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111176] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (29 preceding siblings ...)
  2022-12-03 14:44 ` [ruby-core:111175] " ufuk (Ufuk Kayserilioglu)
@ 2022-12-03 14:49 ` austin (Austin Ziegler)
  2022-12-03 16:15 ` [ruby-core:111178] " ufuk (Ufuk Kayserilioglu)
                   ` (9 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: austin (Austin Ziegler) @ 2022-12-03 14:49 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by austin (Austin Ziegler).


ufuk (Ufuk Kayserilioglu) wrote in #note-31:
> RubyBugs (A Nonymous) wrote in #note-29:
> > As another reference point: in Scala `case class` this operation is called `#copy`, and I think it returns the original object instance in the no-args case.
> > https://docs.scala-lang.org/tour/case-classes.html
> 
> Indeed, it was along these lines that I'd suggested `dup` originally. Like I've said before, I am flexible on what the name should be and `copy` actually addresses the naming concern equally as well as `dup` in my opinion. It also makes sense to ask for a "copy" with no values changed and readability is not hurt in any way.

I think that `#copy` is OK, but this is more of a (checked?) `#merge`, right? I think that would be where my question comes in. If `origin` is a `Point` containing `x` and `y` values, then `origin#copy(x: 1, y: 2, z: 3)` should do…what? Should it error or just take the values that it knows? Because the *shape* provided to `#copy` is no longer a `Point` shape.

----------------------------------------
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-100460

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111178] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (30 preceding siblings ...)
  2022-12-03 14:49 ` [ruby-core:111176] " austin (Austin Ziegler)
@ 2022-12-03 16:15 ` ufuk (Ufuk Kayserilioglu)
  2022-12-03 16:29 ` [ruby-core:111180] " RubyBugs (A Nonymous)
                   ` (8 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) @ 2022-12-03 16:15 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


austin (Austin Ziegler) wrote in #note-32:

> If `origin` is a `Point` containing `x` and `y` values, then `origin#copy(x: 1, y: 2, z: 3)` should do…what? Should it error or just take the values that it knows?

The [current implementation in the PR](https://github.com/ruby/ruby/pull/6766) treats unknown keywords passed to this method as an error. I had already made it clear that I thought it should be an error at the end of my message earlier in this thread https://bugs.ruby-lang.org/issues/19000#note-7 and I think we are all in agreement on this point in this thread.

----------------------------------------
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-100462

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111180] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (31 preceding siblings ...)
  2022-12-03 16:15 ` [ruby-core:111178] " ufuk (Ufuk Kayserilioglu)
@ 2022-12-03 16:29 ` RubyBugs (A Nonymous)
  2022-12-07 20:44 ` [ruby-core:111235] " RubyBugs (A Nonymous)
                   ` (7 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-12-03 16:29 UTC (permalink / raw)
  To: ruby-core

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



> …treats unknown keywords passed to this method as an error. I had already made it clear that I thought it should be an error at the end of my message earlier in this thread … and I think we are all in agreement on this point in this thread.

**+1** agreement on treating unknown keywords as error! :-)

----------------------------------------
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-100464

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111235] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (32 preceding siblings ...)
  2022-12-03 16:29 ` [ruby-core:111180] " RubyBugs (A Nonymous)
@ 2022-12-07 20:44 ` RubyBugs (A Nonymous)
  2022-12-08 14:41 ` [ruby-core:111243] " Eregon (Benoit Daloze)
                   ` (6 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: RubyBugs (A Nonymous) @ 2022-12-07 20:44 UTC (permalink / raw)
  To: ruby-core

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


Hi @mame! Thank you for your questions.

mame (Yusuke Endoh) wrote in #note-24:
> Thanks for the update.
> 
> Now I have a question. Do you really want to write `p.with(field => p.send(field) + delta)`? I don't think it is very elegant. It is not very convincing (at least, to me) as a first motivation example.
> 

The challenge is that this operation "make a copy of an immutable value object with 0 or more fields changed" is so fundamental, it's hard to find examples that strip away every other concern.

Staying with the `Point` example, would **translate** and **invert** work better as examples that change multiple fields?

```ruby
##
# Example of proposed Data#with
#
# To try this out:
#
#     ruby-install ruby-3.2.0-preview3
#
##

Point3d = Data.define(:x, :y, :z) do
  # Example only, too slow due to allocations
  def with(**args)
    raise ArgumentError unless args.keys.all? { |k| members.include? k }
    self.class.new(**(to_h.merge(args)))
  end  

  def translate_2d(dx: 0, dy: 0)
    with(x: x + dx, y: y + dy)
  end

  def invert_2d
    with(x: -x, y: -y)
  end  
end

Origin3d = Point3d.new(x: 0, y: 0, z:0)
north = Origin3d.translate_2d(dy: 1.0)
south = north.invert_2d
east = Origin3d.translate_2d(dx: 1.0)
west = east.invert_2d

west == Point3d.new(x: -1.0, y: 0, z: 0)
# => true

mountain_height = 2.0
western_mountain = west.with(z: mountain_height)
# => #<data Point3d x=-1.0, y=0, z=2.0>
```

> Also, do you need the ability to update multiple fields at once? Both motivation examples only update a single field. This may be the result of simplifying the motivation example, though.
> 

Yes! Definitely need to update multiple fields at once

> Looking at these motivation examples, there may be room to consider an API like `p.with(field) {|old_value| old_value + delta }` or something.

Agreed that this probably comes out of a motivating example which is trying to calculate the new value for a **single** field based on **only** the previous value of that field.

----------------------------------------
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-100524

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111243] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (33 preceding siblings ...)
  2022-12-07 20:44 ` [ruby-core:111235] " RubyBugs (A Nonymous)
@ 2022-12-08 14:41 ` Eregon (Benoit Daloze)
  2022-12-20  7:13 ` [ruby-core:111338] " k0kubun (Takashi Kokubun) via ruby-core
                   ` (5 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: Eregon (Benoit Daloze) @ 2022-12-08 14:41 UTC (permalink / raw)
  To: ruby-core

Issue #19000 has been updated by Eregon (Benoit Daloze).


I also agree the 0-kwargs case should be supported for general use like `data.with(**changes)`, whether `changes` is empty or not. And that reads fine of course.
It's also like `Hash#merge` with no arguments.
I think `#with` with empty kwargs should do the same as `dup`. So this depends on what `Data#dup` does.
If `Data#dup` returns `self` like immutable objects (Fixnum/Symbol/true/false/nil/etc) then let's do that too, if not let's dup in that case.
Note that `Data` is not truly immutable (nor always Ractor-shareable) because it can refer to mutable objects.
Currently `Data#dup` creates a new instance.

----------------------------------------
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-100533

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111338] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (34 preceding siblings ...)
  2022-12-08 14:41 ` [ruby-core:111243] " Eregon (Benoit Daloze)
@ 2022-12-20  7:13 ` k0kubun (Takashi Kokubun) via ruby-core
  2022-12-20 16:27 ` [ruby-core:111352] " Eregon (Benoit Daloze) via ruby-core
                   ` (4 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: k0kubun (Takashi Kokubun) via ruby-core @ 2022-12-20  7:13 UTC (permalink / raw)
  To: ruby-core; +Cc: k0kubun (Takashi Kokubun)

Issue #19000 has been updated by k0kubun (Takashi Kokubun).


Summary of the discussion so far:

* Interface
  * `data.xxx(foo: bar)`, making `data.xxx` (no argument) an error
    * Matz #note-27 is in favor of this.
    * This might make dynamic use of this API harder #note-29 
  * `data.xxx(foo: bar)`, making `data.xxx` (no argument) not an error
  * `data.xxx(foo) { bar }` #note-27
* Method name
  * `with`
    * Proposal: [Feature #19000] by @RubyBugs
    * Other languages: C#, Java (WIP), 
    * Existing gems: sorbet, value_semantics
    * Pull request: https://github.com/ruby/ruby/pull/6766
    * Matz #note-25 said it's acceptable.
  * `update`
    * Proposal: #note-25
    * Matz #note-25 liked this, but ko1 didn't.
  * `dup`
    * Proposal: #note-4 by @ufuk
    * Matz #note-25 said it's not acceptable.
  * `copy`
    * Proposal: #note-29 by @RubyBugs
    * Other languages: Scala, Kotlin
  * `clone`
    * Proposal: #note-5 by @jeremyevans0
    * Existing gems: sequel
* Alternatives
  * `Point.new(foo: Origin.foo, **change)`
  * `Point.new(**(Origin.to_h.merge(change)))`
  * `Point.new(**Origin.to_h, **change)`
    * Suggested at the Ruby developers' meeting in November

---

My personal notes:

I'm late to the party, but in my experience of using data classes in other languages, this feels like a must-have for data classes. It'd be unfortunate if Data were released without it.

> > Also, do you need the ability to update multiple fields at once? Both motivation examples only update a single field.
>
> Yes! Definitely need to update multiple fields at once

I second that. In my previous company, we had an immutable class that represents a state/snapshot for stream-processing multiple events at once (with Amazon Kinesis). It groups multiple events and dispatches a batch operation when a certain threshold is met. Because we had multiple thresholds (e.g. max size, max duration), we naturally needed to create a copy with multiple updates when we create the next state.

> I also agree the 0-kwargs case should be supported for general use like data.with(**changes), whether changes is empty or not. And that reads fine of course.
> It's also like Hash#merge with no arguments.

+1. I'm not sure if it's possible to distinguish them in Ruby, but ideally `data.with` or `data.with()` should be rejected even if we accept `data.with(**changes)`. If it's feasible, then it might clear Matz's concern at #note-27.

----------------------------------------
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-100714

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111352] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (35 preceding siblings ...)
  2022-12-20  7:13 ` [ruby-core:111338] " k0kubun (Takashi Kokubun) via ruby-core
@ 2022-12-20 16:27 ` Eregon (Benoit Daloze) via ruby-core
  2022-12-20 22:02 ` [ruby-core:111358] " ufuk (Ufuk Kayserilioglu) via ruby-core
                   ` (3 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: Eregon (Benoit Daloze) via ruby-core @ 2022-12-20 16:27 UTC (permalink / raw)
  To: ruby-core; +Cc: Eregon (Benoit Daloze)

Issue #19000 has been updated by Eregon (Benoit Daloze).


k0kubun (Takashi Kokubun) wrote in #note-37:
> +1. I'm not sure if it's possible to distinguish them in Ruby, but ideally `data.with` or `data.with()` should be rejected even if we accept `data.with(**changes)` when `changes` are empty.

That's not possible to differentiate and also it shouldn't be (it would be very confusing if it was different).

> If it's feasible, then it might clear Matz's concern at #note-27.

@matz Is it really that big a concern?
I think it's far far more important that `with(**changes)` works, whether `changes` is empty or not.
Honestly I think there is no point to prevent that, it's necessary for the `with(**changes)` use case and it seems pretty much harmless.

----------------------------------------
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-100726

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111358] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (36 preceding siblings ...)
  2022-12-20 16:27 ` [ruby-core:111352] " Eregon (Benoit Daloze) via ruby-core
@ 2022-12-20 22:02 ` ufuk (Ufuk Kayserilioglu) via ruby-core
  2022-12-21 23:37 ` [ruby-core:111371] " matz (Yukihiro Matsumoto) via ruby-core
                   ` (2 subsequent siblings)
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) via ruby-core @ 2022-12-20 22:02 UTC (permalink / raw)
  To: ruby-core; +Cc: ufuk (Ufuk Kayserilioglu)

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


**Status update**

@alanwu and I reworked the PR at https://github.com/ruby/ruby/pull/6766 to ensure the following:

1. Calls to `.with` with no arguments is valid and returns the receiver as-is. As @eregon said, there is no way to differentiate between no args and empty kwargs, so the ability to do `.with(**hash)` freely won the discussion over.
2. The implementation works correctly with Data objects being frozen now.
3. The implementation checks explicitly for keyword arguments and raises argument error for hashes passed as positional argument, as well.

Apart from the decision on the name, the PR should be good to merge. As @k0kubun said above, I also feel like this should be a part of the Ruby 3.2 release.

If the only blocker is the name, @matz can you pick the final name in the next few days, so that we can ship this with 3.2?

Thank you everyone for the discussion and ideas.

----------------------------------------
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-100734

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111371] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (37 preceding siblings ...)
  2022-12-20 22:02 ` [ruby-core:111358] " ufuk (Ufuk Kayserilioglu) via ruby-core
@ 2022-12-21 23:37 ` 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
  40 siblings, 0 replies; 42+ messages in thread
From: matz (Yukihiro Matsumoto) via ruby-core @ 2022-12-21 23:37 UTC (permalink / raw)
  To: ruby-core; +Cc: matz (Yukihiro Matsumoto)

Issue #19000 has been updated by matz (Yukihiro Matsumoto).


`with` seems slightly better than other candidates. `Data` is (or can be considered as) a value object without identity. So names with identity implication (`dup`, `copy`, `clone`) are not ideal. I like `update` but I understand @ko1's concern (`Hash#update` modifies the receiver).

Regarding error-on-no-argument, I thought passing no argument (even via double splat `**`) indicates some kind of logic error, and honored fail-early principle, but as @eregon said, it's not a big deal.

I am not sure @naruse accepts this last minute change, but I am OK with the last PR from @ufuk.

Matz.

----------------------------------------
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-100745

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111373] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (38 preceding siblings ...)
  2022-12-21 23:37 ` [ruby-core:111371] " matz (Yukihiro Matsumoto) via ruby-core
@ 2022-12-22  0:23 ` naruse (Yui NARUSE) via ruby-core
  2022-12-22  0:46 ` [ruby-core:111374] " ufuk (Ufuk Kayserilioglu) via ruby-core
  40 siblings, 0 replies; 42+ messages in thread
From: naruse (Yui NARUSE) via ruby-core @ 2022-12-22  0:23 UTC (permalink / raw)
  To: ruby-core; +Cc: naruse (Yui NARUSE)

Issue #19000 has been updated by naruse (Yui NARUSE).


I'm also ok to merge!
This will ship as a part of Ruby 3.2!

----------------------------------------
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-100747

* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

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

* [ruby-core:111374] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
  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)
                   ` (39 preceding siblings ...)
  2022-12-22  0:23 ` [ruby-core:111373] " naruse (Yui NARUSE) via ruby-core
@ 2022-12-22  0:46 ` ufuk (Ufuk Kayserilioglu) via ruby-core
  40 siblings, 0 replies; 42+ messages in thread
From: ufuk (Ufuk Kayserilioglu) via ruby-core @ 2022-12-22  0:46 UTC (permalink / raw)
  To: ruby-core; +Cc: ufuk (Ufuk Kayserilioglu)

Issue #19000 has been updated by ufuk (Ufuk Kayserilioglu).


Thank you @matz and @naruse. I hate that this ended up being a last-minute change, but it seemed to be little risk adding a method to a new interface and the benefit seemed to greatly outweigh the risk. Otherwise, I would not have asked for consideration.

Thanks again for your understanding. 🙇‍♂️

----------------------------------------
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-100749

* Author: RubyBugs (A Nonymous)
* Status: Closed
* 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
require "values"

# A new class
Point = Value.new(: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) do |p, (field, delta)|
  p.with(field => p.send(field) + delta)
end
```

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

^ 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).