Welcome to the first in our series on the cool new features in our Ruby 3! Today we are going to look at how to improve pattern matching and assignment to help us “destructure” arrays and hashes in Ruby 3 – just like you would in JavaScript – and in some ways it goes way beyond what you might expect.
First, a little history lesson: array decomposition
For a long time, Ruby supported structure decomposition for arrays. For example:
1 2 3 | a <span class="token punctuation">,</span> b <span class="token punctuation">,</span> <span class="token operator">*</span> rest <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token number">3</span> <span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">,</span> <span class="token number">5</span> <span class="token punctuation">]</span> <span class="token comment"># a == 1, b == 2, rest == [3, 4, 5]</span> |
However, you cannot use the same syntax for hashes. Sorry, this doesn’t work:
1 2 3 | <span class="token punctuation">{</span> a <span class="token punctuation">,</span> b <span class="token punctuation">,</span> <span class="token operator">*</span> rest <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token number">2</span> <span class="token punctuation">,</span> c <span class="token punctuation">:</span> <span class="token number">3</span> <span class="token punctuation">,</span> d <span class="token punctuation">:</span> <span class="token number">4</span> <span class="token punctuation">}</span> <span class="token comment"># syntax errors galore! :(</span> |
There is a method for Hash called values_at which you can use to get the keys out of the hash function and return them in an array where you can then decompose the structure:
1 2 | a <span class="token punctuation">,</span> b <span class="token operator">=</span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token number">2</span> <span class="token punctuation">,</span> c <span class="token punctuation">:</span> <span class="token number">3</span> <span class="token punctuation">}</span> <span class="token punctuation">.</span> values_at <span class="token punctuation">(</span> <span class="token symbol">:a</span> <span class="token punctuation">,</span> <span class="token symbol">:b</span> <span class="token punctuation">)</span> |
But that feels a bit confusing, don’t you see that? Not very Ruby quality: v
So let’s see what we can do in Ruby 3!
Get familiar with the “right assignment” operator
In Ruby 3 now we have an “assign right” operator. This goes against heaven and allows you to write an expression before assigning it to a variable. So instead of x =: y, you could write: y => x.
What’s very interesting about this is that these big-brained kids who are evolving Ruby 3 realize that they can also use the same right-hand assignment operator to compare patterns. Pattern comparison was introduced in Ruby 2.7 and allows you to write conditional logic to find and extract variables from complex objects.
Let’s write a simple method to try this out. Today, we’ll bring our A game, so let’s call it a_game:
1 2 3 4 5 | <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">a_game</span></span> <span class="token punctuation">(</span> hsh <span class="token punctuation">)</span> hsh <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token punctuation">}</span> puts <span class="token string">"`a` is <span class="token interpolation"><span class="token delimiter tag">#{</span> a <span class="token delimiter tag">}</span></span> , of type <span class="token interpolation"><span class="token delimiter tag">#{</span> a <span class="token punctuation">.</span> <span class="token keyword">class</span> <span class="token delimiter tag">}</span></span> "</span> <span class="token keyword">end</span> |
Now we can pass in a hash string and see what happens!
1 2 3 4 5 6 7 8 | a_game <span class="token punctuation">(</span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token number">99</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment"># `a` is 99, of type Integer</span> a_game <span class="token punctuation">(</span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token string">"asdf"</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment"># `a` is asdf, of type String</span> |
But what happens when we pass a hash string that does not contain the key “a”?
1 2 3 4 | a_game <span class="token punctuation">(</span> <span class="token punctuation">{</span> b <span class="token punctuation">:</span> <span class="token string">"bee"</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment"># NoMatchingPatternError ({:b=>"bee"})</span> |
Dang it, we get a runtime error. That’s what happens if you lack a key of the hash. But if you like to fail gracefully, rescue will come rescue you. You can rescue at the method level, but most likely you want to rescue at the command level. Let’s edit our method:
1 2 3 4 5 | <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">a_game</span></span> <span class="token punctuation">(</span> hsh <span class="token punctuation">)</span> hsh <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token punctuation">}</span> <span class="token keyword">rescue</span> <span class="token constant">NoMatchingPatternError</span> puts <span class="token string">"`a` is <span class="token interpolation"><span class="token delimiter tag">#{</span> a <span class="token delimiter tag">}</span></span> , of type <span class="token interpolation"><span class="token delimiter tag">#{</span> a <span class="token punctuation">.</span> <span class="token keyword">class</span> <span class="token delimiter tag">}</span></span> "</span> <span class="token keyword">end</span> |
And try again:
1 2 3 4 | a_game <span class="token punctuation">(</span> <span class="token punctuation">{</span> b <span class="token punctuation">:</span> <span class="token string">"bee"</span> <span class="token punctuation">}</span> <span class="token punctuation">)</span> <span class="token comment"># `a` is , of type NilClass</span> |
Now that you have the nil value, you can write defensive code to fix the missing data.
So what about the rest?
Looking back at our original array decomposition example, we could get an array of all the values in addition to the first values we retrieved as a variable. Wouldn’t it be great if we could do that with hashes as well? Now we can!
1 2 3 4 | <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token number">2</span> <span class="token punctuation">,</span> c <span class="token punctuation">:</span> <span class="token number">3</span> <span class="token punctuation">,</span> d <span class="token punctuation">:</span> <span class="token number">4</span> <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">{</span> a <span class="token punctuation">:</span> <span class="token punctuation">,</span> b <span class="token punctuation">:</span> <span class="token punctuation">,</span> <span class="token operator">*</span> <span class="token operator">*</span> rest <span class="token punctuation">}</span> <span class="token comment"># a == 1, b == 2, rest == {:c=>3, :d=>4}</span> |
But there is more! Pattern assignment and matching must actually work with arrays too! We can copy our original example like so:
1 2 3 4 | <span class="token punctuation">[</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token number">3</span> <span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">,</span> <span class="token number">5</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">[</span> a <span class="token punctuation">,</span> b <span class="token punctuation">,</span> <span class="token operator">*</span> rest <span class="token punctuation">]</span> <span class="token comment"># a == 1, b == 2, rest == [3, 4, 5]</span> |
Also, we can do some bad boy things like pull out the array tiles before and after certain values:
1 2 3 4 | <span class="token punctuation">[</span> <span class="token operator">-</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token number">3</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">[</span> <span class="token operator">*</span> left <span class="token punctuation">,</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token operator">*</span> right <span class="token punctuation">]</span> <span class="token comment"># left == [-1, 0], right == [3]</span> |
Assign right to match pattern
You can use the right assignment technique in a pattern matching expression to retrieve different values from an array. In other words, you can retrieve everything up to a particular category, take the value of that type and then retrieve everything afterwards.
You do this by specifying the type (class name) in the template and using => to assign any type of that type to the variable. You can also enter categories without specifying the right to “skip” those categories and move on to the next match.
Let’s look at the following examples:
1 2 3 4 5 6 7 8 9 | <span class="token punctuation">[</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token string">"ha"</span> <span class="token punctuation">,</span> <span class="token number">4</span> <span class="token punctuation">,</span> <span class="token number">5</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">[</span> <span class="token operator">*</span> left <span class="token punctuation">,</span> <span class="token builtin">String</span> <span class="token operator">=</span> <span class="token operator">></span> ha <span class="token punctuation">,</span> <span class="token operator">*</span> right <span class="token punctuation">]</span> <span class="token comment"># left == [1, 2], ha == "ha", right == [4, 5]</span> <span class="token punctuation">[</span> <span class="token number">8</span> <span class="token punctuation">,</span> <span class="token string">"yo"</span> <span class="token punctuation">,</span> <span class="token number">12</span> <span class="token punctuation">,</span> <span class="token number">14</span> <span class="token punctuation">,</span> <span class="token number">16</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">[</span> <span class="token operator">*</span> left <span class="token punctuation">,</span> <span class="token builtin">String</span> <span class="token operator">=</span> <span class="token operator">></span> yo <span class="token punctuation">,</span> <span class="token builtin">Integer</span> <span class="token punctuation">,</span> <span class="token builtin">Integer</span> <span class="token operator">=</span> <span class="token operator">></span> fourteen <span class="token punctuation">,</span> <span class="token operator">*</span> right <span class="token punctuation">]</span> <span class="token comment"># left == [8], yo == "yo", fourteen == 14, right == [16]</span> |
Delicious =))
Pin operator
What if you don’t want to hard-set a value in a pattern but come from somewhere else? After all, you can’t put existing variables directly into the templates:
1 2 3 4 5 6 | int <span class="token operator">=</span> <span class="token number">1</span> <span class="token punctuation">[</span> <span class="token operator">-</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token number">3</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">[</span> <span class="token operator">*</span> left <span class="token punctuation">,</span> int <span class="token punctuation">,</span> <span class="token operator">*</span> right <span class="token punctuation">]</span> <span class="token comment"># left == [], int == -1 …wait wut?!</span> |
But in fact you can! You just need to use the pin ^ operator. Try again!
1 2 3 4 5 6 | int <span class="token operator">=</span> <span class="token number">1</span> <span class="token punctuation">[</span> <span class="token operator">-</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">0</span> <span class="token punctuation">,</span> <span class="token number">1</span> <span class="token punctuation">,</span> <span class="token number">2</span> <span class="token punctuation">,</span> <span class="token number">3</span> <span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">[</span> <span class="token operator">*</span> left <span class="token punctuation">,</span> <span class="token operator">^</span> int <span class="token punctuation">,</span> <span class="token operator">*</span> right <span class="token punctuation">]</span> <span class="token comment"># left == [-1, 0], right == [2, 3]</span> |
You can even use ^ to match previously specified variables in the same pattern. Yeah, that’s crazy. Take a look at this example from the Ruby documentation:
1 2 3 4 5 6 | jane <span class="token operator">=</span> <span class="token punctuation">{</span> school <span class="token punctuation">:</span> <span class="token string">'high'</span> <span class="token punctuation">,</span> schools <span class="token punctuation">:</span> <span class="token punctuation">[</span> <span class="token punctuation">{</span> id <span class="token punctuation">:</span> <span class="token number">1</span> <span class="token punctuation">,</span> level <span class="token punctuation">:</span> <span class="token string">'middle'</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> id <span class="token punctuation">:</span> <span class="token number">2</span> <span class="token punctuation">,</span> level <span class="token punctuation">:</span> <span class="token string">'high'</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> jane <span class="token operator">=</span> <span class="token operator">></span> <span class="token punctuation">{</span> school <span class="token punctuation">:</span> <span class="token punctuation">,</span> schools <span class="token punctuation">:</span> <span class="token punctuation">[</span> <span class="token operator">*</span> <span class="token punctuation">,</span> <span class="token punctuation">{</span> id <span class="token punctuation">:</span> <span class="token punctuation">,</span> level <span class="token punctuation">:</span> <span class="token operator">^</span> school <span class="token punctuation">}</span> <span class="token punctuation">]</span> <span class="token punctuation">}</span> <span class="token comment"># id == 2</span> |
In case you don’t understand the confusing syntax it first specifies the value of the school (in this case “high”), then it looks for the hash in the school array where the level matches the school. The id value is then assigned from that hash, in this case 2.
So these are all surprisingly powerful things. You can of course use pattern match in conditional logic, such as the case where all the original Ruby 2.7 examples showed up, but I tend to think that assigning to the right is even useful. more useful in many cases.
Conclude
While you may not be able to take advantage of all this flexibility if you haven’t been able to upgrade your code base to version 3 of Ruby yet, it’s one of the features I feel you’ll really give up. when you have experienced it, just like the keyword arguments when they are first published. I hope you’ve enjoyed getting into decomposition and pattern matching! Stay tuned for more examples of right assignment and how they improve the readability of Ruby patterns.