ml.ruby-lang.org
Sign In Sign Up
Manage this list Sign In Sign Up

Keyboard Shortcuts

Thread View

  • j: Next unread message
  • k: Previous unread message
  • j a: Jump to all threads
  • j l: Jump to MailingList overview

ruby-core

Thread Start a new thread
Download
Threads by month
  • ----- 2025 -----
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2024 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2023 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2022 -----
  • December
  • November
ruby-core@ml.ruby-lang.org

  • 5 participants
  • 3280 discussions
[ruby-core:111179] [Ruby master Feature#16122] Data: simple immutable value object
by ston1x (Nicolai Stoianov) 03 Dec '22

03 Dec '22
Issue #16122 has been updated by ston1x (Nicolai Stoianov). Thanks a lot for implementing this feature! Can't wait to start applying it in specific use-cases. However, I am also wondering if it is possible to define such `Data`-derived classes in a "traditional way", meaning something like: ```ruby class Ticket < Data # "attrs" is just for example here, might be something different. attrs :event_id, :user_id, :start_at # And other methods defined below def validate puts "Validated!" end end # And then just using it the same way as described in the PR: ticket = Ticket.new( event_id: 78, user_id: 584, start_at: '2022-12-03 15:00:00' ) ``` I guess this might come in handy for IDEs and is simply common across codebases in Ruby. Please correct me if I'm wrong or if you've also considered similar assumptions but decided to not implement it on purpose. ---------------------------------------- Feature #16122: Data: simple immutable value object https://bugs.ruby-lang.org/issues/16122#change-100463 * Author: zverok (Victor Shepelev) * Status: Closed * Priority: Normal * Assignee: zverok (Victor Shepelev) ---------------------------------------- ## Intro (original theoretical part of the proposal) **Value Object** is a useful concept, introduced by Martin Fowler ([his post](https://martinfowler.com/bliki/ValueObject.html), [Wikipedia Entry](https://en.wikipedia.org/wiki/Value_object)) with the following properties (simplifying the idea): * representing some relatively simple data; * immutable; * compared by type & value; * nicely represented. Value objects are super-useful especially for defining APIs, their input/return values. Recently, there were some movement towards using more immutability-friendly approach in Ruby programming, leading to creating several discussions/libraries with value objects. For example, [Tom Dalling's gem](https://github.com/tomdalling/value_semantics), [Good Ruby Value object convention](https://github.com/zverok/good-value-object) (disclaimer: the latter is maintained by yours truly). I propose to introduce **native value objects** to Ruby as a core class. **Why not a gem?** * I believe that concept is that simple, that nobody *will even try* to use a gem for representing it with, unless the framework/library used already provides one. * Potentially, a lot of standard library (and probably even core) APIs could benefit from the concept. **Why `Struct` is not enough** Core `Struct` class is "somewhat alike" value-object, and frequently used instead of one: it is compared by value and consists of simple attributes. On the other hand, `Struct` is: * mutable; * collection-alike (defines `to_a` and is `Enumerable`); * dictionary-alike (has `[]` and `.values` methods). The above traits somehow erodes the semantics, making code less clear, especially when duck-typing is used. For example, this code snippet shows why `to_a` is problematic: ```ruby Result = Struct.new(:success, :content) # Now, imagine that other code assumes `data` could be either Result, or [Result, Result, Result] # So, ... data = Result.new(true, 'it is awesome') Array(data) # => expected [Result(true, 'it is awesome')], got [true, 'it is awesome'] # or... def foo(arg1, arg2 = nil) p arg1, arg2 end foo(*data) # => expected [Result(true, 'it is awesome'), nil], got [true, 'it is awesome'] ``` Having `[]` and `each` defined on something that is thought as "just value" can also lead to subtle bugs, when some method checks "if the received argument is collection-alike", and value object's author doesn't thought of it as a collection. ## `Data` class: consensus proposal/implementation, Sep 2022 * Name: `Data` * PR: https://github.com/ruby/ruby/pull/6353 * Example docs rendering: https://zverok.space/ruby-rdoc/Data.html * Full API: * `Data::define` creates a new Data class; accepts only symbols (no `keyword_init:`, no "first argument is the class name" like the `Struct` had) * `<data_class>::members`: list of member names * `<data_class>::new`: accepts either keyword or positional arguments (but not mix); converts all of the to keyword args; raises `ArgumentError` if there are **too many positional arguments** * `#initialize`: accepts only keyword arguments; the default implementation raises `ArgumentError` on missing or extra arguments; it is easy to redefine `initialize` to provide defaults or handle extra args. * `#==` * `#eql?` * `#inspect`/`#to_s` (same representation) * `#deconstruct` * `#deconstruct_keys` * `#hash` * `#members` * `#to_h` ## Historical original proposal * Class name: `Struct::Value`: lot of Rubyists are used to have `Struct` as a quick "something-like-value" drop-in, so alternative, more strict implementation, being part of `Struct` API, will be quite discoverable; *alternative: just `Value`* * Class API is copying `Struct`s one (most of the time -- even reuses the implementation), with the following exceptions *(note: the immutability is **not** the only difference)*: * Not `Enumerable`; * Immutable; * Doesn't think of itself as "almost hash" (doesn't have `to_a`, `values` and `[]` methods); * Can have empty members list (fun fact: `Struct.new('Foo')` creating member-less `Struct::Foo`, is allowed, but `Struct.new()` is not) to allow usage patterns like: ```ruby class MyService Success = Struct::Value.new(:results) NotFound = Struct::Value.new end ``` `NotFound` here, unlike, say, `Object.new.freeze` (another pattern for creating "empty typed value object"), has nice inspect `#<value NotFound>`, and created consistently with the `Success`, making the code more readable. And if it will evolve to have some attributes, the code change would be easy. **Patch is provided** [Sample rendered RDoc documentation](https://zverok.github.io/ruby-rdoc/Struct-Value.html) ---Files-------------------------------- struct_value.patch (18.6 KB) -- https://bugs.ruby-lang.org/
1 0
0 0
[ruby-core:111178] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by ufuk (Ufuk Kayserilioglu) 03 Dec '22

03 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
[ruby-core:111177] [Ruby master Feature#17325] Adds Fiber#cancel, which forces a Fiber to break/return
by nevans (Nicholas Evans) 03 Dec '22

03 Dec '22
Issue #17325 has been updated by nevans (Nicholas Evans). oh, I still very much want the "break"/"return" semantic, and I still think it should be invoked via Fiber#cancel (not a special exception class with Fiber#raise). But I think the semantics and implementation should be slightly different from this proposal (I don't have time to put together a new proposal before 3.2). So I closed the associated PR. But we could leave the ticket open, at least until it is superceded by a better proposal. ---------------------------------------- Feature #17325: Adds Fiber#cancel, which forces a Fiber to break/return https://bugs.ruby-lang.org/issues/17325#change-100461 * Author: nevans (Nicholas Evans) * Status: Open * Priority: Normal ---------------------------------------- Calling `Fiber#cancel` will force a fiber to return, skipping rescue and catch blocks but running all ensure blocks. It behaves as if a `break` or `return` were used to jump from the last suspension point to the top frame of the fiber. Control will be transferred to the canceled fiber so it can run its ensure blocks. ## Propagation from resuming to resumed fibers Any non-root living fiber can be canceled and cancellation will propagate to child (resumed) fibers. In this way, a suspended task can be canceled even if it is e.g. resuming into an enumerator, and the enumerator will be canceled as well. Transfer of control should match #17221's *(much improved)* transfer/resume semantics. After the cancellation propagates all the way to the bottom of the fiber resume stack, the last fiber in the chain will then be resumed. Resuming fibers will not run until they are yielded back into. ## Suspension of canceled fibers Canceled fibers can still transfer control with `resume`, `yield`, and `transfer`, which may be necessary in order to release resources from `ensure` blocks. For simplicity, subsequent cancels will behave similarly to calling `break` or `return` inside an `ensure` block, and the last cancellation reason will overwrite earlier reasons. ## Alternatives `Fiber#raise` could be used, but: * Can only raise on resumable fibers. * Cannot propagate cancellation down to resumed fibers. * Exceptions are bigger and slower than `break`. * `#raise` can't (and shouldn't) be sent to resuming fibers. (It can't propagate.) * Exceptions can be caught. This might be desirable, but that should be at the discretion of the calling fiber. Catch/Throw could be used (with an anonymous `Object.new`), but: * We would need to add `Fiber#throw` (or wrap/intercept `Fiber.yield`). * A hypothetical `Fiber#throw` should probably have similar semantics to `#resume` and thus only be allowed on resumable fibers. * In that case, it wouldn't propagate down to resumed fibers. * `catch` adds an extra stack frame. We could use go-style "Context" objects that contain a "done?" queue/future. * These would need to be explicitly passed around. * Although their usage could be enforced via linters like rubocop, I think that placing it off to the side will give developers the impression that it is optional Some sort of cancel propagation mechanism is not optional for structured concurrency. * It should built into any task-scheduler library, which would allow application code to use it explicitly. * But this suffers the same problem as current Fiber wrappers: it works fine if your code uses the wrapper, but code that uses fibers without the wrapper can be incompatible and introduce bugs (e.g. fibers that are released without running their `ensure` blocks). * This make sense for a language like go which doesn't have exceptions but does have a convention of returning an "error" value. It feels out of place in ruby, IMO. Letting the fiber-task-scheduler mitigates that... for code that uses the fiber-task-scheduler. We could add a keyword option to `Fiber#raise` that gives it similar propagation semantics to this. * IMO, the simplicity of `Fiber#raise` simply being a specialized version of `Fiber#resume` is worth preserving. * The propagation changes alone are enough of a semantic difference to warrant a new method. We could implement `Fiber#cancel` by using `fiber.raise(FiberCancellationError)` on the bottom fiber and catching that exception during termination of the canceled fiber. * This would have the "benefit" that the exception could be rescued. * I might be wrong, but I think that doing this would mostly duplicate my PR, but with some added complexity around exception construction and catching. * It might be a good keyword option? e.g. `Fiber#cancel(with_exception: [true,#exception,#to_str])` Just let the task-fiber-scheduler library handle this. * That's what I'm already doing now. It's mostly fine. It works in my code. * Putting it into ruby core should lead to a small performance boost on very commonly repeated code. * There's probably a better way to store the `cancel_reason` that doesn't require the overhead of adding another `VALUE` to `rb_fiber_struct`. Maybe it can be placed directly into `errinfo`? * Although the common cases can be handled via a trampoline fiber or #17221, there can still be situations where your application's fiber-scheduler library might not know about fibers created by other libraries. This adds interoperability to a common scenario. * Coroutine cancellation is IMO a core feature. It's important to have something like this for all applications and libraries to use as a baseline for interoperability. Implementation: https://github.com/ruby/ruby/pull/3766 -- https://bugs.ruby-lang.org/
1 0
0 0
[ruby-core:111176] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by austin (Austin Ziegler) 03 Dec '22

03 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
[ruby-core:111175] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by ufuk (Ufuk Kayserilioglu) 03 Dec '22

03 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
[ruby-core:111174] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by tomstuart (Tom Stuart) 03 Dec '22

03 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
[ruby-core:111173] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by RubyBugs (A Nonymous) 03 Dec '22

03 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
[ruby-core:111171] [Ruby master Feature#18980] Re-reconsider numbered parameters: `it` as a default block parameter
by funny_falcon (Yura Sokolov) 03 Dec '22

03 Dec '22
Issue #18980 has been updated by funny_falcon (Yura Sokolov). Ruby takes so much syntax last years. I fear it. Let's not strain our lovely language, please. ---------------------------------------- Feature #18980: Re-reconsider numbered parameters: `it` as a default block parameter https://bugs.ruby-lang.org/issues/18980#change-100456 * Author: k0kubun (Takashi Kokubun) * Status: Open * Priority: Normal ---------------------------------------- ## Problem Numbered parameters (`_1`, `_2`, ...) look like unused local variables and I don't feel motivated to use them, even though I need this feature very often and always come up with `_1`. ```rb [1, 2, 3].each { puts _1 } ``` I have barely used it in the last 2~3 years because it looks like a compromised syntax. I even hesitate to use it on IRB. ### Why I don't use `_1` I'm not clever enough to remember the order of parameters. Therefore, when a block has multiple parameters, I'd always want to name those parameters because which is `_1` or `_2` is not immediately obvious. Thus I would use this feature only when a block takes a single argument, which is actually pretty common. If I use `_1`, it feels like there might be a second argument, and you might waste time to think about `_2`, even if `_2` doesn't exist, which is a cognitive overhead. If you use `it`, it kinda implies there's only a single argument, so you don't need to spend time remembering whether `_2` exists or not. It is important for me that there's no number in `it`. ## Proposal Hoping to introduce `it` as an alternative to `_1` later, experiment with warning `#it` method calls without any arguments or blocks. If nobody sees serious problems after some warning period, we'll implement `it` as follows: ### Specification ```rb [1, 2, 3].each { puts it } ``` `it`s behavior should be as close to `_1` as possible. `it` should treat array arguments in the same way as `_1`. `it` doesn't work in a block when an ordinary parameter is defined. `it` is implemented as a special case of `getlocal` insn, not a method. `it` without an argument is considered `_1` or a normal local variable if defined. `it` is considered a method call only when it has any positional/keyword/block arguments. ## Past discussions * [Feature #4475] default variable name for parameter: Proposed `it`, and merged as `@1`. * 2019/03/13: [DevelopersMeeting20190311Japan](https://docs.google.com/document/d/e/2PACX-… * 2019/04/17: [DevelopersMeeting20190417Japan](https://docs.google.com/document/d/1hw6Xca8… * 2019/04/20: [Ruby Committers vs the World](https://youtu.be/5eAXAUTtNYU?t=3118) * [Feature #15723] Reconsider numbered parameters: Renamed `@1` to `_1`. * 2019/08/29: [DevelopersMeeting20190829Japan](https://docs.google.com/document/d/1XypDO1c… * [Feature #15897] `it` as a default block parameter: Proposed `it`, and got closed because `_1` was merged. ### Compatibility `it` has not necessarily been rejected by Matz; he just said [it's difficult to keep compatibility](https://bugs.ruby-lang.org/issues/4475#note-6) and [`it` or `this` _could_ break existing code](https://bugs.ruby-lang.org/issues/15723#note-2). It feels like everybody thinks `it` is the most beautiful option but is not sure if `it` breaks compatibility. But, in reality, does `it`? The following cases have been discussed: * `it` method, most famously in RSpec: You almost always pass a positional and/or block argument to RSpec's `it`, so the conflict is avoided with my proposal. You virtually never use a completely naked `it` ([comment](https://bugs.ruby-lang.org/issues/15897#note-29)) * `it` local variable: With the specification in my proposal, the existing code can continue to work if we consider `it` as a local variable when defined. With the specification in my proposal, existing code seems to break if and only if you call a method `#it` without an argument. But it seems pretty rare (reminder: a block given to an RSpec test case is also an argument). It almost feels like people are too afraid of compatibility problems that barely exist or have not really thought about options to address them. Also, you could always experiment with just showing warnings, which doesn't break any compatibility. Even if it takes 2~3 years of a warning period, I'd be happy to use that in 3 years. ### Confusion We should separately discuss incompatible cases and "works but confusing" cases. Potential confusion points: * RSpec's `it "tests something" do ... end` vs `it` inside the `do ... end` * `it` could be a local variable or `_1`, depending on the situation My two cents: You'd rarely need to write `it` directly under RSpec's `it` block, and you would just name a block argument for that case. In a nested block under a test case, I don't think you'd feel `it` is RSpec's. When you use a local variable `it = 1`, you'd use the local variable in a very small scope or few lines because otherwise, it'd be very hard to figure out what the local variable has anyway. So you'd likely see the assignment `it = 1` near the use of the local variable and you could easily notice `it` is not `_1`. If not, such code would be confusing and fragile even without this feature. The same applies when `it` is a method/block argument. I believe it wouldn't be as confusing as some people think, and you can always choose to not use `it` in places where `it` is confusing. -- https://bugs.ruby-lang.org/
1 0
0 0
[ruby-core:111170] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by ufuk (Ufuk Kayserilioglu) 02 Dec '22

02 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
[ruby-core:111169] [Ruby master Feature#19000] Data: Add "Copy with changes method" [Follow-on to #16122 Data: simple immutable value object]
by matz (Yukihiro Matsumoto) 02 Dec '22

02 Dec '22
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-t… 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-insta… ## 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/
1 0
0 0
  • ← Newer
  • 1
  • ...
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • ...
  • 328
  • Older →

HyperKitty Powered by HyperKitty version 1.3.12.