Project

General

Profile

Actions

Feature #16122

closed

Data: simple immutable value object

Added by zverok (Victor Shepelev) over 4 years ago. Updated over 1 year ago.

Status:
Closed
Target version:
-
[ruby-core:94508]

Description

Intro (original theoretical part of the proposal)

Value Object is a useful concept, introduced by Martin Fowler (his post, Wikipedia Entry) 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, Good Ruby Value object convention (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:

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 Structs 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:
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


Files

struct_value.patch (18.6 KB) struct_value.patch zverok (Victor Shepelev), 08/23/2019 05:40 PM

Related issues 1 (0 open1 closed)

Related to Ruby master - Feature #16769: Struct.new(..., immutable: true)RejectedActions
Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like1