pattern_matching - Documentation for Ruby 4.0 (original) (raw)

Pattern matching

Pattern matching is a feature allowing deep matching of structured values: checking the structure and binding the matched parts to local variables.

Pattern matching in Ruby is implemented with the case/in expression:

case in ... in ... in ... else ... end

(Note that in and when branches can NOT be mixed in one case expression.)

Or with the => operator and the in operator, which can be used in a standalone expression:

=>

in

The case/in expression is exhaustive: if the value of the expression does not match any branch of the case expression (and the else branch is absent), NoMatchingPatternError is raised.

Therefore, the case expression might be used for conditional matching and unpacking:

config = {db: {user: 'admin', password: 'abc123'}}

case config in db: {user:} puts "Connect with user '#{user}'" in connection: {username: } puts "Connect with user '#{username}'" else puts "Unrecognized structure of config" end

whilst the => operator is most useful when the expected data structure is known beforehand, to just unpack parts of it:

config = {db: {user: 'admin', password: 'abc123'}}

config => {db: {user:}}

puts "Connect with user '#{user}'"

<expression> in <pattern> is the same as case <expression>; in <pattern>; true; else false; end. You can use it when you only want to know if a pattern has been matched or not:

users = [{name: "Alice", age: 12}, {name: "Bob", age: 23}] users.any? {|user| user in {name: /B/, age: 20..} }

See below for more examples and explanations of the syntax.

Patterns

Patterns can be:

Any pattern can be nested inside array/find/hash patterns where <subpattern> is specified.

Array patterns and find patterns match arrays, or objects that respond to deconstruct (see below about the latter). Hash patterns match hashes, or objects that respond to deconstruct_keys (see below about the latter). Note that only symbol keys are supported for hash patterns.

An important difference between array and hash pattern behavior is that arrays match only a whole array:

case [1, 2, 3] in [Integer, Integer] "matched" else "not matched" end

while the hash matches even if there are other keys besides the specified part:

case {a: 1, b: 2, c: 3} in {a: Integer} "matched" else "not matched" end

{} is the only exclusion from this rule. It matches only if an empty hash is given:

case {a: 1, b: 2, c: 3} in {} "matched" else "not matched" end

case {} in {} "matched" else "not matched" end

There is also a way to specify there should be no other keys in the matched hash except those explicitly specified by the pattern, with **nil:

case {a: 1, b: 2} in {a: Integer, **nil} "matched a part" in {a: Integer, b: Integer, **nil} "matched a whole" else "not matched" end

Both array and hash patterns support “rest” specification:

case [1, 2, 3] in [Integer, *] "matched" else "not matched" end

case {a: 1, b: 2, c: 3} in {a: Integer, **} "matched" else "not matched" end

Parentheses around both kinds of patterns could 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

[1, 2] => a, b [1, 2] in a, b

{a: 1, b: 2, c: 3} => a: {a: 1, b: 2, c: 3} in a:

Find pattern is similar to array pattern but it can be used to check if the given object has any elements that match the pattern:

case ["a", 1, "b", "c", 2] in [*, String, String, *] "matched" else "not matched" end

Variable binding

Besides deep structural checks, one of the very important features of the pattern matching is the binding of the matched parts to local variables. 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):

case [1, 2] in Integer => a, Integer "matched: #{a}" else "not matched" end

case {a: 1, b: 2, c: 3} in a: Integer => m "matched: #{m}" else "not matched" end

If no additional check is required, for only binding some part of the data to a variable, a simpler form could be used:

case [1, 2] in a, Integer "matched: #{a}" else "not matched" end

case {a: 1, b: 2, c: 3} in a: m "matched: #{m}" else "not matched" end

For hash patterns, even a simpler form exists: key-only specification (without any sub-pattern) binds the local variable with the key’s name, too:

case {a: 1, b: 2, c: 3} in a: "matched: #{a}" else "not matched" end

Binding works for nested patterns as well:

case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]} in name:, friends: [{name: first_friend}, *] "matched: #{first_friend}" else "not matched" end

The “rest” part of a pattern also can be bound to a variable:

case [1, 2, 3] in a, *rest "matched: #{a}, #{rest}" else "not matched" end

case {a: 1, b: 2, c: 3} in a:, **rest "matched: #{a}, #{rest}" else "not matched" end

Binding to variables currently does NOT work for alternative patterns joined with |:

case {a: 1, b: 2} in {a: } | Array

