class Data - Documentation for Ruby 4.0
Description: class Data: Class \Data provides a convenient way to define simple classes for value-alike objects. Measure = Data.
Keywords: ruby,class,Data

🔍

No recent searches

# class Data

Class Data provides a convenient way to define simple classes for value-alike objects.

The simplest example of usage:

    Measure = Data.define(:amount, :unit)

    # Positional arguments constructor is provided
    distance = Measure.new(100, 'km')
    #=> #<data Measure amount=100, unit="km">

    # Keyword arguments constructor is provided
    weight = Measure.new(amount: 50, unit: 'kg')
    #=> #<data Measure amount=50, unit="kg">

    # Alternative form to construct an object:
    speed = Measure[10, 'mPh']
    #=> #<data Measure amount=10, unit="mPh">

    # Works with keyword arguments, too:
    area = Measure[amount: 1.5, unit: 'm^2']
    #=> #<data Measure amount=1.5, unit="m^2">

    # Argument accessors are provided:
    distance.amount #=> 100
    distance.unit #=> "km"

Constructed object also has a reasonable definitions of `==` operator, `to_h` hash conversion, and `deconstruct` / `deconstruct_keys` to be used in pattern matching.

`::define` method accepts an optional block and evaluates it in the context of the newly defined class. That allows to define additional methods:

    Measure = Data.define(:amount, :unit) do
      def <=>(other)
        return unless other.is_a?(self.class) && other.unit == unit
        amount <=> other.amount
      end

      include Comparable
    end

    Measure[3, 'm'] < Measure[5, 'm'] #=> true
    Measure[3, 'm'] < Measure[5, 'kg']
    # comparison of Measure with Measure failed (ArgumentError)

`Data` provides no member writers, or enumerators: it is meant to be a storage for immutable atomic values. But note that if some of data members is of a mutable class, `Data` does no additional immutability enforcement:

    Event = Data.define(:time, :weekdays)
    event = Event.new('18:00', %w[Tue Wed Fri])
    #=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri"]>

    # There is no #time= or #weekdays= accessors, but changes are
    # still possible:
    event.weekdays << 'Sat'
    event
    #=> #<data Event time="18:00", weekdays=["Tue", "Wed", "Fri", "Sat"]>

See also `Struct`, which is a similar concept, but has more container-alike API, allowing to change contents of the object and enumerate it.

### Public Class Methods

define(*symbols) → class
Source

    static VALUE
    rb_data_s_def(int argc, VALUE *argv, VALUE klass)
    {
        VALUE rest;
        long i;
        VALUE data_class;

        rest = rb_ident_hash_new();
        RBASIC_CLEAR_CLASS(rest);
        for (i=0; i<argc; i++) {
            VALUE mem = rb_to_symbol(argv[i]);
            if (rb_is_attrset_sym(mem)) {
                rb_raise(rb_eArgError, "invalid data member: %"PRIsVALUE, mem);
            }
            if (RTEST(rb_hash_has_key(rest, mem))) {
                rb_raise(rb_eArgError, "duplicate member: %"PRIsVALUE, mem);
            }
            rb_hash_aset(rest, mem, Qtrue);
        }
        rest = rb_hash_keys(rest);
        RBASIC_CLEAR_CLASS(rest);
        OBJ_FREEZE(rest);
        data_class = anonymous_struct(klass);
        setup_data(data_class, rest);
        if (rb_block_given_p()) {
            rb_mod_module_eval(0, 0, data_class);
        }

        return data_class;
    }

Defines a new Data class.

    measure = Data.define(:amount, :unit)
    #=> #<Class:0x00007f70c6868498>
    measure.new(1, 'km')
    #=> #<data amount=1, unit="km">

    # It you store the new class in the constant, it will
    # affect #inspect and will be more natural to use:
    Measure = Data.define(:amount, :unit)
    #=> Measure
    Measure.new(1, 'km')
    #=> #<data Measure amount=1, unit="km">

Note that member-less Data is acceptable and might be a useful technique for defining several homogeneous data classes, like

    class HTTPFetcher
      Response = Data.define(:body)
      NotFound = Data.define
      # ... implementation
    end

Now, different kinds of responses from `HTTPFetcher` would have consistent representation:

    #<data HTTPFetcher::Response body="<html...">
    #<data HTTPFetcher::NotFound>

And are convenient to use in pattern matching:

    case fetcher.get(url)
    in HTTPFetcher::Response(body)
      # process body variable
    in HTTPFetcher::NotFound
      # handle not found case
    end

DataClass::members → array_of_symbols
Source

    #define rb_data_s_members_m rb_struct_s_members_m

Returns an array of member names of the data class:

    Measure = Data.define(:amount, :unit)
    Measure.members # => [:amount, :unit]

