From: "ston1x (Nicolai Stoianov)" Date: 2022-12-03T16:24:12+00:00 Subject: [ruby-core:111179] [Ruby master Feature#16122] Data: simple immutable value object 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) * `::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/