^ SyntaxError (variable capture in alternative pattern)

"matched: #{a}" else "not matched" end

Variables that start with _ are the only exclusions from this rule:

case {a: 1, b: 2} in {a: _, b: foo} | Array "matched: #{}, #{_foo}" else "not matched" end

It is, though, not advised to reuse the bound value, as this pattern’s goal is to signify a discarded value.

Variable pinning

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

For this case, the pin operator ^ can be used, to tell Ruby “just use this value as part of the pattern”:

expectation = 18 case [1, 2] in ^expectation, *rest "matched. expectation was: #{expectation}" else "not matched. expectation was: #{expectation}" end

One important usage of variable pinning is specifying that the same value should occur in the pattern several times:

jane = {school: 'high', schools: [{id: 1, level: 'middle'}, {id: 2, level: 'high'}]} john = {school: 'high', schools: [{id: 1, level: 'middle'}]}

case jane in school:, schools: [*, {id:, level: ^school}] "matched. school: #{id}" else "not matched" end

case john in school:, schools: [*, {id:, level: ^school}] "matched. school: #{id}" else "not matched" end

In addition to pinning local variables, you can also pin instance, global, and class variables:

$gvar = 1 class A @ivar = 2 @@cvar = 3 case [1, 2, 3] in ^$gvar, ^@ivar, ^@@cvar "matched" else "not matched" end

end

You can also pin the result of arbitrary expressions using parentheses:

a = 1 b = 2 case 3 in ^(a + b) "matched" else "not matched" end

Matching non-primitive objects: deconstruct and deconstruct_keys

As already mentioned above, array, find, and hash patterns besides literal arrays and hashes will try to match any object implementing deconstruct (for array/find patterns) or deconstruct_keys (for hash patterns).

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
"matched: #{px}" else "not matched" end

"matched: 1"

case Point.new(1, -2) in x: 0.. => px "matched: #{px}" else "not matched" end

keys are passed to deconstruct_keys to provide a room for optimization in the matched class: if calculating a full hash representation is expensive, one may calculate only the necessary subhash. When the **rest pattern is used, nil is passed as a keys value:

case Point.new(1, -2) in x: 0.. => px, **rest "matched: #{px}" else "not matched" end

Additionally, when matching custom classes, the expected class can be specified as part of the pattern and is checked with ===

class SuperPoint < Point end

case Point.new(1, -2) in SuperPoint(x: 0.. => px) "matched: #{px}" else "not matched" end

case SuperPoint.new(1, -2) in SuperPoint[x: 0.. => px] "matched: #{px}" else "not matched" end

These core and library classes implement deconstruction:

Guard clauses

if can be used to attach an additional condition (guard clause) when the pattern matches in case/in expressions. This condition may use bound variables:

case [1, 2] in a, b if b == a*2 "matched" else "not matched" end

case [1, 1] in a, b if b == a*2 "matched" else "not matched" end

unless works, too:

case [1, 1] in a, b unless b == a*2 "matched" else "not matched" end

Note that => and in operator can not have a guard clause. The following examples is parsed as a standalone expression with modifier if.

[1, 2] in a, b if b == a*2

Appendix A. Pattern syntax

Approximate syntax is:

pattern: value_pattern | variable_pattern | alternative_pattern | as_pattern | array_pattern | find_pattern | hash_pattern

value_pattern: literal | Constant | ^local_variable | ^instance_variable | ^class_variable | ^global_variable | ^(expression)

variable_pattern: variable

alternative_pattern: pattern | pattern | ...

as_pattern: pattern => variable

array_pattern: [pattern, ..., *variable] | Constant(pattern, ..., *variable) | Constant[pattern, ..., *variable]

find_pattern: [*variable, pattern, ..., *variable] | Constant(*variable, pattern, ..., *variable) | Constant[*variable, pattern, ..., *variable]

hash_pattern: {key: pattern, key:, ..., **variable} | Constant(key: pattern, key:, ..., **variable) | Constant[key: pattern, key:, ..., **variable]

Appendix B. Some undefined behavior examples

To leave room for optimization in the future, the specification contains some undefined behavior.

Use of a variable in an unmatched pattern:

case [0, 1] in [a, 2] "not matched" in b "matched" in c "not matched" end a c

Number of deconstruct, deconstruct_keys method calls:

$i = 0 ary = [0] def ary.deconstruct $i += 1 self end case ary in [0, 1] "not matched" in [0] "matched" end $i