Everything You Need to Know About Destructuring in Ruby 3

How improved pattern matching and rightward assignment make it possible to “destructure” hashes and arrays in Ruby 3.

Jared White by Jared White on January 6, 2021

Welcome to our first article in a series all about the exciting new features in Ruby 3! Today we’re going to look how improved pattern matching and rightward assignment make it possible to “destructure” hashes and arrays in Ruby 3—much like how you’d accomplish it in, say, JavaScript—and some of the ways it goes far beyond even what you might expect.

First, a primer: destructuring arrays

For the longest time Ruby has had solid destructuring support for arrays. For example:

a, b, *rest = [1, 2, 3, 4, 5]
# a == 1, b == 2, rest == [3, 4, 5]

So that’s pretty groovy. However, you haven’t been able to use a similar syntax for hashes. This doesn’t work unfortunately:

{a, b, *rest} = {a: 1, b: 2, c: 3, d: 4}
# syntax errors galore! :(

Now there’s a method for Hash called values_at which you could use to pluck keys out of a hash and return in an array which you could then destructure:

a, b = {a: 1, b: 2, c: 3}.values_at(:a, :b)

But that feels kind of clunky, y’know? Not very Ruby-like.

So let’s see what we can do now in Ruby 3!

Introducing rightward assignment

In Ruby 3 we now have a “rightward assignment” operator. This flips the script and lets you write an expression before assigning it to a variable. So instead of x = :y, you can write :y => x. (Yay for the hashrocket resurgence!)

What’s so cool about this is the smart folks working on Ruby 3 realized that they could use the same rightward assignment operator for pattern matching as well. Pattern matching was introduced in Ruby 2.7 and lets you write conditional logic to find and extract variables from complex objects. Now we can do that in the context of assignment!

Let’s write a simple method to try this out. We’ll be bringing our A game today, so let’s call it a_game:

def a_game(hsh)
  hsh => {a:}
  puts "`a` is #{a}, of type #{a.class}"
end

Now we can pass some hashes along and see what happens!

a_game({a: 99})

# `a` is 99, of type Integer

a_game({a: "asdf"})

# `a` is asdf, of type String

But what happens when we pass a hash that doesn’t contain the “a” key?

a_game({b: "bee"})

# NoMatchingPatternError ({:b=>"bee"})

Darn, we get a runtime error. Now maybe that’s what you want if your code would break horribly with a missing hash key. But if you prefer to fail gracefully, rescue comes to the rescue. You can rescue at the method level, but more likely you’d want to rescue at the statement level. Let’s fix our method:

def a_game(hsh)
  hsh => {a:} rescue nil
  puts "`a` is #{a}, of type #{a.class}"
end

And try it again:

a_game({b: "bee"})

# `a` is , of type NilClass

Now that you have a nil value, you can write defensive code to work around the missing data.

What about all the **rest?

Looking back at our original array destructuring example, we were able to get an array of all the values besides the first ones we pulled out as variables. Wouldn’t it be cool if we could do that with hashes too? Well now we can!

{a: 1, b: 2, c: 3, d: 4} => {a:, b:, **rest}

# a == 1, b == 2, rest == {:c=>3, :d=>4}

But wait, there’s more! Rightward assignment and pattern matching actually works with arrays as well! We can replicate our original example like so:

[1, 2, 3, 4, 5] => [a, b, *rest]

# a == 1, b == 2, rest == [3, 4, 5]

In addition, we can do some crazy stuff like pull out array slices before and after certain values:

[-1, 0, 1, 2, 3] => [*left, 1, 2, *right]

# left == [-1, 0], right == [3]

Rightward assignment within pattern matching 🤯

Ready to go all Inception now?!

freaky folding city GIF

You can use rightward assignment techniques within a pattern matching expression to pull out disparate values from an array. In other words, you can pull out everything up to a particular type, grab that type’s value, and then pull out everything after that.

You do this by specifying the type (class name) in the pattern and using => to assign anything of that type to the variable. You can also put types in without rightward assignment to “skip over” those and move on to the next match.

Take a gander at these examples:

[1, 2, "ha", 4, 5] => [*left, String => ha, *right]

# left == [1, 2], ha == "ha", right == [4, 5]

[8, "yo", 12, 14, 16] => [*left, String => yo, Integer, Integer => fourteen, *
right]

# left == [8], yo == "yo", fourteen == 14, right == [16]

Powerful stuff!

And the pièce de résistance: the pin operator

What if you don’t want to hardcode a value in a pattern but have it come from somewhere else? After all, you can’t put existing variables in patterns directly:

int = 1

[-1, 0, 1, 2, 3] => [*left, int, *right]

# left == [], int == -1 …wait wut?!

But in fact you can! You just need to use the pin operator ^. Let’s try this again!

int = 1

[-1, 0, 1, 2, 3] => [*left, ^int, *right]

# left == [-1, 0], right == [2, 3]

You can even use ^ to match variables previously assigned in the same pattern. Yeah, it’s nuts. Check out this example from the Ruby docs:

jane = {school: 'high', schools: [{id: 1, level: 'middle'}, {id: 2, level: 'high'}]}

jane => {school:, schools: [*, {id:, level: ^school}]}

# id == 2

In case you didn’t follow that mind-bendy syntax, it first assigns the value of school (in this case, "high"), then it finds the hash within the schools array where level matches school. The id value is then assigned from that hash, in this case, 2.

So this is all amazingly powerful stuff. Of course you can use pattern matching in conditional logic such as case which is what all the original Ruby 2.7 examples showed, but I tend to think rightward assignment is even more useful for a wide variety of scenarios.

Conclusion

While you might not be able to take advantage of all this flexibility if you’re not yet able to upgrade your codebase to v3 of Ruby, it’s one of those features I feel you’ll keenly miss after you’ve gotten a taste of it, just like keyword arguments when they were first released. I hope you enjoyed this deep dive into rightward assignment and pattern matching! Stay tuned for further examples of rightward assignment and how they improve the readability of Ruby templates.

“Ruby is simple in appearance, but is very complex inside, just like our human body.”

matz

Subscribe to receive a timely tip you can apply directly to your Ruby site or application each week:

Banner image by Kiwihug on Unsplash


Other Recent Articles

Teaching Ruby to Beginners? Trying New Gems or Techniques? Use Bridgetown!

The next big release of Bridgetown provides an intriguing new environment for teaching and learning Ruby and trying out new tools in the Ruby ecosystem.

Continue Reading

Better OOP Through Lazily-Instantiated Memoized Dependencies

There are various schools of thought around how best to define dependencies in your object graph. Let’s learn about the one I prefer to use the majority of the time. It takes advantage of three techniques Ruby provides for us: variable-like method calls, lazy instantiation, and memoization.

Continue Reading

More This Way