Class: Data
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
-
.define(*symbols) ⇒ class
Defines a new Data class.
- .members
-
.new(*args) ⇒ instance
constructor
Constructors for classes defined with .define accept both positional and keyword arguments.
Instance Method Summary
- #==
- #deconstruct
- #deconstruct_keys
- #eql? ⇒ Boolean
- #hash
-
#inspect ⇒ String
Alias for #to_s.
- #members
- #to_h
-
#to_s ⇒ String
(also: #inspect)
Returns a string representation of
self
: -
#with(**kwargs) ⇒ instance
Returns a shallow copy of
self
— the instance variables ofself
are copied, but not the objects they reference. - #initialize_copy(s) Internal use only
Constructor Details
.new(*args) ⇒ instance
.new(**kwargs) ⇒ instance
.[](*args) ⇒ instance
.[](**kwargs) ⇒ instance
instance
.new(**kwargs) ⇒ instance
.[](*args) ⇒ instance
.[](**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 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) 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>>
# 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
# 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; }
.members
[ GitHub ]Instance Method Details
#==
[ GitHub ]#deconstruct
[ GitHub ]#deconstruct_keys
[ GitHub ]
#eql? ⇒ Boolean
#hash
[ GitHub ]#initialize_copy(s)
# 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; }
Alias for #to_s.
#members
[ GitHub ]#to_h
[ GitHub ]Also known as: #inspect
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">
# File 'struct.c', line 1884
static VALUE rb_data_inspect(VALUE s) { return rb_exec_recursive(inspect_struct, s, rb_str_new2("#<data ")); }
#with(**kwargs) ⇒ instance
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
# 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); }