Project

General

Profile

Feature #16986

Updated by ko1 (Koichi Sasada) 3 months ago

# Abstract 

 How about to introduce anonymous Struct literal such as `${a: 1, b: 2}`? 
 It is almost same as `Struct.new(:a, :b).new(1, 2)`. 

 # Proposal 

 ## Background 

 In many cases, people use hash objects to represents a set of values such as `person = {name: "ko1", country: 'Japan'}` and accesses it with `person[:name]` and so on. It is not easy to write (3 letters `[:]`!), and easy to introduce misspelling (`person[:nama]` doesn't raise an error). 
 If we make a `Struct` objects such as `Person = Struct.new(:name, :age)` and `person = Person.new('ko1', 'Japan')`, we can access it with `person.name` naturally. 
 However making new `Struct` is a cost of coding. Some cases we don't    want to name (such as `Person`). 
 Using `OpenStruct` (`person = OpenStruct.new(name: "ko1", country: "Japan")`), we can access it with `person.name`, but we can extend the fields and the performance is not good. 
 Of course, we can define the class `Person` and attr_readers. But several lines we need. 

 To summaries the issues: 

 * Easy to Write 
   * Don't need to declare the class 
   * Accessible with `person.name` format 
 * Limited fields 
 * Better performance 

 ## Idea 

 Introduce new syntax to make an anonymous Struct literal such as: `${ a: 1, b: 2 }`. 
 Similar to Hash syntax (with labels), but `$` prefix to recognize. 

 Anonymous structs which has same member with same order share the class. 

 ```ruby 
     s1 = ${a: 1, b: 2, c: 3} 
     s2 = ${a: 1, b: 2, c: 3} 
     assert s1 == s2 

     s3 = ${a: 1, c: 3, b: 2} 
     s4 = ${d: 4} 

     assert_equal false, s1 == s3 
     assert_equal false, s1 == s4 
 ``` 

 ## Note 

 Unlike Hash literal syntax, this proposal only allows `label: expr` notation. No `${**h}` syntax. 
 This is because if we allow to splat a Hash, it can be a vulnerability by splatting outer-input Hash. 

 Thanks for this spec, we can specify the anonymous Struct class at compile time. 
 We don't need to find or create Struct classes at runtime. 

 ## Implementatation 

 https://github.com/ruby/ruby/pull/3259 

 # Discussion 

 ## Notation 

 Matz said he thought about `{|a: 1, b: 2 |}` syntax. 

 ## Performance 

 Surprisingly, Hash is fast and Struct is slow. 

 ```ruby 
 Benchmark.driver do |r| 
   r.prelude <<~PRELUDE 
   st = Struct.new(:a, :b).new(1, 2) 
   hs = {a: 1, b: 2} 
   class C 
     attr_reader :a, :b 
     def initialize() = (@a = 1; @b = 2) 
   end 
   ob = C.new 
   PRELUDE 
   r.report "ob.a" 
   r.report "hs[:a]" 
   r.report "st.a" 
 end 
 __END__ 
 Warming up -------------------------------------- 
                 ob.a      38.100M i/s -       38.142M times in 1.001101s (26.25ns/i, 76clocks/i) 
               hs[:a]      37.845M i/s -       38.037M times in 1.005051s (26.42ns/i, 76clocks/i) 
                 st.a      33.348M i/s -       33.612M times in 1.007904s (29.99ns/i, 87clocks/i) 
 Calculating ------------------------------------- 
                 ob.a      87.917M i/s -      114.300M times in 1.300085s (11.37ns/i, 33clocks/i) 
               hs[:a]      85.504M i/s -      113.536M times in 1.327850s (11.70ns/i, 33clocks/i) 
                 st.a      61.337M i/s -      100.045M times in 1.631064s (16.30ns/i, 47clocks/i) 
 Comparison: 
                 ob.a:    87917391.4 i/s 
               hs[:a]:    85503703.6 i/s - 1.03x    slower 
                 st.a:    61337463.3 i/s - 1.43x    slower 
 ``` 

 I believe we can speed up `Struct` similar to ivar accesses, so we can improve the performance. 


 BTW, OpenStruct (os.a) is slow. 

 ``` 
 Comparison: 
               hs[:a]:    92835317.7 i/s 
                 ob.a:    85865849.5 i/s - 1.08x    slower 
                 st.a:    53480417.5 i/s - 1.74x    slower 
                 os.a:    12541267.7 i/s - 7.40x    slower 
 ``` 


 For memory consumption, `Struct` is more lightweight because we don't need to keep key names. 

 ## Naming 

 If we name the anonymous class, the same member literals share the name. 

 ```ruby 
 s1 = ${a:1} 
 s2 = ${a:2} 
 p [s1, s2] #=> [#<struct a=1>, #<struct a=2>] 
 A = s1.class 
 p [s1, s2] #=> [#<struct A a=1>, #<struct A a=2>] 

 ``` 

 Maybe it is not good behavior. 

Back