loading...

Ruby 3 - Anonymous Struct

baweaver profile image Brandon Weaver ・4 min read

Starting off the first of many posts on new features in Ruby 3 we'll be looking at the Anonymous Struct syntax which was recently discussed on the bug tracker

Quick Reference

So what does it do? It allows you to make Structs inline, almost like an OpenStruct:

bob = ${ name: 'Bob', age: 42 }
sue = ${ name: 'Sue', age: 42 }

Which might look like this with OpenStruct instead:

bob = OpenStruct.new(name: 'Bob', age: 42)
sue = OpenStruct.new(name: 'Sue', age: 42)

Details

That said, let's take a look.

What Can We Use It For?

Let's start with the fun section before we take a look into the rest of it. Some of this I will caveat with not knowing if it'll quite work as I can't experiment with this immediately.

Pattern Matching

If this works I will be a very happy programmer:

bob = Person.new(name: 'Bob', age: 42)

case bob
in ${ name: /^B/, age: 20..50 }
  true
else
  false
end

While one could fairly say:

case bob
in Person[name: /^B/, age: 20..50]
  true
else
  false
end

...I like the potential for duck-typing on a set of attributes instead.

If we start getting into nested object types you can see where this could get real interesting real fast.

Destructuring

With pattern matching that means we also get the very nifty Struct#deconstruct_keys which can be used to deconstruct keys out of objects.

Since Object itself doesn't have this behavior we can't quite get away with some of the same fun we can here like this:

case ${ name: 'Bob', age: 42 }
in name: /^B/
end

...which allows us to skip some of the intermediary syntax.

Debugging / Testing

How many times do you need to use something like double or make a fake object to respond to certain methods? With this new syntax you have a real quick way to have an inline mock object to feed to methods:

def some_expecting_method(object)
  Rails.logger.info "#{object.name}: #{object.value} is interesting"
end

some_expecting_method(${ name: 'something', value: 'interesting' })

That'd be great for all types of spelunking and debugging adventures, as I've definitely used OpenStruct and my own quick Struct hacks for this in the past.

So Why Not OpenStruct?

Because it turns out to be a bit slow. From the bugtracker:

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

Splatting

This feature explicitly forbids Hash splatting:

# Works
a = { a: 1, b: 2 }
b = { **a, c: 3 }

# Does not work
c = ${ **b, d: 4 }

The reasoning given is that a user could potentially input a faulty Hash causing a security problem.

Caveat

I cannot say I agree with this necessarily as there could be value in such a feature, and the language should not seek to prevent users from doing such things. If this were the case we would have locked down eval and other constructs long ago.

I would agree with formatters warning that it's not a great idea in some cases, but preventing the feature I don't quite agree with.

Block KWArg Syntax and other alternatives

It was mentioned that Matz thought about the following syntax:

{ |a: 1, b: 2| }

This may cause confusion with blocks as was noted by another commenter.

The other debated syntaxes were:

# 1. Original
${a:1, b:2}

# 2. Matz's idea, conflicts with blocks
{|a:1, b:2|}

# 3. New Struct keyword, could introduce incompatibilities
struct a: 1, b: 2

# 4. %o for Object
%o{a:1, b:2}

# 5. Use parens
(a:1, b:2)

# 6. Methods on Struct or references to other class behavior
Struct.anonymous(a:1, b:2)

# 6.2 Kernel Struct method
Struct(a:1, b:2)

# 6.3 Struct#[] method, like Hash[a: 1]
Struct[a:1, b:2]

Personally I really like 6.3 as it follows along with a lot of behaviors from other classes in Ruby, but I have a bias towards ${} for brevity.

Parting Thoughts

I really like this feature idea, and will very likely be using it a lot in my own code for some of the above reasons. I would caution against using it too freely in production code outside of debugging as named classes are more easily documented and explained.

Looking forward to what Ruby 3.0 brings, and I'll be writing on new features as I see them.

Notice one that I haven't? Feel free to DM me on Twitter @keystonelemur and I'll take a look into it.

Note: Currently working on a Ractor post but want to do more research on it first.

Until next time!

Discussion

pic
Editor guide
Collapse
rhymes profile image
rhymes

Thanks Brandon for the write up! Anonymous structs look cool! Between these, pattern matching and "assignment on the right" there's potential that a Ruby source code file in 5 years will look quite different than today :)

Collapse
citizen428 profile image
Michael Kohl

F# recently added anonymous records and they also are great when you want to return multiple values with named accessors without having to define a product type for that first (say inside a library).