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 collapse
-
.define(*symbols) ⇒ Class
Defines a new Data class.
- .members ⇒ Object
Instance Method Summary collapse
- #== ⇒ Object
- #deconstruct ⇒ Object
- #deconstruct_keys ⇒ Object
- #eql? ⇒ Boolean
- #hash ⇒ Object
-
#initialize(*args) ⇒ Object
constructor
::[](**kwargs) -> instance.
-
#initialize_copy(s) ⇒ Object
:nodoc:.
-
#inspect ⇒ Object
(also: #to_s)
Returns a string representation of
self
:. - #members ⇒ Object
- #to_h ⇒ Object
-
#with(**kwargs) ⇒ Object
Returns a shallow copy of
self
— the instance variables ofself
are copied, but not the objects they reference.
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
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;
}
|
.members ⇒ Object
Instance Method Details
#== ⇒ Object
#deconstruct ⇒ Object
#deconstruct_keys ⇒ Object
#eql? ⇒ Boolean
#hash ⇒ Object
#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;
}
|
#inspect ⇒ String #to_s ⇒ String Also known as: to_s
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 "));
}
|
#members ⇒ Object
#to_h ⇒ Object
#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);
}
|