Ruby Pattern Matching

Sebastian Suttner
Sep 3, 2019 · 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.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store