Project

General

Profile

Feature #16986

Anonymous Struct literal

Added by ko1 (Koichi Sasada) 3 months ago. Updated 17 days ago.

Status:
Open
Priority:
Normal
Target version:
-
[ruby-core:98947]

Description

Abstract

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

Proposal

Background

In many cases, people use hash objects to represent a set of values such as person = {name: "ko1", country: 'Japan'} and access its values through person[:name] and so on. It is not easy to write (three characters [:]!), and it easily introduces misspelling (person[:nama] doesn't raise an error).

If we make a Struct object by doing Person = Struct.new(:name, :age) and person = Person.new('ko1', 'Japan'), we can access its values through person.name naturally. However, it costs coding. And in some cases, we don't want to name the class (such as Person).

Using OpenStruct (person = OpenStruct.new(name: "ko1", country: "Japan")), we can access it through person.name, but we can extend the fields unintentionally, and the performance is not good.

Of course, we can define a class Person with attr_readers. But it takes several lines.

To summarize the needs:

  • Easy to write
    • Doesn't require declaring the class
    • Accessible through person.name format
  • Limited fields
  • Better performance

Idea

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

Anonymous structs which have the same member in the same order share their class.

    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 to this spec, we can specify anonymous Struct classes 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.

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 similarly 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 the key names.

Naming

If we name an anonymous class, literals with the same members share the name.

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 that is not a good behavior.

Also available in: Atom PDF