ruby-changes:73806
From: Victor <ko1@a...>
Date: Fri, 30 Sep 2022 18:23:48 +0900 (JST)
Subject: [ruby-changes:73806] ad651925e3 (master): Add Data class implementation: Simple immutable value object
https://git.ruby-lang.org/ruby.git/commit/?id=ad651925e3 From ad651925e365ca18645f05b5e9b2eca9cd5721bc Mon Sep 17 00:00:00 2001 From: Victor Shepelev <zverok.offline@g...> Date: Fri, 30 Sep 2022 12:23:19 +0300 Subject: Add Data class implementation: Simple immutable value object --- NEWS.md | 6 + array.c | 2 +- internal/array.h | 1 + spec/ruby/core/data/constants_spec.rb | 14 +- struct.c | 529 +++++++++++++++++++++++++++++++++- test/ruby/test_data.rb | 170 +++++++++++ 6 files changed, 716 insertions(+), 6 deletions(-) create mode 100644 test/ruby/test_data.rb diff --git a/NEWS.md b/NEWS.md index 3cc1983a8c..0aa51bb032 100644 --- a/NEWS.md +++ b/NEWS.md @@ -102,6 +102,11 @@ Note that each entry is kept to a minimum, see links for details. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L102 Note: We're only listing outstanding class updates. +* Data + * New core class to represent simple immutable value object. The class is + similar to `Struct` and partially shares an implementation, but has more + lean and strict API. [[Feature #16122]] + * Encoding * Encoding#replicate has been deprecated and will be removed in 3.3. [[Feature #18949]] * The dummy `Encoding::UTF_16` and `Encoding::UTF_32` encodings no longer @@ -323,3 +328,4 @@ The following deprecated APIs are removed. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L328 [Feature #18949]: https://bugs.ruby-lang.org/issues/18949 [Feature #19008]: https://bugs.ruby-lang.org/issues/19008 [Feature #19026]: https://bugs.ruby-lang.org/issues/19026 +[Feature #16122]: https://bugs.ruby-lang.org/issues/16122 diff --git a/array.c b/array.c index cacc549a24..73e8a3c9ce 100644 --- a/array.c +++ b/array.c @@ -5587,7 +5587,7 @@ ary_recycle_hash(VALUE hash) https://github.com/ruby/ruby/blob/trunk/array.c#L5587 * Related: Array#difference. */ -static VALUE +VALUE rb_ary_diff(VALUE ary1, VALUE ary2) { VALUE ary3; diff --git a/internal/array.h b/internal/array.h index 17d91a800b..a0d16dec3f 100644 --- a/internal/array.h +++ b/internal/array.h @@ -35,6 +35,7 @@ void rb_ary_cancel_sharing(VALUE ary); https://github.com/ruby/ruby/blob/trunk/internal/array.h#L35 size_t rb_ary_size_as_embedded(VALUE ary); void rb_ary_make_embedded(VALUE ary); bool rb_ary_embeddable_p(VALUE ary); +VALUE rb_ary_diff(VALUE ary1, VALUE ary2); static inline VALUE rb_ary_entry_internal(VALUE ary, long offset); static inline bool ARY_PTR_USING_P(VALUE ary); diff --git a/spec/ruby/core/data/constants_spec.rb b/spec/ruby/core/data/constants_spec.rb index 1d469f9237..d9d55b50f9 100644 --- a/spec/ruby/core/data/constants_spec.rb +++ b/spec/ruby/core/data/constants_spec.rb @@ -14,10 +14,22 @@ ruby_version_is ''...'3.0' do https://github.com/ruby/ruby/blob/trunk/spec/ruby/core/data/constants_spec.rb#L14 end end -ruby_version_is '3.0' do +ruby_version_is '3.0'...'3.2' do describe "Data" do it "does not exist anymore" do Object.should_not have_constant(:Data) end end end + +ruby_version_is '3.2' do + describe "Data" do + it "is a new constant" do + Data.superclass.should == Object + end + + it "is not deprecated" do + -> { Data }.should_not complain + end + end +end diff --git a/struct.c b/struct.c index 1e7294eb5e..57d7cffc30 100644 --- a/struct.c +++ b/struct.c @@ -28,7 +28,11 @@ enum { https://github.com/ruby/ruby/blob/trunk/struct.c#L28 AREF_HASH_THRESHOLD = 10 }; +/* Note: Data is a stricter version of the Struct: no attr writers & no + hash-alike/array-alike behavior. It shares most of the implementation + on the C level, but is unrelated on the Ruby level. */ VALUE rb_cStruct; +static VALUE rb_cData; static ID id_members, id_back_members, id_keyword_init; static VALUE struct_alloc(VALUE); @@ -44,7 +48,7 @@ struct_ivar_get(VALUE c, ID id) https://github.com/ruby/ruby/blob/trunk/struct.c#L48 for (;;) { c = rb_class_superclass(c); - if (c == 0 || c == rb_cStruct) + if (c == 0 || c == rb_cStruct || c == rb_cData) return Qnil; RUBY_ASSERT(RB_TYPE_P(c, T_CLASS)); ivar = rb_attr_get(c, id); @@ -297,6 +301,29 @@ rb_struct_s_inspect(VALUE klass) https://github.com/ruby/ruby/blob/trunk/struct.c#L301 return inspect; } +static VALUE +rb_data_s_new(int argc, const VALUE *argv, VALUE klass) +{ + if (rb_keyword_given_p()) { + if (argc > 1 || !RB_TYPE_P(argv[0], T_HASH)) { + rb_error_arity(argc, 0, 0); + } + return rb_class_new_instance_pass_kw(argc, argv, klass); + } + else { + VALUE members = struct_ivar_get(klass, id_members); + int num_members = RARRAY_LENINT(members); + + rb_check_arity(argc, 0, num_members); + VALUE arg_hash = rb_hash_new_with_size(argc); + for (long i=0; i<argc; i++) { + VALUE k = rb_ary_entry(members, i), v = argv[i]; + rb_hash_aset(arg_hash, k, v); + } + return rb_class_new_instance_kw(1, &arg_hash, klass, RB_PASS_KEYWORDS); + } +} + #if 0 /* for RDoc */ /* @@ -349,6 +376,30 @@ setup_struct(VALUE nstr, VALUE members) https://github.com/ruby/ruby/blob/trunk/struct.c#L376 return nstr; } +static VALUE +setup_data(VALUE subclass, VALUE members) +{ + long i, len; + + members = struct_set_members(subclass, members); + + rb_define_alloc_func(subclass, struct_alloc); + rb_define_singleton_method(subclass, "new", rb_data_s_new, -1); + rb_define_singleton_method(subclass, "[]", rb_data_s_new, -1); + rb_define_singleton_method(subclass, "members", rb_struct_s_members_m, 0); + rb_define_singleton_method(subclass, "inspect", rb_struct_s_inspect, 0); // FIXME: just a separate method?.. + + len = RARRAY_LEN(members); + for (i=0; i< len; i++) { + VALUE sym = RARRAY_AREF(members, i); + VALUE off = LONG2NUM(i); + + define_aref_method(subclass, sym, off); + } + + return subclass; +} + VALUE rb_struct_alloc_noinit(VALUE klass) { @@ -912,10 +963,11 @@ rb_struct_each_pair(VALUE s) https://github.com/ruby/ruby/blob/trunk/struct.c#L963 } static VALUE -inspect_struct(VALUE s, VALUE dummy, int recur) +inspect_struct(VALUE s, VALUE prefix, int recur) { VALUE cname = rb_class_path(rb_obj_class(s)); - VALUE members, str = rb_str_new2("#<struct "); + VALUE members; + VALUE str = prefix; long i, len; char first = RSTRING_PTR(cname)[0]; @@ -972,7 +1024,7 @@ inspect_struct(VALUE s, VALUE dummy, int recur) https://github.com/ruby/ruby/blob/trunk/struct.c#L1024 static VALUE rb_struct_inspect(VALUE s) { - return rb_exec_recursive(inspect_struct, s, 0); + return rb_exec_recursive(inspect_struct, s, rb_str_new2("#<struct ")); } /* @@ -1519,6 +1571,448 @@ rb_struct_dig(int argc, VALUE *argv, VALUE self) https://github.com/ruby/ruby/blob/trunk/struct.c#L1571 return rb_obj_dig(argc, argv, self, Qnil); } +/* + * Document-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. + */ + +/* + * call-seq: + * define(name, *symbols) -> class + * define(*symbols) -> class + * + * Defines a new \Data class. If the first argument is a string, the class + * is stored in <tt>Data::<name></tt> constant. + * + * 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 natura (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/