Was experimental in Ruby 2.7. But is stable since 3.0. Some feature where also added and stable in later versions.
from Ruby doc:
Pattern matching is a feature allowing deep matching of structured values by checking the structure and binding the matched parts to local variables.
In other words: data validation and extraction.
They are 4 different patterns you can use
- Value
- Array
- Hash
- Find (Stable in 3.2)
Note: Any pattern can be nested inside array/find/hash pattern
And two kind of expressions
case
/in
case <expression>
in <pattern1>
...
in <pattern2>
...
in <pattern3>
...
else
...
end
Note:
Note that in
and when
branches can NOT be mixed in one case
expression
standalone expression with =>
or in
operators
<expression> => <pattern>
<expression> in <pattern>
👮♀️
case
/in
and =>
expression are exhaustive.
NoMatchingPatternError
is raised if no match
Note:
Unless there is an else
branch for case
/in
.
case 1
in Integer
"it's an int"
in String
"it's a string"
else
"something else"
end
#=> "it's an int"
It behaves like case
/when
and use the ===
operator
It is meant to be mixed with other patterns
Integer === 1 #=> true
1 === Integer #=> false
1 === 1 #=> true
"hello there!" === "hello world!" #=> false, like `==`
(1..10) === 5 #=> true
/^Hello/ === "Hello there!" #=> true
case [1, 2]
in [Integer, Integer]
"matched"
else
"not matched"
end
#=> "matched
pattern matches the whole Array
case { a: 1, b: 2, c: 3 }
in { a: Integer }
"matched"
else
"not matched"
end
#=> "matched"
matches even if there are other keys
(except for {}
)
case { name: "Riri", age: 14 }
in { name: String, age: 18.. }
"and adult"
else
"not an adult"
end
#=> "not an adult"
Syntaxic sugar FTW 🍭
parenthesis can be omitted
case [1, 2]
in Integer, Integer
"matched"
else
"not matched"
end
case { a: 1, b: 2, c: 3 }
in a: Integer
"matched"
else
"not matched"
end
similar to array pattern but it can be used to check if the given object has any elements that match the pattern
experimental since 3.0 ; stable in 3.2
contacts = [
{ name: "Riri" },
{ name: "Fifi" },
{ name: "Feel Good Inc.", company: true },
{ name: "Loulou" }
]
case contacts
in [*, { company: true }, *]
"at least one company"
else
"no company at all"
end
#=> "at least one company"
Both array and hash patterns support rest operator
case [1, 2, 3]
in [Integer, *]
"matched"
else
"not matched"
end
#=> "matched"
case { a: 1, b: 2, c: 3 }
in { a: Integer, ** } # same with or without
"matched"
else
"not matched"
end
#=> "matched"
if there should be no other keys
case { a: 1, b: 2, c: 3 }
in { a: 1, **nil }
"matched"
else
"not matched"
end
#=> not matched
case { a: 1, b: 2, c: 3 }
in { a: 1, **nil } | { b: 2, c: 3 }
"matched"
else
"not matched"
end
#=> matched
Note: Simply said: combining patterns
Note: Besides checks, a very important features of pattern matching is binding of matched parts to local variables.
Basic form
<pattern> => <local variable>
case [1, 2]
in Integer => int, Integer
"matched: #{int}"
else
"not matched"
end
#=> "matched: 1"
case { a: 1, b: 2, c: 3 }
in a: Integer => int
"matched: #{int}"
else
"not matched"
end
#=> "matched: 1"
Note: The basic form of binding is just specifying => variable_name after the matched (sub)pattern (one might find this similar to storing exceptions in local variables in a rescue ExceptionClass => var clause):
More syntaxic sugar ! 🍬
case [1, 2]
in int, Integer
"matched: #{int}"
else
"not matched"
end
#=> "matched: 1"
case { a: 1, b: 2, c: 3 }
in a: int
"matched: #{int}"
else
"not matched"
end
#=> "matched: 1"
Note:
case [1, 2, 3]
in int, *rest
"matched: #{int}, rest: #{rest}"
else
"not matched"
end
#=> "matched: 1, rest: [2, 3]"
case { a: 1, b: 2, c: 3 }
in a: int, **rest
"matched: #{int}, rest: #{rest}"
else
"not matched"
end
#=> "matched: 1, rest: { :b => 2, :c => 3 }"
Note: Same behavior but much shorter syntax for simpler use case
<expression> in <pattern>
# true or false
<expression> => <pattern>
# nil or raise NoMatchingPatternError
in
as condition
if current_user in { admin: true }
"user is admin"
else
"lambda user"
end
=>
as guard clause
def do_action
current_user => { admin: true }
rescue NoMatchingPatternError
redirect_to root_path, alert: "nope!"
end
Note:
I also see as a way to explicitly write expected "hash schema",
and avoiding unclear undefined method [] for NilClass
Checking and extracting (inspired from real example)
def extract_full_name_parts(extracted_line)
extracted_line => { full_name: String => full_name }
full_name.match(regexp) => { last_name:, first_name: }
# stuff with `last_name` and `full_name` local variables
rescue NoMatchingPatternError
extracted_line.error!("full_name is missing or incomplete")
end
Note: Due to the variable binding feature, existing local variable can not be straightforwardly used as a sub-pattern:
expectation = 18
case [1, 2]
in expectation, *rest
"matched. expectation was: #{expectation}"
else
"not matched. expectation was: #{expectation}"
end
# expected: "not matched. expectation was: 18"
# real: "matched. expectation was: 1" -- local variable just rewritten
expectation = 18
case [1, 2]
in ^expectation, *rest
"matched. expectation was: #{expectation}"
else
"not matched. expectation was: #{expectation}"
end
#=> "not matched. expectation was: 18"
case find_registered_contact(contact_attributes)
in Contact => contact
{ _other_attrs:, contact_id: contact.id }
in ContactToCreate[String => tmp_contact_id, _line]
# contact does not exist yet and needs to be created
{ _other_attrs:, tmp_contact_id: }
end
pinning extracted variable within pattern
case { riri: "Duck", fifi: "Duck", loulou: "Duck" }
in { riri: last_name, fifi: ^last_name, loulou: ^last_name }
"They are all brothers, from #{last_name} family"
else
"They are not from the same family"
end
#=> "They are all brothers, from Duck family"
if/unless 🤯
extract_company_name = false
contacts_data = [
{ name: "Feel Good Inc.", company: true },
{ name: "Riri", last_name: "Duck" },
{ name: "Fifi", last_name: "Duck" },
{ name: "Loulou", last_name: "Duck" },
{ name: "Charlie" }
]
case contacts_data
in [*, { company: true, name: }, *] if extract_company_name
"company: #{name}"
in [*, { name:, **nil }, *] if name == "Charlie"
"I found Charlie!"
in [*, { company: true }, *]
"at least one company present"
else
"no company found"
end
#=> "I found Charlie!"
class Point
def initialize(x, y)
@x, @y = x, y
end
def deconstruct
puts "deconstruct called"
[@x, @y]
end
def deconstruct_keys(keys)
puts "deconstruct_keys called with #{keys.inspect}"
{ x: @x, y: @y }
end
end
case Point.new(1, -2)
in px, Integer # sub-patterns and variable binding works
"matched: #{px}"
else
"not matched"
end
# deconstruct called
#=> "matched: 1"
case Point.new(1, -2)
in x: 0.. => px
"matched: #{px}"
else
"not matched"
end
# deconstruct_keys called with [:x]
#=> "matched: 1"