Ruby Pattern Matching

Sebastian Suttner
Sep 3 · 5 min read

Pattern Matching is finally coming to Ruby in version 2.7, and I’m very excited for it to be one more tool in the toolbelt of us Rubyists.
You might or might not already know what Pattern Matching is; either way, this post is meant to give a very simple crash-course or go-to reference.

Kazuki Tsujimoto presented very straightforward definitions in RubyKaigi 2019:

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)

Especially for Rubyists, he stated:

It’s basically “case/when” in conjunction with “multiple assignment”


Syntax

case expr
in pattern [if|unless condition]
...
in pattern [if|unless condition]
...
else
...
end
  • Like case, patterns are run in sequence until there is a match
  • NoMatchingPatternError is raised, if no match and no else clause exists
  • When using guards, the guard expression is evaluated if the preceding pattern matches

Here is a very simple example:

case [0, 1]
in [a, b] unless a == b
:match
end
#=> :match

Patterns

A value pattern matches an object such that pattern === object. The best definition for thesubsumption operator (===) I could find is:

“If I have a drawer labelled pattern would it make sense to put object in that drawer?”

case 0
in 0
:match
end
#=> :match
case 0
in -1..1
:match
end
#=> :match
case 0
in 3..6
:no_match
end
#=> NoMatchingPatternError (0)
case 0
in Integer
:match
end
#=> :match
case 0
in String
:no_match
end
#=> NoMatchingPatternError (0)

When using a variable pattern it binds the variable to the value that matches the pattern.

case 0
in a
a
end
#=> 0

A variable pattern always binds the variable even if you have anouter variable with the same name.

a = 0
case 1
in a
a
end
#=> 1

To overcome this, use^:

a = 0
case 1
in ^a # this translates to: `in 0`
:no_match
end
#=> NoMatchingPatternError

Using _ can be useful to drop/skip values:

case [0, 1]
in [_, a]
a
end
#=> 1

An alternative pattern matches if any of the patterns matches.

case 0
in 0 | 1 | 2
:match
end
#=> :match

An as pattern can be used to assign the value that matches the pattern that lives on the left side of the hash rocket (=>), to the variable that lives on the right side.

case [0, 1]
in [0, a => b]
b
end
#=> 1

Here are some basic array pattern examples:

case [0, 1, 2]
in [0, a, 3]
:no_match
end
#=> NoMatchingPatternError ([0, 1, 2])
case [0, 1, 2]
in [0, a, 2]
a
end
#=> 1
case [0, 1, 2]
in [0, *tail]
tail
end
#=> [1, 2]

There is syntactic sugar that allows us to do:

case [0, 1, 2]
in Array(0, a, 2)
a
end
#=> 1
case [0, 1, 2]
in Object[0, a, 2]
a
end
#=> 1

We can even omit the brackets []:

case [0, 1, 2]
in 0, a, 2
a
end
#=> 1

Actually, array pattern is not solely for Array objects.

An array pattern will also match if object has a #deconstruct method that returns Array, and pattern matches the Array that object.deconstruct returns.

Here is an example with Struct:

class Struct
def deconstruct
to_a
end
end
Color = Struct.new(:r, :g, :b)color = Color[0, 10, 20]
color.deconstruct
#=> [0, 10, 20]

In the following example, pattern will need to match color.deconstruct which results in [0, 10, 20] .

case color 
in Color[0, 0, 0]
:no_match
in Color[255, 0, 0]
:no_match
in Color[r, g, b]
"#{r}, #{g}, #{b}"
end
#=> "0, 10, 20"

Here are some basic hash pattern examples:

case {a: 0, b: 1, c: 2}
in {a: 0, b: b, c: 3}
:no_match
end
#=> NoMatchingPatternError ({:a=>0, :b=>1, :c=>2})
case {a: 0, b: 1, c: 2}
in {a: 0, b: b, c: 2}
b
end
#=> 1
case {a: 0, b: 1, c: 2}
in {a: 0, **rest}
rest
end
#=> {:b=>1, :c=>2}

There is syntactic sugar that allows us to do:

case {a: 0, b: 1, c: 2}
in Hash(a: 0, b: b, c: 2)
b
end
#=> 1
case {a: 0, b: 1, c: 2}
in Object[a: 0, b: b, c: 2]
b
end
#=> 1

And b: is equivalent to b: b:

case {a: 0, b: 1, c: 2}
in {a:, b:, c:}
b
end
#=> 1

We can even omit the curly brackets {}:

case {a: 0, b: 1, c: 2}
in a: 0, b: b, c: 2
b
end
#=> 1
case {a: 0, b: 1, c: 2}
in a:, b:, c:
b
end
#=> 1

One big difference that hash pattern has with array pattern, is that hash pattern matches subsets of Hashes, while array pattern matches exact Arrays.

case {a: 0, b: 1, c: 2}
in b:
b
end
#=> 1

Actually, hash pattern is not solely for Hash objects.

A hash pattern will also match if object has a #deconstruct_keys method that returns Hash, and pattern matches the Hash that object.deconstruct_keys(keys) returns.

Here is an example with Date:

class Date
def deconstruct_keys(keys)
{year: year, month: month, day: day}
end
end

The keys argument that deconstruct_keys receives is the set of keys present in the pattern.

In the implementation above, we are not actually leveraging the keys argument. But, it’s very important thatdeconstruct_keys is implemented as efficiently as possible to improve the overall efficiency of case. Therefore, here is a smarter implementation where only the required keys are deconstructed:

class Date
VALID_KEYS = %i(year month day)
def deconstruct_keys(keys)
if keys
(VALID_KEYS & keys).each_with_object({}) do |k, h|
h[k] = send(k)
end
end
end
end
today = Date.today
today.deconstruct_keys([:year])
#=> {year: 2019}

If **rest is specified in the pattern, nil is passed in the keys argument. In that scenario, we need to return all key-value pairs. Let’s add an else clause:

class Date
VALID_KEYS = %i(year month day)
def deconstruct_keys(keys)
if keys
(VALID_KEYS & keys).each_with_object({}) do |k, h|
h[k] = send(k)
end
else
{year: year, month: month, day: day}
end
end
end
today = Date.today
today.deconstruct_keys(nil)
#=> {year: 2019, month: 9, day: 1}

Finally, in the following example, pattern will need to match today.deconstruct_keys([:year]) which results in {year: 2019}.

case today
in year:
year
end
#=> 2019

Conclusion

It becomes obvious how this is going to come in handy. It becomes easier to deconstruct complex objects and work with their data. This will help us reduce the complexity of the code and increase its maintainability.

Things might still change around Pattern Matching; but don’t worry, Ruby makes sure to remind this to us whenever we use it:

warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

Cedarcode

Adding value from day one. Effective software development & expertise for your products and teams.

Sebastian Suttner

Written by

Co-founder & Software Engineer at @cedarcode

Cedarcode

Cedarcode

Adding value from day one. Effective software development & expertise for your products and teams.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade