Pattern Matching in Ruby 2.7.0

A Junior Developer way to understand it.

Patryk Stępień
akra-polska
11 min readMay 6, 2020

--

Photo by Rod Long on Unsplash

What is Pattern Matching?

Kazuki Tsujimoto described it in his presentation at RubyKaigi 2019 as

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 — Learn You a Haskell for Great Good! (Miran Lipovaca)

In other words, with pattern matching we obtain specific parts of data selected on specified rules.

In his presentation we can also find a sentence, that for Rubyists pattern matching is a case/when with a multiple assignment.

Basics and syntax

https://gist.github.com/droznyk/6d526bcb6743b80f96c31f420f5f7aae

Just like in normal case , the patterns are checked in sequence until the first one that matches. It means that if we use a multi-pattern matching, we have to take care about the order of patterns.

As we can see from the syntax in the example, pattern can be followed by a guard expression. The guard expression is considered only if the preceding pattern matches.

If the pattern is not found, the else clause will be executed. In case when there’s no pattern nor else clause found, it will raise the NoMatchingPatternError exception.

Patterns

In patterns we can use all Ruby literals: Booleans, nil, Numbers, Strings, Symbols, Arrays, Hashes, Ranges, Regular Expressions and Procs.

Value pattern

In a value pattern the condition for matching is pattern === object

As I mentioned before, we have to be aware of the order of the patterns because the sequence will stop on the first match.

Variable pattern

This pattern matches any passed value and assigns it to a variable. Here’s the example:

We can also use a variable pattern with arrays to deconstruct the array:

Or underscore to don’t care:

Keep in mind that if you defined a variable before and you’ll use it in pattern matching, then this variable will be overwritten.

When we need to match against the variable we defined before, there is a solution. Variable pattern matching provides the pin operator ^ known from Elixir. By using this pattern with the pin operator — matcher will not overwrite the variable and will use the value of the variable.

Let’s consider two scenarios:
1. The variable and the case have the same value.
2. The variable and the case have different values

Alternative pattern

The alternative pattern achieves a match when any of patterns matches.

As pattern

In this pattern the value is assigned to a variable only if the pattern matches.

We can use it, for example, to pick a specific part of a complex object:

Array pattern

Before the examples, we have to discuss the rules that this pattern has. Firstly, the name of this pattern is a little bit misleading. Array patterns are used not only for array objects. Secondly, as Kazuki Tsujimoto described it, the array pattern matches if:

  • Constant === object returns true . Constant can be an
    Array(1, 2, ...) , an Object[1, 2, ...] or [1, 2, ...] ,
  • The object has a #deconstruct method that returns Array,
  • The result of applying the nested pattern to object.deconstruct is true .

The simplest example of where all patterns are correct is:

So with array pattern we can deconstruct arrays:

Or get one value from it:

Splat operator * can be helpful with arrays of unknown size or when we want to pick only a part of the array.

The best thing is that we can combine the patterns!

As I mentioned before, array pattern can be used for other objects, for example: Structs. Let’s say that we have a Struct called Desk and it responds to the .deconstruct method.

We can use array pattern in this case.

There’s one more thing I have to notice here. Array pattern is an exact match by default. It will check both values and the structure of data.

Tuple matching with array pattern

Tuples are common in Elixir language. They can be described as a list that can hold any value, e.g. {"John", 25} . This tuple can be matched, so assigned to variables from the left side of =, like this:

Tuples are commonly used in Elixir to return a result from a function call. We can call a function and get the {:ok, result} or {:error, errors} tuples as a result. What if we would like to do the same in Ruby? Let’s check.

I created a simple User class with authorization method that checks if the provided password matches with the user password — let’s skip the implementation details. Our user is John Doe and his password is foobar. Now we have to create a method for tuple matching:

And check the results. Matching with valid password:

Matching with invalid password:

Looks like we can do tuple matching in more or less the same way as in Elixir. :)

Hash pattern