new(*args) → instance
new(**kwargs) → instance
::[](*args) → instance
::[](**kwargs) → instance
Source

    static VALUE
    rb_data_initialize_m(int argc, const VALUE *argv, VALUE self)
    {
        VALUE klass = rb_obj_class(self);
        rb_struct_modify(self);
        VALUE members = struct_ivar_get(klass, id_members);
        size_t num_members = RARRAY_LEN(members);

        if (argc == 0) {
            if (num_members > 0) {
                rb_exc_raise(rb_keyword_error_new("missing", members));
            }
            OBJ_FREEZE(self);
            return Qnil;
        }
        if (argc > 1 || !RB_TYPE_P(argv[0], T_HASH)) {
            rb_error_arity(argc, 0, 0);
        }

        VALUE missing = rb_ary_dup(members);
        RBASIC_CLEAR_CLASS(missing);
        struct struct_hash_set_arg arg = {
            .self = self,
            .unknown_keywords = Qnil,
            .missing_keywords = missing,
            .missing_count = (long)num_members,
        };
        rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), num_members);
        rb_hash_foreach(argv[0], data_hash_set_i, (VALUE)&arg);
        // Freeze early before potentially raising, so that we don't leave an
        // unfrozen copy on the heap, which could get exposed via ObjectSpace.
        OBJ_FREEZE(self);
        if (UNLIKELY(arg.missing_count > 0)) {
            rb_ary_compact_bang(missing);
            RUBY_ASSERT(RARRAY_LEN(missing) == arg.missing_count, "missing_count=%ld but %ld", arg.missing_count, RARRAY_LEN(missing));
            RBASIC_SET_CLASS_RAW(missing, rb_cArray);
            rb_exc_raise(rb_keyword_error_new("missing", missing));
        }
        VALUE unknown_keywords = arg.unknown_keywords;
        if (UNLIKELY(!NIL_P(unknown_keywords))) {
            RBASIC_SET_CLASS_RAW(unknown_keywords, rb_cArray);
            rb_exc_raise(rb_keyword_error_new("unknown", unknown_keywords));
        }

        return Qnil;
    }

Constructors for classes defined with `::define` accept both positional and keyword arguments.

    Measure = Data.define(:amount, :unit)

    Measure.new(1, 'km')
    #=> #<data Measure amount=1, unit="km">
    Measure.new(amount: 1, unit: 'km')
    #=> #<data Measure amount=1, unit="km">

    # Alternative shorter initialization with []
    Measure[1, 'km']
    #=> #<data Measure amount=1, unit="km">
    Measure[amount: 1, unit: 'km']
    #=> #<data Measure amount=1, unit="km">

All arguments are mandatory (unlike `Struct`), and converted to keyword arguments:

    Measure.new(amount: 1)
    # in `initialize': missing keyword: :unit (ArgumentError)

    Measure.new(1)
    # in `initialize': missing keyword: :unit (ArgumentError)

Note that `Measure#initialize` always receives keyword arguments, and that mandatory arguments are checked in `initialize`, not in `new`. This can be important for redefining initialize in order to convert arguments or provide defaults:

    Measure = Data.define(:amount, :unit)
    class Measure
      NONE = Data.define

      def initialize(amount:, unit: NONE.new)
        super(amount: Float(amount), unit:)
      end
    end

    Measure.new('10', 'km') # => #<data Measure amount=10.0, unit="km">
    Measure.new(10_000)     # => #<data Measure amount=10000.0, unit=#<data Measure::NONE>>

### Public Instance Methods

self == other → true or false
Source

    #define rb_data_equal rb_struct_equal

Returns `true` if `other` is the same class as `self`, and all members are equal.

Examples:

    Measure = Data.define(:amount, :unit)

    Measure[1, 'km'] == Measure[1, 'km'] #=> true
    Measure[1, 'km'] == Measure[2, 'km'] #=> false
    Measure[1, 'km'] == Measure[1, 'm']  #=> false

    Measurement = Data.define(:amount, :unit)
    # Even though Measurement and Measure have the same "shape"
    # their instances are never equal
    Measure[1, 'km'] == Measurement[1, 'km'] #=> false

deconstruct → array
Source

    #define rb_data_deconstruct rb_struct_to_a

Returns the values in `self` as an array, to use in pattern matching:

    Measure = Data.define(:amount, :unit)

    distance = Measure[10, 'km']
    distance.deconstruct #=> [10, "km"]

    # usage
    case distance
    in n, 'km' # calls #deconstruct underneath
      puts "It is #{n} kilometers away"
    else
      puts "Don't know how to handle it"
    end
    # prints "It is 10 kilometers away"

Or, with checking the class, too:

    case distance
    in Measure(n, 'km')
      puts "It is #{n} kilometers away"
    # ...
    end

deconstruct_keys(array_of_names_or_nil) → hash
Source

    static VALUE
    rb_data_deconstruct_keys(VALUE s, VALUE keys)
    {
        return deconstruct_keys(s, keys, true);
    }

