Overview
Pattern matching in Ruby operates through the case/in syntax introduced in Ruby 2.7, providing structural matching against data patterns rather than simple equality comparisons. The feature integrates with Ruby's existing case statement but adds pattern-based matching that can destructure arrays, hashes, objects, and primitive values.
Ruby implements pattern matching through several core mechanisms. The case/in construct serves as the primary interface, while objects can participate in pattern matching by implementing #deconstruct for positional patterns and #deconstruct_keys for keyword patterns. The pattern matching system evaluates patterns sequentially and binds matched values to local variables within the scope of each in clause.
# Basic pattern matching structure
case [1, 2, 3]
in [a, b, c]
puts "Matched: #{a}, #{b}, #{c}"
end
# => Matched: 1, 2, 3
The pattern matching system supports multiple pattern types including value patterns that match exact values, variable patterns that bind to any value, array patterns that destructure sequences, hash patterns that match key-value structures, and object patterns that use custom deconstruction methods.
# Multiple pattern types in one case
data = { name: "John", age: 30, city: "NYC" }
case data
in { name: String => n, age: (18..65) => a }
puts "Adult: #{n}, age #{a}"
in { name: String => n, age: (0..17) => a }
puts "Minor: #{n}, age #{a}"
end
# => Adult: John, age 30
Ruby's pattern matching integrates with existing language features including guards, alternative patterns, and variable pinning. The system maintains lexical scoping rules where variables bound in patterns become available within their respective in clause but not outside the case statement.
Basic Usage
The fundamental pattern matching syntax uses case followed by an expression and one or more in clauses that define patterns to match against. Each pattern can bind variables, check values, or combine multiple conditions.
Value patterns match exact values using the same equality semantics as regular case statements. These patterns work with any object that responds to #=== including classes, regular expressions, and ranges.
# Value patterns
case "hello"
in "hello"
puts "Exact match"
in /^h/
puts "Regex match"
end
# => Exact match
case 42
in Integer
puts "Integer type"
in (40..50)
puts "Range match"
end
# => Integer type
Variable patterns bind the matched value to a local variable. Ruby creates these variables within the scope of the matching in clause. Underscore variables (_) act as wildcards that match any value without binding.
# Variable binding
case [1, 2, 3]
in [first, _, last]
puts "First: #{first}, Last: #{last}"
end
# => First: 1, Last: 3
Array patterns destructure array-like objects that respond to #deconstruct. The pattern can specify exact length, use splat operators for variable length, or combine both approaches.
# Array pattern variations
data = [1, 2, 3, 4, 5]
case data
in [a, b]
puts "Exactly two: #{a}, #{b}"
in [first, *middle, last]
puts "First: #{first}, Middle: #{middle}, Last: #{last}"
in [head, *tail]
puts "Head: #{head}, Tail: #{tail}"
end
# => First: 1, Middle: [2, 3, 4], Last: 5
Hash patterns match hash-like objects that respond to #deconstruct_keys. These patterns can specify required keys, optional keys, or use double splat to capture remaining keys.
# Hash patterns
person = { name: "Alice", age: 25, city: "Boston", country: "USA" }
case person
in { name: n, age: a, **rest }
puts "Person: #{n}, #{a} years old"
puts "Other info: #{rest}"
end
# => Person: Alice, 25 years old
# => Other info: {:city=>"Boston", :country=>"USA"}
Multiple patterns within a single in clause use the | operator for alternative matching. The first matching pattern binds its variables, and subsequent alternatives in the same clause do not execute.
# Alternative patterns
case [1, 2]
in [a, b] | [a, b, c]
puts "Two or three elements: #{a}, #{b}"
end
# => Two or three elements: 1, 2
Advanced Usage
Advanced pattern matching combines multiple pattern types, uses guards for conditional logic, implements variable pinning for exact matches, and integrates with custom objects through deconstruction protocols.
Guard clauses add conditional logic to patterns using the if keyword. Ruby evaluates guards after pattern matching succeeds, and the pattern only matches when both the pattern and guard condition are true.
# Guard clauses
data = [10, 20, 30]
case data
in [a, b, c] if a + b > c
puts "Sum of first two exceeds third: #{a} + #{b} > #{c}"
in [a, b, c] if a < b && b < c
puts "Ascending sequence: #{a} < #{b} < #{c}"
end
# => Ascending sequence: 10 < 20 < 30
Variable pinning uses the ^ operator to match against existing variable values rather than binding new variables. This technique checks that the matched value equals the pinned variable's current value.
# Variable pinning
expected_status = "success"
response = { status: "success", data: [1, 2, 3] }
case response
in { status: ^expected_status, data: results }
puts "Expected status matched, data: #{results}"
in { status: status, data: results }
puts "Different status: #{status}, data: #{results}"
end
# => Expected status matched, data: [1, 2, 3]
Nested patterns combine multiple pattern types within complex data structures. Ruby evaluates nested patterns recursively, binding variables at each level of the structure.
# Complex nested patterns
complex_data = {
users: [
{ name: "John", profile: { age: 30, skills: ["ruby", "python"] } },
{ name: "Jane", profile: { age: 25, skills: ["javascript", "go"] } }
],
metadata: { version: "1.0", created_at: "2024-01-01" }
}
case complex_data
in {
users: [
{ name: first_name, profile: { age: (25..35) => age, skills: [*langs] } },
*other_users
],
metadata: { version: version }
}
puts "First user: #{first_name}, age #{age}"
puts "Languages: #{langs}"
puts "Version: #{version}"
puts "Other users: #{other_users.size}"
end
# => First user: John, age 30
# => Languages: ["ruby", "python"]
# => Version: 1.0
# => Other users: 1
Custom objects participate in pattern matching by implementing deconstruction methods. The #deconstruct method returns an array for positional patterns, while #deconstruct_keys accepts a keys array and returns a hash for keyword patterns.
# Custom deconstruction
class Point
attr_reader :x, :y, :z
def initialize(x, y, z = 0)
@x, @y, @z = x, y, z
end
def deconstruct
[x, y, z]
end
def deconstruct_keys(keys)
{ x: x, y: y, z: z }.slice(*keys)
end
end
point = Point.new(10, 20, 5)
case point
in Point[x, y, 0]
puts "2D point: (#{x}, #{y})"
in Point[x, y, z] if z > 0
puts "3D point: (#{x}, #{y}, #{z})"
end
# => 3D point: (10, 20, 5)
case point
in Point(x: px, y: py) if px > py
puts "X dominates: #{px} > #{py}"
end
# => X dominates: 10 > 20 (this won't match due to condition)
Pattern matching integrates with Ruby's assignment operators for one-liner destructuring without full case statements. The => operator assigns matched patterns directly to variables.
# One-liner pattern assignment
data = { name: "Bob", scores: [85, 90, 88] }
data => { name: person_name, scores: [first_score, *other_scores] }
puts "#{person_name} first score: #{first_score}"
puts "Other scores: #{other_scores}"
# => Bob first score: 85
# => Other scores: [90, 88]
Common Pitfalls
Pattern matching contains several behaviors that frequently cause confusion, particularly around variable scoping, pattern evaluation order, and performance characteristics. Understanding these edge cases prevents subtle bugs in production code.
Variable binding in pattern matching creates new local variables within each in clause, but these variables do not persist outside the case statement. This scoping behavior differs from variable assignment in other Ruby constructs and can lead to NameError when accessing pattern variables outside their scope.
# Variable scoping gotcha
data = [1, 2, 3]
case data
in [a, b, c]
puts "Inside pattern: #{a}, #{b}, #{c}"
end
# This raises NameError
begin
puts "Outside pattern: #{a}"
rescue NameError => e
puts "Error: #{e.message}"
end
# => Inside pattern: 1, 2, 3
# => Error: undefined local variable or method `a' for main:Object
Pattern evaluation occurs in the order patterns appear, and Ruby stops at the first successful match. Placing general patterns before specific patterns can mask more precise matching logic, leading to unexpected behavior.
# Pattern order importance
value = { type: "user", role: "admin", name: "Alice" }
# Incorrect order - general pattern matches first
case value
in { type: "user", **rest }
puts "General user pattern matched: #{rest}"
in { type: "user", role: "admin", name: name }
puts "Admin user: #{name}" # This never executes
end
# => General user pattern matched: {:role=>"admin", :name=>"Alice"}
# Correct order - specific patterns first
case value
in { type: "user", role: "admin", name: name }
puts "Admin user: #{name}"
in { type: "user", **rest }
puts "General user pattern matched: #{rest}"
end
# => Admin user: Alice
Hash pattern matching can behave unexpectedly when keys are missing or have nil values. Ruby distinguishes between missing keys and keys with nil values, which affects pattern matching behavior.
# Nil vs missing key behavior
data_with_nil = { name: "John", age: nil }
data_missing_key = { name: "John" }
# Both patterns match differently
case data_with_nil
in { name: n, age: a }
puts "With nil - Name: #{n}, Age: #{a.inspect}"
end
case data_missing_key
in { name: n, age: a }
puts "Missing key - Name: #{n}, Age: #{a.inspect}"
rescue NoMatchingPatternError => e
puts "Pattern failed: #{e.message}"
end
# => With nil - Name: John, Age: nil
# => Pattern failed: {name: "John"} (NoMatchingPatternError)
Performance degradation can occur with complex nested patterns or frequent pattern matching operations. Ruby evaluates each pattern sequentially and performs structural comparisons, which can become expensive with deep nesting or large data structures.
# Performance consideration with deep nesting
large_nested = {
level1: {
level2: {
level3: {
level4: { data: (1..1000).to_a }
}
}
}
}
# This pattern requires deep structural traversal
case large_nested
in { level1: { level2: { level3: { level4: { data: [first, *rest] } } } } }
puts "Deep match successful, first: #{first}, count: #{rest.size}"
end
# => Deep match successful, first: 1, count: 999
Alternative patterns using | can create variable binding conflicts when different alternatives bind different variable names. Ruby requires consistent variable names across all alternatives in the same in clause.
# Variable binding conflicts in alternatives
data = [1, 2]
# This causes a syntax error
begin
eval <<~RUBY
case data
in [a, b] | [x, y, z]
puts "Alternative matched"
end
RUBY
rescue SyntaxError => e
puts "Syntax error: #{e.message}"
end
# => Syntax error: illegal variable name in alternative pattern
# Correct approach with consistent variables
case data
in [a, b] | [a, b, c]
puts "Variables consistent: #{a}, #{b}"
puts "Third element: #{c}" if defined?(c)
end
# => Variables consistent: 1, 2
Pattern exhaustiveness warnings do not exist in Ruby's pattern matching implementation. Non-matching patterns raise NoMatchingPatternError unless an else clause handles unmatched cases, which can cause runtime failures in production.
# Missing exhaustive coverage
def process_shape(shape)
case shape
in { type: "circle", radius: r }
"Circle with radius #{r}"
in { type: "rectangle", width: w, height: h }
"Rectangle #{w}x#{h}"
# Missing triangle case
end
end
begin
result = process_shape({ type: "triangle", base: 10, height: 5 })
rescue NoMatchingPatternError => e
puts "Unhandled case: #{e.message}"
end
# => Unhandled case: {:type=>"triangle", :base=>10, :height=>5} (NoMatchingPatternError)
Reference
Pattern Matching Syntax
| Construct | Syntax | Description |
|---|---|---|
case/in statement |
case expr; in pattern; end |
Primary pattern matching construct |
| Pattern assignment | expr => pattern |
Direct assignment with pattern matching |
| Value pattern | in 42, in "string" |
Matches exact values using === |
| Variable pattern | in var |
Binds matched value to variable |
| Wildcard pattern | in _ |
Matches any value without binding |
| Array pattern | in [a, b, c] |
Destructures arrays via #deconstruct |
| Hash pattern | in { key: var } |
Destructures hashes via #deconstruct_keys |
| Alternative pattern | in pattern1 | pattern2 |
Matches first successful alternative |
| Guard clause | in pattern if condition |
Adds conditional logic to patterns |
| Variable pinning | in ^var |
Matches against existing variable value |
Array Pattern Operators
| Pattern | Example | Description |
|---|---|---|
| Fixed length | [a, b, c] |
Matches exactly three elements |
| Splat operator | [first, *rest] |
Captures remaining elements |
| Middle splat | [first, *middle, last] |
Captures middle elements |
| Empty splat | [*] |
Matches any array length |
| Nested arrays | [[a, b], [c, d]] |
Matches nested array structures |
Hash Pattern Operators
| Pattern | Example | Description |
|---|---|---|
| Specific keys | { name: n, age: a } |
Matches specific keys |
| Double splat | { key: val, **rest } |
Captures remaining keys |
| Nested hashes | { user: { name: n } } |
Matches nested hash structures |
| Key existence | { key: } |
Checks key exists (Ruby 3.1+) |
Deconstruction Methods
| Method | Parameters | Returns | Purpose |
|---|---|---|---|
#deconstruct |
None | Array |
Provides positional pattern data |
#deconstruct_keys |
Array<Symbol> or nil |
Hash |
Provides keyword pattern data |
Exception Classes
| Exception | Raised When | Recovery |
|---|---|---|
NoMatchingPatternError |
No pattern matches and no else clause |
Add else clause or additional patterns |
SyntaxError |
Invalid pattern syntax | Fix pattern syntax |
NameError |
Accessing pattern variables outside scope | Move variable access inside pattern scope |
Performance Characteristics
| Pattern Type | Time Complexity | Notes |
|---|---|---|
| Value patterns | O(1) | Uses === operator |
| Variable patterns | O(1) | Direct assignment |
| Array patterns | O(n) | Iterates through elements |
| Hash patterns | O(k) | Depends on number of keys |
| Nested patterns | O(depth × elements) | Recursive evaluation |
| Guards | O(pattern + condition) | Evaluates after pattern match |
Method Integration
| Context | Method | Behavior |
|---|---|---|
| Custom objects | #deconstruct |
Returns array for in Object[...] patterns |
| Custom objects | #deconstruct_keys(keys) |
Returns hash for in Object(key: val) patterns |
| Arrays | Built-in | Uses array indexing for patterns |
| Hashes | Built-in | Uses hash key lookup for patterns |
| Ranges | #=== |
Provides inclusion testing for value patterns |
| Classes | #=== |
Provides type checking for value patterns |