What you (probably) don't know yet about pattern matching in Ruby?
Jędrzej (NJ) Urbański
Posted on January 30, 2023
So you most probably know about pattern matching in Ruby, right? No...
Well, let's recap what everyone is saying about it.
As for ruby-docs:
Pattern matching is a feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables.
Yawn... 🥱 Are you sleep? No? Good! Yes? Well, I'm not surprised...
Ok, stay alert now...
This is what typical "Pattern Matching in Ruby 3" article will tell you...
I assume that you already read article like this:
You can pass variable to
case
statement and match it.case [1337, 420, 2137] in foo, *bar "matched. foo was: #{foo}" else "not matched." end => "matched. foo was: 1337"
And it's great classic example how you can use pattern matching.
... but are they telling you this?
You can use pattern matching in ruby in other was then just case
statement.
Let me introduce you to ✨ =>
and in
✨ operators, which could be used as standalone expression:
<expression> => <pattern>
<expression> in <pattern>
The only difference between these two is that in
returns boolean value, and does not raise an error when pattern is not met. However =>
raises NoMatchingPatternError
or returns nil
.
How to use it?
Interesting use case might be when using guard clause. It produces quite powerful ruby one-liner.
result = {mail: "me@nj.test"}
return mail_address if result in {mail: mail_address}
or even shorter:
result = {mail: "me@nj.test"}
return mail if result in {mail:}
Testing with =>
Let's create method:
def create_user(params)
user = {type: :user, name: params[:name]}
if params.key?(:mail)
user[:mail] = params[:mail] unless params[:mail].nil?
end
user
end
Let's write simple test for it:
# rspec
describe "create_user/1" do
it "creates user" do
user = create_user({name: "Mary"})
user => {type:, name:}
# => type = :user, name = "Mary"
end
it "creates user with mail if given" do
user = create_user({name: "John", mail: "john@doe.test"})
user => {type:, name:, mail:}
# => type = :user, name = "John", mail = "john@doe.test"
end
end
We are testing here if method create_user/1
are creating has with expected keys.
The test above will pass, because it is matching all key in our user hash.
Now, let's write test that will intentionally fail:
# rspec
describe "create_user/1" do
# ...
it "will fail, becuase mail is nil" do
user = create_user({name: "John", mail: nil})
user => {type:, name:, mail:}
# NoMatchingPatternKeyError
end
end
1) create_user/1 creates user with mail if given
Failure/Error: user => {type:, name:, mail:}
NoMatchingPatternKeyError:
{:type=>:user, :name=>"John"}: key not found: :mail
As you can see test case above fails, because it cannot match :mail
in user-hash. Value for this key does not exists.
We don't need to use asserts, =>
will throw NoMatchingPatternKeyError
which make test fail.
Testing with in
Okay, let's now rewrite test using in
operator.
# rspec
describe "create_user/1" do
# ...
it "will fail, becuase mail is nil" do
user = create_user({name: "Mary", mail: nil})
user in {type:, name:, mail:}
# type = :user, name = "Mary", mail = nil
assert mail
end
end
As in
does not throw any error, we need to assert mail, so we know it's now nil
.
Thanks! 👋 Hope you liked. Comments how you will use pattern matching in your code. 🤗 🗣 ⤵️
Posted on January 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.