Last December we got for Christmas Ruby 2.7. release and between its many interesting features and improvements, one in specific is pretty much a first for the ruby language.
This article is an overview of it, ideal for ruby beginners curious about Ruby new features.
Pattern Matching
The experimental feature Pattern matching is, in the words of its creator, 'a combination of case/when and multiple assignments' and, if you are coming from Javascript, it might look like a quite lean switch statement.
That means we now can combine deconstruct arrays and hashes, and with matching its items with specific patterns or classes.
Pattern matching can be especially powerful when working with complex objects, like parsed JSON, YAML files, or even HTTP routers.
That all sounds very cool and all, but first let's play a bit with the basics of the feature.
Value pattern
Let's get used to the basic syntax.
It starts with the keyword case
followed by the object or variable you will be matching.
case 67
Then we add the first matching case with the keyword in
in the left.
In this one, we want to test if 67 is between 0 and 100.
case 67
in 0..100
This is the only possibility we want to match for right now, so let's ask it to print something if it matches and close it with the end
keyword.
case 67
in 0..100
p 'it\'s a match!'
end
It should output "It's a match"
since the condition we matched for was true.
What if it's not the case though?
Let's test it again but with a number in the case that will fail the matching.
case 167
in 0..100
p 'it\'s a match!'
end
=> NoMatchingPatternError (167)
When the matching fails it outputs a NoMatchingPatternError followed by the tested object inside a parenthesis.
Let's handle when we don't have a match with an else
.
case 167
in 0..100
p 'it\'s a match!'
else
p 'Number not in range :-('
end
=> "Number not in range :-("
That's way better than getting an error.
It's possible to add multiple matching conditions too.
case 80
in 0..10
p 'A small number'
in 11..50
p 'An okay sized number'
in 51..100
p 'A respectable sized number'
else
p 'Number not in range :-('
end
=> "A respectable sized number"
Variable pattern
What if we want to print the value that got matched itself?
That's when binding a variable to it comes in handy. Let's start simple.
case 100
in num
p "num got its name bound to #{num}"
end
=> "num got its name bound to 100"
And now we can combine the variable binding with the matching.
case 100
in Integer => num
p "Bind #{num} only if matched"
end
=> "Bind 100 only if matched"
In the example above we first make sure that 100 is an Integer, and if that's true we bind its value to the variable num and print the message.
Alternative pattern
Sometimes more than one condition might be good.
make_it = 'better'
case make_it
in 'harder' | 'better' | 'faster' | 'stronger' => like_this
p "Do it #{like_this}"
end
=> "do it better"
As pattern
It's also possible to test regular expressions
bad_email = 'wrongmailATmail.com'
case bad_email
in /.+@.+\.\w/ => email
p "Sending to #{email}"
else
p 'This is not an email'
end
=> "This is not an email"
good_email = 'rightmail@mail.com'
case good_email
in /.+@.+\.\w/ => email
p "Sending to #{email}"
end
=> "Sending to rightmail@mail.com"
With Arrays
The real fun with Pattern matching starts when you use it with objects though. It's possible to match an array all the ways in the example below.
case [0, 1, 2]
in Array(0, 1, 2) => arr
p arr
end
case [0, 1, 2]
in Object[0, 1, 2] => arr
p arr
end
case [0, 1, 2]
in [0, 1, 2] => arr
p arr
end
=> [0, 1, 2]
Be careful with combining syntax sugar and variable binding though.
case [0, 1, 2]
in 0, 1, 2 => arr
p arr
end
=> 2
By default, arrays match exactly. The code bellow should fail the matching.
arr = [0, 1, 2]
case arr
in [1, 2] => a
p a
end
=> NoMatchingPatternError ([0, 1, 2])
If we don't care for the first item the array we can use _
. Now it should only test the second and last array element and ignore the value of the first one.
arr = [0, 1, 2]
case arr
in [_, 1, 2] => a
p a
end
=> [0, 1, 2]
Or even better, we can match only a subset of the array by using *
. Now it will only try to match the last array element with the number 2.
arr = [0, 1, 2]
case arr
in [*, 2] => a
p a
end
=> [0, 1, 2]
We can also bind variables to array elements.
user = [7271, ['Marcela', 'Pena', 26]]
case user
in [id, [first_name, last_name, age]]
p "User #{id}, #{first_name} #{last_name}, is #{age} years old"
end
=> "User 7271, Marcela Pena, is 26 years old"
Let's ignore some of the values with _
, and use a guard clause to check if the user is of age.
user = [7271, ['Marcela', 'Pena', 26]]
case user
in [_, [first_name, _, age]] if age >= 18
p "#{first_name} is #{age} years old"
else
p "User is underage"
end
end
=> "Marcela is 26 years old"
With hashes
my_hash = {a: 0, b: 1, c: 2}
case my_hash
in Hash(a: a, b: 1) => h
p h
end
=> {:a=>0, :b=>1, :c=>2}
case my_hash
in Object[a: a, b: 1] => h
p h
end
=> {:a=>0, :b=>1, :c=>2}
case my_hash
in {a: a, b: 1} => h
p h
end
=> {:a=>0, :b=>1, :c=>2}
# careful if you go no brackets though
case my_hash
in a: a, b: b => h
p h
end
=> 1
Hash patterns are more flexible than array matches, by default they always test for subsets.
my_hash = {a: 0, b: 1, c: 2}
case my_hash
in {b: 1}
p rest
end
=> [[0, 1, 2]]
case {a: 0, b: 1, c: 2}
in a:, b:
p a
p b
end
=> 0
=> 1
It's possible to make exact matches with hashes though.
my_hash = {a: 0, b: 1, c: 2}
case my_hash
in {a: 0, **rest} if rest.empty?
p a
end
=>NoMatchingPatternError ({:a=>0, :b=>1, :c=>2})
other_hash = {a: 0}
case other_hash
in {a: 0, **rest} if rest.empty?
p a
end
=> 0
And power it up with deconstruction
user_hash = {id: 189, name: {first_name: 'Alanis', last_name: 'Morris'}, nick_name: nil, pets: {cats: ['Flufbell', 'Lady Merlin'], dogs: []}}
case user_hash
in {id: nil => id, name: }
# save user if it doesn't have an id and has a name
p 'Save new user!'
in {name:, nick_name: String => nick_name}
p "#{nickname} is all set up"
in {name: {first_name:, last_name:}, nick_name: String => nick_name, pets: {cats:, dogs:}} if cats.empty? && !dogs.empty?
p "#{first_name} nick name will be #{last_name}-woof"
in {name: {first_name:}, nick_name: nil => nick_name, pets: {cats:, dogs:}} if !cats.empty? && dogs.empty?
p "#{first_name} nick name will be Catty#{first_name}"
in {name: {first_name:}, nick_name: nil => nick_name, pets: {cats:, dogs:}} if !cats.empty? && !dogs.empty?
p "#{first_name} nick name will be #{first_name}Meoof"
else
'Sign up'
end
=> "Alanis nick name will be CattyAlanis"
Now that you got used to the basics of pattern matching and really want to go into the meat of it make sure to read the official documentation and other awesome in-depth articles written about it and to watch Kazuki Tsujimoto presenting it at RubyConf 2019.
Let me know if this article was useful for you or if it can be improved.
Happy codding!
Top comments (2)
Declarative programming for the win!
It seem powerful, thanks for writing about it.
It reminds me of pattern matching in Erlang and the sort of declarative matching Go does with JSON.
I wonder how faster/slower it'd be than checking stuff manually, this slide clearly shows how more direct the code looks - even though it might be slightly less readable as probably Rubysts are more accustomed to following conditional logic than these declarations. Especially "variable binding" and the "pin operator" :D
They also added "if/unless preconditions" which once again remind me of Erlang and its "guards".
I'm really, really excited about this! Hopefully it'll be stable for the next major release of Ruby and we can start using it :D
A tip: you can add syntax highlighting to your example by adding the language name after the triple backticks:
was written as:
Thank you I didn't know that!
Yeah, I heard it's not that performative right now but hopefully it will be faster in the future versions.