Ruby 2.7 experimental pattern matching in few examples

Josef Šimánek
6 min readDec 25, 2019

--

Which one matches that pattern?

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:

  1. looking for suitable (matching) pattern
  2. 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.

deconstructing array with matching variables and element sizes

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.

deconstructing array to less variables than element sizes

When there’s less elements than available variables nil values are used to fill in those "overlapping" variables.

deconstructing array to more variables than element sizes

Easy. It is also possible to gather “overlapping” elements using *.

deconstructing array to less variables than element sizes and gather values

And finally it is possible to skip some elements.

deconstructing array to less variables than element sizes skipping 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.

YAY! Pattern matching works in 2.7!

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.

BOOM! Pattern matching wasn’t successful.

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.

pattern matching and values gathering

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 *).

pattern matching can skip values as well

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).

Yes, this doesn’t work. Have you been expecting different result?

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 recipe (pattern) how to deconstruct hash makes pattern matching happy.

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.

Hello! I’m Hopík and I’m totally unrelated to this topic.

Pattern matching in case statement in Ruby 2.7

“Inline deconstructing” is fine, but pattern matching is even more powerful combined with case statement.

Uhh, try to rewrite this without pattern matching to get additional bonus excitement for pattern matching.

We have array of hashes with inconsistent keys and case statement using various patterns.

  • in {name:, role: 'Leader'} matching hash having name key (assigning it’s value to name variable) and having role equal to Leader .
  • in {name: , role: position} hash having name key (assigning it’s value to name variable) and having anyrole (passing it’s value to position position variable).
  • in {name:} hash having name key (assigning it’s value to name variable)
  • in {role: position} having anyrole (passing it’s value to position position variable).
  • else statement to fallback to when no pattern is matched. If we omit else statement and no pattern is matched NoMatchingPatternError 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.

I remember reading some Elixir tutorial explaining pattern matching on JSON-like structures. It amazed me and made me envy. There’s nothing similar in Ruby itself. With Ruby 2.7 my dream is closer to become reality.

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:

  1. successfully responding to GET request method requests
  2. printing back query params if present
  3. printing back static text if no query params present
  4. return status code 405 (method not allowed) for non GET requests
  5. return status code 400 (bad request) if request method is not provided

I’ll start (for real TDD/BDD experience) with Gemfile and test.

rack, rack-test, minitest is all we need
Simple test describing our requirements in rack-test DSL.

Feel free to make those tests green as a homework. Here’s my simple Rack implementation.

Wanna try me in browser? Try “bundle exec rackup condition_router.rb” in your console and reach http://localhost:9292 in your browser.

… composing and running tests…

Just including original RouterTest and assigning initial implementation to app method. Yes, that how rack-test works.
To be honest, it took me few iterations to get all tests passing.

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.

Wanna try me in browser as well? Try “bundle exec rackup pattern_router.rb” in your console and reach http://localhost:9292 in your browser.
Again, just providing proper app to rack-test.

And finally we can check our new pattern matching based implementation status.

To be honest, it took me just one iteration to get all tests passing.

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!

--

--