From: "zverok (Victor Shepelev)" Date: 2022-12-05T18:43:47+00:00 Subject: [ruby-core:111213] [Ruby master Feature#16122] Data: simple immutable value object Issue #16122 has been updated by zverok (Victor Shepelev). @ston1x I understand where you are coming from (make the declaration more homogenous for the sake of IDE's convenience), and I don't think that the goal is meaningless, but in this case, it will lead to severe complication of semantics/implementation: what's the state of the object between `Ticket < Data` and `attr`? What if `attr` never declared? What if it used several times? What if it used again in the descendant of `Ticket`? This kind of complication doesn't seem worth the gain, for me. It is up to IDE to handle `Data`, it seems (as far as I know, say, YARD documentation system has a handling for `Struct` descendants, and it isn't impossible for other tools, too). Especially considering that a lot of `Data` usage would probably be just short one-line declarations and ideally, IDE that wants to autocomplete available attributes should understand them anyway, so... ---------------------------------------- Feature #16122: Data: simple immutable value object https://bugs.ruby-lang.org/issues/16122#change-100503 * 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) * `::members`: list of member names * `::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 `#`, 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/ ______________________________________________ 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/