Sometimes I like new changes, new features, or improvements in Ruby and sometimes not. Today I would like to tell you more about the changes that I’m very excited about. I mean the Pattern Matching. I wrote a separate article about Pattern Matching in Ruby some time ago. Now it’s the time to go deeper into the Pattern Matching news. So let’s get started!
1. One-line Pattern Matching
It’s the Pattern Matching feature I didn’t notice before when I was testing it the first time. Anyway, now we can do pattern matching in one line.
In Ruby 3.0 we have two ways of doing one-line pattern matching: in
and =>
. The behavior is different in both cases. When we use in
the result will be true
or false
. There will be no exception. This behavior allows us to use in
one-line pattern matching inside of blocks like any?
or all?
. I will tell you more about it later. In the case of =>
, we will get an assignment to the variable or an exception. Here are examples:
One-line Pattern Matching with Hash
When we have a match for in
{ foo: 1, bar: 2 } in { foo: f }
=> true
3.0.0> f
=> 1
without match
{ foo: 1, bar: 2 } in { baz: b }
=> false
3.0.0> b
=> nil
The same we can do without a variable declaration
{ foo: 1, bar: 2 } in { foo: }
=> true
3.0.0> foo
=> 1
In the case of =>
match we have
{ foo: 1, bar: 2 } => { foo: f }
=> nil
3.0.0> f
=> 1
without a match, we will get an exception
{ foo: 1, bar: 2 } => { baz: }
Traceback (most recent call last):
4: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>'
3: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load'
2: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
1: from (irb):7:in `<main>'
NoMatchingPatternError ({:foo=>1, :bar=>2})
One-line Pattern Matching with Array
A similar thing we get for Array using in
[1, 2, 3] in [a, 2, 3]
=> true
3.0.0> a
=> 1
and using =>
[1, 2, 3] => [a, 2, 3]
=> nil
3.0.0> a
=> 1
One-line Pattern in Ruby 3.1
First of all, one-line pattern matching in Ruby 3.1 is no longer experimental. The second news is related to parentheses. They can be omitted for array or hash in one-line pattern matching.
Code with parentheses:
3.0.0> [0, 1] => [_, x]
3.0.0> x
=> 1
In Ruby 3.0, we have a syntax error when we omit parentheses.
3.0.0> [0, 1] => _, x
Traceback (most recent call last):
3: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>'
2: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load'
1: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):6: syntax error, unexpected ',', expecting end-of-input)
[0, 1] => _, x
In Ruby 3.1, everything is working.
3.1.0> [0, 1] => _, x
=> nil
3.1.0> x
=> 1
2. Pattern matching for specific beginning and end of Array
[1, 2, 3, 4, 5, 6] in [first, *middle, last]
3.0.0> first
=> 1
3.0.0> middle
=> [2, 3, 4, 5]
3.0.0> last
=> 6
or when we don’t care about the middle part of Array
[1, 2, 3, 4, 5, 6] in [first, *, last]
3.0.0> first
=> 1
3.0.0> last
=> 6
3. Exact match for Hash
As I mentioned in my previous article, an exact match for hashes is different than for arrays. Here I will show you what’s new in these two cases, but a first short reminder about an exact match.
When we have an array without an exact match, we get an error.
case [1, 2]
in [1]
:no_match
end
Traceback (most recent call last):
4: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>'
3: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load'
2: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
1: from (irb):12:in `<main>'
NoMatchingPatternError ([1, 2])
In the case of hash, even there is no exact match between the pattern and input data, we won’t have an error.
case { foo: 1, bar: 2 }
in foo:
:match
end
=> :match
3.0.0> foo
=> 1
If we want to have an exact match for the hash, we can do that in this way:
case { foo: 1, bar: 2 }
in foo:, **rest if rest.empty?
:no_match
end
Traceback (most recent call last):
5: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>'
4: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load'
3: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
2: from (irb):15:in `<main>'
1: from (irb):16:in `rescue in <main>'
NoMatchingPatternError ({:foo=>1, :bar=>2})
but also like that:
case { foo: 1, bar: 2 }
in foo:, **nil
:no_match
end
Traceback (most recent call last):
5: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>'
4: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load'
3: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
2: from (irb):19:in `<main>'
1: from (irb):20:in `rescue in <main>'
NoMatchingPatternError ({:foo=>1, :bar=>2})
4. Exact match for Array
In Ruby 2.7, there was no way to match any array element (something similar like we can do for hash). We could match first or last
case [1, 2, 3]
in [1, *]
:match
end
=> :match
case [1, 2, 3]
in [*, 3]
:match
end
=> :match
but now in 3.0, we can match something in the middle of an array
case [1, 2, 3, 4]
in [*, 2, a, *]
:match
end
=> :match
3.0.0> a
=> 3
We can even name our *
case [1, 2, 3, 4]
in [*first, 2, a, *last]
:match
end
=> :match
3.0.0> a
=> 3
3.0.0> first
=> [1]
3.0.0> last
=> [4]
This feature is more visible on data like this:
json = {
name: "Woman on Rails",
friends: [{ name: "Alex", age: 24 }, { name: "Tom", age: 25 }]
}
json in { name: "Woman on Rails", friends: [*, { name: "Alex", age: age }, *] }
3.0.0> age
=> 24
5. Alternative pattern with variables
We know that we cannot use an alternative pattern with variables
case [1, 2]
in [1, 3] | [1, c]
:match
end
Traceback (most recent call last):
1: from (irb)
SyntaxError ((irb):55: illegal variable in alternative pattern (c))
but there is one exception from that rule. We can use _
:
case [1, 2]
in [1, 3] | [1, _]
:match
end
=> :match
3.0.0> _
=> :match
We can even name it:
case [1, 2]
in [1, 3] | [1, _last]
:match
end
=> :match
3.0.0> _last
=> 2
6. Assign the same variable in our pattern many times
Thanks for ^
we can check pattern matching by using the same variables multiple times.
case { name: "Woman on Rails", people: [{ name: "Alex", age: 24 }, { name: "Woman on Rails", age: 25 }] }
in name:, people: [*, {age:, name: ^name}]
:match
end
=> :match
3.0.0> name
=> "Woman on Rails"
3.0.0> age
=> 25
7. Infinite ranges in Pattern Matching
It’s related to the new Ruby feature for infinite ranges.
case { a: 1, b: 2 }
in a: 0.. => first
:match
end
:match
3.0.0> first
=> 1
case { a: 1, b: 2 }
in b: ..3 => first
:match
end
=> :match
3.0.0> first
=> 2
8. Pattern Matching with regular expressions
website = 'womanonrails.com'
case website
in /\w*\.com/ => favorite_website
end
3.0.0> favorite_website
=> "womanonrails.com"
9. Pattern Matching in blocks
We can use in
one-line pattern matching in blocks like any?
, 'all?
, select
or find
.
users = [{ name: "Woman on Rails", age: 22 }, { name: "Alex", age: 23 }]
users.any? { |user| user in { name: /C/, age: 20.. } }
=> false
users.any? { |user| user in { name: /A/, age: 20.. } }
=> true
10. Pattern Matching pin operator (^) with expression
Since Ruby 3.1, expressions and non-local variables are allowed in pin operator ^
. In the previous Ruby version, only constants, literals, and pinned local variables were allowed.
So, for example, in Ruby 3.0, there was a possibility to use range with numbers:
3.0.0> { version: 12 } in { version: 10..15 }
=> true
but not with more complex objects:
3.0.0> { version: 12 } in { version: (BigDecimal('10')..BigDecimal('15')) }
Traceback (most recent call last):
3: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `<main>'
2: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/bin/irb:23:in `load'
1: from /home/agnieszka/.rvm/rubies/ruby-3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):19: syntax error, unexpected .., expecting ')')
...n {version: (BigDecimal('10')..BigDecimal('15'))}
... ^~
(irb):19: syntax error, unexpected ')', expecting end-of-input
...ecimal('10')..BigDecimal('15'))}
... ^
Now in Ruby 3.1, we can do those calculations using pin operator ^
:
3.1.0> require 'bigdecimal'
=> true
3.1.0> { version: 12 } in { version: ^(BigDecimal('10')..BigDecimal('15')) }
=> true
It’s now valid for any complex expression. Remember that parenthesis are mandatory. You cannot do:
3.1.0> Time.now.year in ^rand(2021..2023)
/home/agnieszka/.rvm/rubies/ruby-3.1.0/lib/ruby/3.1.0/irb/workspace.rb:119:in `eval': (irb):16: rand: no such local variable (SyntaxError)
(irb):16: syntax error, unexpected '(', expecting end-of-input
Time.now.year in ^rand(2021..2023)
^
from /home/agnieszka/.rvm/rubies/ruby-3.1.0/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
from /home/agnieszka/.rvm/rubies/ruby-3.1.0/bin/irb:25:in `load'
from /home/agnieszka/.rvm/rubies/ruby-3.1.0/bin/irb:25:in `<main>'
instead, you need to do:
3.1.0 :017 > Time.now.year in ^(rand(2021..2023))
=> false
That’s all that I have today for you. Do you know more pattern matching tricks and tips? Share them in the comment below.
Bibliography
- Patter Matching in Ruby
- Ruby lang docs - news for Ruby 2.7.0
- Ruby lang docs - news for Pattern Matching
- Ruby introduces find pattern in pattern matching
Need help?
If you're looking for a Ruby developer with over a decade of experience, don't hesitate to contact me.
I have experience in a variety of domains, with a focus on short user feedback loops and teamwork. I can help you build a great product.