[前][次][番号順一覧][スレッド一覧]

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/

[前][次][番号順一覧][スレッド一覧]