Ruby 2.7 experimental pattern matching in few examples
TL;DR
Ruby 2.7 landed with experimental pattern matching feature baked-in (similar to Elixir or Haskell one). Check the syntax definition! Can’t read syntax definition or looking for more details and few examples? Just continue reading.
This is not technical paper. To make it easier to understand (and for my lack of knowledge) I’m probably not using proper terms and names in this article.
What is pattern matching?
As mentioned by Kazuki Tsujimoto (author of pattern matching in Ruby 2.7), there is a good explanation of pattern matching concept in Learn You a Haskell for Great Good! book by Miran Lipovača.
Pattern matching consists of specifying patterns to which some data should conform and then checking to see if it does and deconstructing the data according to those patterns.
It is already common technique in various languages like Elixir and Haskell. Starting Ruby 2.7 experimental support is present for us (Rubyists) as well.
I see pattern matching as 2 steps process:
- looking for suitable (matching) pattern
- deconstruct by matching pattern
If you’re advanced Ruby programmer, you should be familiar with deconstructing already.
Deconstructing array
In Ruby it is already possible to deconstruct array by “multi-assignment” for years. We can take a look at few examples just to remind this. Feel free to skip to next section if you’re familiar with this already.
Looks familiar, doesn’t? We have array of three names and we can deconstruct that array into three variables in one line. If there’s more elements in array than available variables to deconstruct, the rest of the array will be ignored.
When there’s less elements than available variables nil
values are used to fill in those "overlapping" variables.
Easy. It is also possible to gather “overlapping” elements using *
.
And finally it is possible to skip some elements.
In summary, we do assign array to “multiple variables” and it is deconstructed by rules defined on the left side of assignment. And that’s it. We can consider this “simple pattern matching”. We have pattern on the left side of assignment, data on the right side and data are deconstructed based on pattern (left side).
Array and pattern matching in Ruby 2.7
In Ruby 2.7 there’s experimental support (you’ll see warning on $STDOUT
using this feature) for native pattern matching syntax. Let’s try to use new Ruby syntax ( in
) to “refactor” our first example with names.
Cool! It works. But how is this more useful than good ol’ plain array deconstruction? Let’s try it with some “non-matching” pattern. We can try to deconstruct array to less variables than elements.
Whoops. We have provided pattern of array having 3 elements and tried to match it to array having 4 elements. Pattern wasn’t matched in this case and exception was raised. If we would like to gather elements in array, we can still use *
to define different pattern. Getting back to primes
examples with the middle
gathering all except first and last element, we can use pattern matching for this as well.
Also it is possible to skip the part we do not care about using the same approach as we did in array deconstructing before (using *
).
As you can see the main difference, pattern matching on array is more strict of pattern provided.
Hash and pattern matching in Ruby 2.7
Deconstructing of arrays is really useful, but what about another objects? Let’s try to deconstruct hash the old way (similar to array one).
OK, it doesn’t work. Whole hash is just assigned to name
and the rest of variables is assigned to nil
. I would be really surprised to see this successfully deconstructed, since hash is not ordered structure and we do not provide any rule (pattern) how to deconstruct the hash. Let's try to deconstruct our hash via new Ruby 2.7 way using in
.
Providing pattern (recipe how to deconstruct our hash — {name: name, role: position}
) was enough to get variables assigned. We can use any variable name for destructing. In original hash there's key role
, but we define in pattern to deconstruct that value to variable position
.
Pattern matching in case statement in Ruby 2.7
“Inline deconstructing” is fine, but pattern matching is even more powerful combined with case
statement.
We have array of hashes with inconsistent keys and case
statement using various patterns.
in {name:, role: 'Leader'}
matching hash havingname
key (assigning it’s value toname
variable) and havingrole
equal toLeader
.in {name: , role: position}
hash havingname
key (assigning it’s value toname
variable) and having anyrole
(passing it’s value toposition
position variable).in {name:}
hash havingname
key (assigning it’s value toname
variable)in {role: position}
having anyrole
(passing it’s value toposition
position variable).else
statement to fallback to when no pattern is matched. If we omitelse
statement and no pattern is matchedNoMatchingPatternError
is raised (same as we seen before in one-line version).
Only first matched pattern is used. This is same to case when
statement.
TIP: If hash key and assigned variable share the same name, we can use shorter definition of pattern in {name:}
which is equal to in {name: name}
. Shorthand property names (from ES2015) anyone?
I’m trying to stick with simple examples. Imagine people
is not array of simple hashes, but array of complex structures (like nested hashes with arrays as values...). Pattern matching supports more complex patterns including deeper matching of structures and even some type checking! If you would like to see more advanced structures and pattern matching in action take a look at demo by Context Free.
Pattern matching in action
Finally we will take a look at some “close enough to real world” usage. HTTP router!
Let’s build simple HTTP endpoint using Rack following few simple rules:
- successfully responding to
GET
request method requests - printing back query params if present
- printing back static text if no query params present
- return status code 405 (method not allowed) for non
GET
requests - return status code 400 (bad request) if request method is not provided
I’ll start (for real TDD/BDD experience) with Gemfile and test.
Feel free to make those tests green as a homework. Here’s my simple Rack implementation.
… composing and running tests…
OK, we have prototype now. Time to rewrite this using new pattern matching feature. I recommend you to try this on your own for real Ruby pattern matching experience. My simple implementation follows.
And finally we can check our new pattern matching based implementation status.
It works! Time to compare both implementations side to side. It can take some time to get comfortable reading case in
statements effectively, but once you get better friends, you’ll get excited by the difference (at least I hope).
Even in this simple example, pattern matching based implementation is easier to read and to understand for me. Have you been battling huge incoming JSON payloads? Do you already see how pattern matching can be useful?
TIP: You can find my router code at gist as well.
History, present, future…
This is just start. Pattern matching allows much more than I was able to showcase in this article.
Array and Hash are not the only classes implementing deconstruction logic. Even your custom classes can implement it. If you’re interested into more details of supported patterns and how to implement deconstructing logic into custom class please take a look at awesome presentation by Kazuki Tsujimoto done at RubyConf 2019 (slides with various examples available at speakerdeck). A lot of examples are also part of Ruby test suite. Additional interesting notes and history of this feature is present in official Ruby Redmine. It is worth it to check as well.
As you can see Ruby is not dead language. It is moving forward. Sometimes it’s successful and publicly well accepted move. Sometimes it is less successful and publicly not well accepted experimental feature removed before final release (sending my condolences to Ruby’s take on pipeline |>
operator).
I hope this is start of new way of thinking about Ruby coding with advanced data structures. I'm pretty excited and looking forward! Thanks everyone involved in this new experimental feature.
As usual, feel free to ping me here, at reddit comments or at @retrorubies with your corrections, questions, experiences, alternatives or any ideas related to this post. Would you like to see more on this topic? Ping me as well!