The situation with hash pattern is quite similar to that of the array pattern. Hash patterns are not only for hash objects and also have rules. This pattern matches if:

  • Constant === object returns true . In this case Constant can be a Hash(one: "one", two: "two", ...) , an Object[one: "one", two: "two", ...] or { one: "one", two: "two", ... } ,
  • The object has a #deconstruct_keys method and this method returns Hash,
  • The result of applying the nested pattern to object.deconstruct_keys(keys) is true.

The simplest example of where all patterns are correct is:

I’m sure you have already noticed that I used y: here. Why the x: value is not used? The answer is: because it’s not necessary. The hash pattern is a subset match. It means that we can achieve a match with only a subset of fields in pattern.

In array pattern we used a splat operator to grab a bigger part of data.
In this pattern we also have the possibility to do that — the double splat operator ** .

Hash pattern will work with any object that responds to #deconstruct_keys method. Let’s test it! We’ll create a simple Struct named Person .

Let’s see how hash pattern will behave in this case.

It works pretty well. I was really curious what exactly was sent to the deconstruct_keys method as keys parameter. As you can guess, it was [:name, :age, :city] . But, what if we will use the double splat operator and check the keys ? Let’s add the p keys at the beginning of #deconstruct_keys method and see what’s the value.

The keys parameter contains nil . This behavior is also described in Kazuki Tsujimoto presentation, but for me it’s a little bit strange. I would expect that the keys variable contains at least the :name key because I asked for it.

When we’re using a double splat operator in a hash pattern, we also have to keep in mind that we should return all key-value pairs in deconstruct_keys method. It makes no sense to ask for the rest of the hash if we don’t provide that, right? 😄

There’s one more thing about this pattern. What should {} match? Hash pattern is a subset match so we suppose it will match any hash object. On the other hand, we might expect that {} will match only with empty hash object. Both ways seem correct, depending on what we need. However, Ruby does it in the second way.

JSON example

Since we went through all the patterns, we can start with a more practical example and convince ourselves — if you’re not convinced yet — that pattern matching is awesome.

Thanks to SWAPI we have a structure like this.

input.json

Let’s store it in a file and load it into a variable.

Now we can go to exercise. I know that Chewbacca was a pilot of the Millennium Falcon, but I forgot which race he belongs to… Fortunately, I can get this information from the file using matching pattern!

Fantastic! Now I recall that he’s a Wookiee! But wait… If I just wanted to get the information about Chewbacca, then why I also added the *rest_of_array ? Let’s delete that unnecessary part.

Oops, we get the NoMatchingPatternError . It’s because the Array pattern is an exact match, as we discovered before, so we have to handle the rest of the array too.

Chewbacca was a first element of pilots array, so it was an easy peasy to match his data. What if I would like to get the same information about Lando Calrissian? Let’s check it.

It will not work this way. So how can I get this desired information? I’ve played around a bit and I haven’t found any nice solution using pattern matching, so I went plain ruby.

As we can see, pattern matching is not a solution for all problems. However, it’s still an easier and more pleasant way to handle JSON data than an army of conditional statements. At least for me.

Work in progress

Let’s go back to the hash pattern. Have you noticed that in every single example I’ve used symbol as a key? It’s because this pattern doesn’t support the non-symbol keys at the moment. Why? It’s not a trivial problem, so once again I will refer to Kazuki Tsujimoto presentation, where you can find a nice explanation.

Another thing is that combining some patterns with guard expression can result with strange behavior. Literally, we can check the value of a variable even if the match failed.

The last thing I’ve noticed is that we can’t use complex expressions (e.g. method calling) in patterns. Let’s define a Cat class and a cat object.

Sanity check before we do a pattern matching.

When we use a cat.meow in pattern matching, it will throw a syntax error. To see the error we have to run the code from a file.

Of course we can achieve the match with a little change in code.

Pattern matching is a great and powerful tool that we can use in many, many, many ways. I really enjoyed testing its power. However, at the end of the day we have to remember one thing that irb told me a billion times.

Thank you for reading this article. I hope you enjoyed it. Feel free to leave a comment and start a discussion!

--

--