Returns a hash of the name/value pairs, to use in pattern matching.

    Measure = Data.define(:amount, :unit)

    distance = Measure[10, 'km']
    distance.deconstruct_keys(nil) #=> {:amount=>10, :unit=>"km"}
    distance.deconstruct_keys([:amount]) #=> {:amount=>10}

    # usage
    case distance
    in amount:, unit: 'km' # calls #deconstruct_keys underneath
      puts "It is #{amount} kilometers away"
    else
      puts "Don't know how to handle it"
    end
    # prints "It is 10 kilometers away"

Or, with checking the class, too:

    case distance
    in Measure(amount:, unit: 'km')
      puts "It is #{amount} kilometers away"
    # ...
    end

eql?(other) → true or false
Source

    #define rb_data_eql rb_struct_eql

Equality check that is used when two items of data are keys of a `Hash`.

The subtle difference with `==` is that members are also compared with their `eql?` method, which might be important in some cases:

    Measure = Data.define(:amount, :unit)

    Measure[1, 'km'] == Measure[1.0, 'km'] #=> true, they are equal as values
    # ...but...
    Measure[1, 'km'].eql? Measure[1.0, 'km'] #=> false, they represent different hash keys

See also `Object#eql?` for further explanations of the method usage.

hash → integer
Source

    #define rb_data_hash rb_struct_hash

Redefines `Object#hash` (used to distinguish objects as `Hash` keys) so that data objects of the same class with same content would have the same `hash` value, and represented the same `Hash` key.

    Measure = Data.define(:amount, :unit)

    Measure[1, 'km'].hash == Measure[1, 'km'].hash #=> true
    Measure[1, 'km'].hash == Measure[10, 'km'].hash #=> false
    Measure[1, 'km'].hash == Measure[1, 'm'].hash #=> false
    Measure[1, 'km'].hash == Measure[1.0, 'km'].hash #=> false

    # Structurally similar data class, but shouldn't be considered
    # the same hash key
    Measurement = Data.define(:amount, :unit)

    Measure[1, 'km'].hash == Measurement[1, 'km'].hash #=> false

inspect → string
Source

    static VALUE
    rb_data_inspect(VALUE s)
    {
        return rb_exec_recursive(inspect_struct, s, rb_str_new2("#<data "));
    }

Returns a string representation of `self`:

    Measure = Data.define(:amount, :unit)

    distance = Measure[10, 'km']

    p distance  # uses #inspect underneath
    #<data Measure amount=10, unit="km">

    puts distance  # uses #to_s underneath, same representation
    #<data Measure amount=10, unit="km">

Also aliased as: to_s
members → array_of_symbols
Source

    #define rb_data_members_m rb_struct_members_m

Returns the member names from `self` as an array:

    Measure = Data.define(:amount, :unit)
    distance = Measure[10, 'km']

    distance.members #=> [:amount, :unit]

to_h → hash
to_h {|name, value| ... } → hash
Source

    #define rb_data_to_h rb_struct_to_h

Returns `Hash` representation of the data object.

    Measure = Data.define(:amount, :unit)
    distance = Measure[10, 'km']

    distance.to_h
    #=> {:amount=>10, :unit=>"km"}

Like `Enumerable#to_h`, if the block is provided, it is expected to produce key-value pairs to construct a hash:

    distance.to_h { |name, val| [name.to_s, val.to_s] }
    #=> {"amount"=>"10", "unit"=>"km"}

Note that there is a useful symmetry between `to_h` and initialize:

    distance2 = Measure.new(**distance.to_h)
    #=> #<data Measure amount=10, unit="km">
    distance2 == distance
    #=> true

to_s → string

Returns a string representation of `self`:

    Measure = Data.define(:amount, :unit)

    distance = Measure[10, 'km']

    p distance  # uses #inspect underneath
    #<data Measure amount=10, unit="km">

    puts distance  # uses #to_s underneath, same representation
    #<data Measure amount=10, unit="km">

Alias for: inspect
with(**kwargs) → instance
Source

    static VALUE
    rb_data_with(int argc, const VALUE *argv, VALUE self)
    {
        VALUE kwargs;
        rb_scan_args(argc, argv, "0:", &kwargs);
        if (NIL_P(kwargs)) {
            return self;
        }

        VALUE h = rb_struct_to_h(self);
        rb_hash_update_by(h, kwargs, 0);
        return rb_class_new_instance_kw(1, &h, rb_obj_class(self), TRUE);
    }

Returns a shallow copy of `self` — the instance variables of `self` are copied, but not the objects they reference.

If the method is supplied any keyword arguments, the copy will be created with the respective field values updated to use the supplied keyword argument values. Note that it is an error to supply a keyword that the `Data` class does not have as a member.

    Point = Data.define(:x, :y)

    origin = Point.new(x: 0, y: 0)

    up = origin.with(x: 1)
    right = origin.with(y: 1)
    up_and_right = up.with(y: 1)

    p origin       # #<data Point x=0, y=0>
    p up           # #<data Point x=1, y=0>
    p right        # #<data Point x=0, y=1>
    p up_and_right # #<data Point x=1, y=1>

    out = origin.with(z: 1) # ArgumentError: unknown keyword: :z
    some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments