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 |