Overview
Pattern matching operators in Ruby provide a declarative syntax for extracting values from complex data structures and matching against specific patterns. Ruby implements pattern matching through the case
/in
construct and the rightward assignment operator =>
, both introduced to support structural pattern matching.
The core functionality revolves around the in
operator for pattern matching within case statements and the =>
operator for rightward assignment pattern matching. Ruby's pattern matching system works with arrays, hashes, objects, and primitive values, supporting both exact matches and variable capture.
# Basic pattern matching with case/in
case [1, 2, 3]
in [a, b, c]
puts "#{a}, #{b}, #{c}" # => "1, 2, 3"
end
# Rightward assignment pattern matching
[1, 2] => [x, y]
puts "#{x}, #{y}" # => "1, 2"
Pattern matching operators support guard conditions, nested patterns, and alternative patterns. The system integrates with Ruby's existing type system and works seamlessly with custom classes that implement the deconstruct
and deconstruct_keys
methods.
# Hash pattern matching
user = { name: "Alice", age: 30, role: "admin" }
case user
in { name: String => n, role: "admin" }
puts "Admin: #{n}"
end
Basic Usage
Pattern matching operators handle array destructuring through position-based matching. The in
operator within case statements matches array elements by position, binding matched values to variables or matching against literal values.
numbers = [1, 2, 3, 4]
case numbers
in [first, *middle, last]
puts "First: #{first}, Last: #{last}, Middle: #{middle}"
# => "First: 1, Last: 4, Middle: [2, 3]"
end
# Exact value matching
case [1, 2, 3]
in [1, x, 3]
puts "Middle value: #{x}" # => "Middle value: 2"
in [1, 2, 4]
puts "No match"
else
puts "Pattern not found"
end
Hash pattern matching extracts key-value pairs using symbolic key notation. The system matches specified keys while ignoring additional keys unless explicitly restricted with **nil
.
person = { name: "Bob", age: 25, city: "NYC", country: "USA" }
case person
in { name: n, age: a }
puts "#{n} is #{a} years old"
# => "Bob is 25 years old"
end
# Restricting additional keys
case person
in { name: String, age: Integer, **nil }
puts "Exact match"
else
puts "Extra keys present" # This executes
end
The rightward assignment operator =>
provides inline pattern matching without case statements. This operator attempts pattern matching and raises NoMatchingPatternError
when patterns fail to match.
# Array rightward assignment
[10, 20, 30] => [a, b, c]
puts "#{a} #{b} #{c}" # => "10 20 30"
# Hash rightward assignment
{ x: 1, y: 2 } => { x: x_val, y: y_val }
puts "x=#{x_val}, y=#{y_val}" # => "x=1, y=2"
# Failed pattern matching raises NoMatchingPatternError
begin
[1, 2] => [a, b, c]
rescue NoMatchingPatternError => e
puts "Pattern failed: #{e.message}"
end
Guard conditions add conditional logic to pattern matching using if
clauses. Guards evaluate after successful pattern matching and must return truthy values for the pattern to succeed.
numbers = [5, 10, 15]
case numbers
in [a, b, c] if a + b == c
puts "Sum sequence: #{a} + #{b} = #{c}"
# => "Sum sequence: 5 + 10 = 15"
end
# Multiple guard conditions
case 42
in x if x > 0 && x % 2 == 0
puts "Positive even number: #{x}"
end
Advanced Usage
Nested pattern matching handles complex data structures by combining multiple pattern types within single expressions. The system recursively applies pattern matching to nested elements, supporting arbitrary nesting depth.
data = {
users: [
{ name: "Alice", contacts: [{ type: "email", value: "alice@test.com" }] },
{ name: "Bob", contacts: [{ type: "phone", value: "555-1234" }] }
],
meta: { version: 2 }
}
case data
in { users: [{ name: first_name, contacts: [{ type: "email", value: email }] }, *], meta: { version: v } }
puts "First user: #{first_name}, Email: #{email}, Version: #{v}"
# => "First user: Alice, Email: alice@test.com, Version: 2"
end
Alternative patterns using the pipe operator |
allow matching against multiple possible patterns within single branches. Ruby evaluates alternatives left-to-right, binding variables from the first successful match.
def process_response(response)
case response
in { status: "success", data: String => content } |
{ status: "ok", body: String => content }
puts "Content: #{content}"
in { status: "error" | "failure", message: msg }
puts "Error: #{msg}"
in { code: 200..299, response: data }
puts "HTTP success: #{data}"
else
puts "Unknown response format"
end
end
process_response({ status: "success", data: "Hello" })
# => "Content: Hello"
process_response({ status: "failure", message: "Not found" })
# => "Error: Not found"
Object pattern matching requires classes to implement deconstruct
for array-like matching or deconstruct_keys
for hash-like matching. These methods define how objects expose their internal structure for pattern matching.
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def deconstruct
[x, y]
end
def deconstruct_keys(keys)
{ x: x, y: y }
end
end
class Rectangle
attr_reader :top_left, :bottom_right
def initialize(top_left, bottom_right)
@top_left, @bottom_right = top_left, bottom_right
end
def deconstruct_keys(keys)
{ top_left: top_left, bottom_right: bottom_right }
end
end
point = Point.new(10, 20)
case point
in Point(x, y) if x > 0 && y > 0
puts "Positive quadrant: (#{x}, #{y})"
end
rectangle = Rectangle.new(Point.new(0, 0), Point.new(10, 10))
case rectangle
in Rectangle(top_left: Point(0, 0), bottom_right: Point(w, h))
puts "Rectangle from origin: #{w}x#{h}"
end
Variable pinning using the pin operator ^
matches against existing variable values rather than binding new variables. This prevents accidental variable shadowing and enables matching against computed values.
target = 42
values = [10, 42, 30]
case values
in [a, ^target, b]
puts "Found target #{target} between #{a} and #{b}"
# => "Found target 42 between 10 and 30"
end
# Pinning with expressions
threshold = 50
numbers = [60, 45, 70]
case numbers
in [a, b, c] if a > ^(threshold + 10) && c > ^threshold
puts "First #{a} and third #{c} exceed thresholds"
# => "First 60 and third 70 exceed thresholds"
end
Common Pitfalls
Variable scoping in pattern matching creates new local variables that persist outside the pattern matching construct. Variables bound within patterns remain accessible after the case statement completes, potentially overwriting existing variables with the same names.
x = "original"
data = [1, 2, 3]
case data
in [x, y, z]
puts "Inside: x=#{x}, y=#{y}, z=#{z}"
# => "Inside: x=1, y=2, z=3"
end
puts "Outside: x=#{x}" # => "Outside: x=1" (overwrote original!)
# Use different variable names or pin operator to avoid this
x = "original"
case data
in [a, b, c] # Different names
puts "Values: #{a}, #{b}, #{c}"
end
puts "Preserved: x=#{x}" # => "Preserved: x=original"
Hash pattern matching ignores unspecified keys by default, which can lead to unexpected matches when expecting exact structure validation. Use **nil
to explicitly reject additional keys.
config = { host: "localhost", port: 3000, debug: true, timeout: 30 }
# This matches despite extra keys
case config
in { host: h, port: p }
puts "Matched: #{h}:#{p}"
# => "Matched: localhost:3000"
end
# Strict matching rejects extra keys
case config
in { host: h, port: p, **nil }
puts "Exact match"
else
puts "Extra keys found" # This executes
end
Array length mismatches cause pattern failures even when using splat operators incorrectly. The splat operator captures remaining elements but cannot create elements that don't exist.
short_array = [1, 2]
case short_array
in [a, b, c]
puts "Three elements"
else
puts "Length mismatch" # This executes
end
# Splat handles variable lengths correctly
case short_array
in [a, *rest]
puts "First: #{a}, Rest: #{rest}"
# => "First: 1, Rest: [2]"
end
# But splat cannot match specific minimum lengths
case [1]
in [a, b, *rest]
puts "At least two elements"
else
puts "Too few elements" # This executes
end
Type pattern matching requires understanding Ruby's type system and class hierarchy. Patterns match against exact classes unless using inheritance-aware comparisons.
class Animal; end
class Dog < Animal; end
dog = Dog.new
case dog
in Animal
puts "Matched Animal" # This executes (inheritance works)
end
case dog
in Dog
puts "Matched Dog" # This also executes (exact class)
end
# String matching gotcha
case "123"
in String => s if s.match?(/^\d+$/)
puts "Numeric string: #{s}"
end
# But this fails
case 123
in String
puts "Not a string"
else
puts "Type mismatch" # This executes
end
NoMatchingPatternError exceptions occur with rightward assignment when patterns fail, but case statements fall through to else clauses. This behavioral difference can cause unexpected program termination.
data = [1, 2]
# Case statement handles failure gracefully
case data
in [a, b, c]
puts "Three elements"
else
puts "Pattern failed gracefully"
end
# Rightward assignment raises exception
begin
data => [x, y, z]
rescue NoMatchingPatternError => e
puts "Exception: #{e.message}"
# => "Exception: [1, 2]: [x, y, z]"
end
# Guard failures also cause exceptions with rightward assignment
begin
[5, 10] => [a, b] if a > b
rescue NoMatchingPatternError
puts "Guard condition failed"
end
Reference
Pattern Matching Operators
Operator | Context | Purpose | Example |
---|---|---|---|
in |
case statement | Pattern matching branch | case x; in [a, b]; end |
=> |
Rightward assignment | Inline pattern matching | [1, 2] => [a, b] |
^ |
Pattern | Pin variable value | in [^x, y] |
* |
Array pattern | Splat remaining elements | in [first, *rest] |
** |
Hash pattern | Splat remaining key-values | in {a: 1, **rest} |
**nil |
Hash pattern | Reject additional keys | in {a: 1, **nil} |
| |
Pattern | Alternative patterns | in String | Integer |
Array Pattern Syntax
Pattern | Matches | Binds |
---|---|---|
[a, b, c] |
Exactly 3 elements | a , b , c |
[first, *rest] |
1+ elements | first , rest (array) |
[*init, last] |
1+ elements | init (array), last |
[a, *middle, b] |
2+ elements | a , middle (array), b |
[] |
Empty array | None |
[*, x] |
Any length, capture last | x |
Hash Pattern Syntax
Pattern | Matches | Binds |
---|---|---|
{a: x, b: y} |
Hash with keys :a , :b (+ others) |
x , y |
{a: x, **rest} |
Hash with key :a (+ others) |
x , rest (hash) |
{a: x, **nil} |
Hash with exactly key :a |
x |
{} |
Empty hash or any hash | None |
{**nil} |
Exactly empty hash | None |
Guard Conditions
Syntax | Behavior |
---|---|
in pattern if condition |
Pattern matches AND condition true |
in pattern unless condition |
Pattern matches AND condition false |
case x; in a if a > 0; end |
Guard accesses bound variables |
[1, 2] => [a, b] if a < b |
Exception if guard fails |
Custom Class Integration
Method | Purpose | Return Type | Usage |
---|---|---|---|
deconstruct |
Array-like pattern matching | Array |
in ClassName(a, b) |
deconstruct_keys(keys) |
Hash-like pattern matching | Hash |
in ClassName(x: a) |
Exception Handling
Exception | Raised When | Context |
---|---|---|
NoMatchingPatternError |
Pattern match fails | Rightward assignment => |
NoMatchingPatternError |
Guard condition fails | Rightward assignment with guard |
Type Patterns
Pattern Type | Syntax | Example |
---|---|---|
Class match | ClassName |
in String |
Value match | literal |
in 42 |
Variable bind | variable |
in x |
Pin variable | ^variable |
in ^x |
Guard pattern | pattern if expr |
in x if x > 0 |
Evaluation Order
- Pattern structure matching (array length, hash keys, object type)
- Value binding or literal comparison
- Guard condition evaluation (if present)
- Variable assignment (on successful match)
- Exception raising (rightward assignment failures)