Class: Data

Inherits:
Object show all
Defined in:
struct.c,
struct.c

Overview

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.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#new(*args) ⇒ Object #new(**kwargs) ⇒ Object

::[](**kwargs) -> instance

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 <tt>Measure#initialize</tt> 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) do
     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 NONE>>


1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
# File 'struct.c', line 1775

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));
        }
        return Qnil;
    }
    if (argc > 1 || !RB_TYPE_P(argv[0], T_HASH)) {
        rb_error_arity(argc, 0, 0);
    }

    if (RHASH_SIZE(argv[0]) < num_members) {
        VALUE missing = rb_ary_diff(members, rb_hash_keys(argv[0]));
        rb_exc_raise(rb_keyword_error_new("missing", missing));
    }

    struct struct_hash_set_arg arg;
    rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), num_members);
    arg.self = self;
    arg.unknown_keywords = Qnil;
    rb_hash_foreach(argv[0], struct_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_RAW(self);
    if (arg.unknown_keywords != Qnil) {
        rb_exc_raise(rb_keyword_error_new("unknown", arg.unknown_keywords));
    }
    return Qnil;
}

Class Method Details

.define(*symbols) ⇒ 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 homogenous 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

Returns:



1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
# File 'struct.c', line 1667

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_RAW(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;
}

.membersObject

Instance Method Details

#==Object

#deconstructObject

#deconstruct_keysObject

#eql?Boolean

Returns:

  • (Boolean)

#hashObject

#initialize_copy(s) ⇒ Object

:nodoc:



1813
1814
1815
1816
1817
1818
1819
# File 'struct.c', line 1813

static VALUE
rb_data_init_copy(VALUE copy, VALUE s)
{
    copy = rb_struct_init_copy(copy, s);
    RB_OBJ_FREEZE_RAW(copy);
    return copy;
}

#inspectString #to_sString Also known as: to_s

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">

Overloads:



1884
1885
1886
1887
1888
# File 'struct.c', line 1884

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

#membersObject

#to_hObject

#with(**kwargs) ⇒ Object

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


1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
# File 'struct.c', line 1851

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);
}