Ruby 3.1 introduces pattern matching pin operator against expression


Pattern matching was introduced in Ruby 2.7, a feature commonly found in functional programming languages like Scala. Unlike other programming languages, pattern matching in Ruby uses the case statement. Instead of using the when keyword, the keyword in is used.

Let us check the below example to see how pattern matching works in Ruby.

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

#=> "matched"

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

#=> "matched"

We have passed a variable data (a hash) and an expression [1, 2, 3] to the case statement. The data variable values and array indexes are matched with the pattern of every in clause. Once a pattern has matched the code within that in clause gets executed.

Besides structural checks, one of the features of pattern matching is the binding of the matched parts to local variables.

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

puts a
#=> 1

As seen in the above example, the local variable a was assigned the value 1 when an expression matches to a pattern.

Due to the variable binding feature, the existing local variable cannot be used straightforwardly as a sub-pattern. For the below example, the variable current_price should not have changed. But because of variable binding, the value got updated.

current_price = 15

case [1, 2]
in current_price, *rest
  "matched. current_price was: #{current_price}"
else
  "not matched. current_price was: #{current_price}"
end
# expected: "not matched. current_price was: 15"
# real: "matched. current_price was: 1" -- local variable updated

To avoid such issues, “variable pinning” operator (^) can be used, which will use the value for pattern matching.

current_price = 15

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

Before

The pin operator worked fine in the case of variables, constants, and literals but failed when an expression or range is passed.

Let’s take an example where we have the following JSON data:

data = { "name": "Alice",
         "age": 30,
         "children": [
            {
              "name": "Bob",
              "age": 6
            },
            {
              "name": "Jilly",
              "age": 4
            }
          ]
        }

We want to match all children of Alice in the range 2 to 4. Using pattern matching, if we try to pass the age parameter as a range it will fail with a syntax error.

case data
in name: "Alice", children: [*, { name: child_name, age: ^(1..3) }]
  "matched: #{child_name}"
else
  "not matched"
end

SyntaxError ((irb):61: syntax error, unexpected `end')

After

Ruby 3.1 introduces a pin operator against expressions to fix the above issue. It has been committed to the trunk and can be tested/played using Ruby 3.1.0-dev through RubyBuild.

We can now pass expressions and range using pin operator and, it should work fine without raising an error.

case "Do you like cats?"
in ^(/like/)
  puts "Match"
end

#=> Match

case data
in name: "Alice", children: [*, { name: child_name, age: ^(1..3) }]
  "matched: #{child_name}"
else
  "not matched"
end

#=> matched: Jilly

Note:

Pattern matching is still an experimental feature and, a warning is displayed when it is used.

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

#warning: Find pattern is experimental, and the behavior may change in future versions of Ruby!
#=> "matched"