Overview
Pattern matching in Ruby uses the case
/in
syntax to match values against patterns, enabling destructuring and conditional logic based on data shape and content. Ruby's pattern matching system supports literal values, variable binding, array patterns, hash patterns, and object patterns.
The pattern matching mechanism evaluates each pattern sequentially until finding a match. When a pattern matches, Ruby executes the corresponding code block and binds any capture variables. If no patterns match, Ruby raises a NoMatchingPatternError
.
case [1, 2, 3]
in [x, y, z]
puts "#{x}, #{y}, #{z}"
end
# => "1, 2, 3"
Ruby implements pattern matching through the ===
operator and specialized matching protocols. Objects can customize their matching behavior by implementing deconstruct
for array patterns and deconstruct_keys
for hash patterns.
case {name: "Alice", age: 30}
in {name: String => n, age: Integer => a}
puts "Name: #{n}, Age: #{a}"
end
# => "Name: Alice, Age: 30"
Pattern matching works with built-in types including arrays, hashes, strings, numbers, and custom objects. The system distinguishes between value patterns that match specific values and variable patterns that capture matched values.
Basic Usage
Ruby's pattern matching syntax begins with case
followed by the expression to match. Each pattern branch uses in
instead of when
. The simplest patterns match literal values directly.
status_code = 404
case status_code
in 200
puts "Success"
in 404
puts "Not Found"
in 500
puts "Server Error"
else
puts "Other status"
end
# => "Not Found"
Variable patterns capture the matched value and bind it to a variable name. Ruby distinguishes between existing variables and new pattern variables based on whether the variable exists in the current scope.
x = 10
case [1, 2, 3]
in [a, b, c] # Creates new variables a, b, c
puts "#{a}, #{b}, #{c}"
end
case [1, 2, 3]
in [^x, y, z] # Pin operator ^ uses existing variable x
puts "First element is #{x}"
else
puts "First element is not #{x}"
end
# => "First element is not 10"
Array patterns destructure arrays and match individual elements. The splat operator *
captures remaining elements into an array. Patterns can specify exact lengths or use flexible matching.
case [1, 2, 3, 4, 5]
in [first, *middle, last]
puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end
# => "First: 1, Middle: [2, 3, 4], Last: 5"
case ["GET", "/users", {id: 123}]
in [method, path, params]
puts "#{method} request to #{path} with #{params}"
end
# => "GET request to /users with {:id=>123}"
Hash patterns match hash keys and capture values. Ruby matches only the specified keys, ignoring additional keys unless using the **nil
rest pattern to require exact matches.
request = {method: "POST", path: "/api/users", body: {name: "Bob"}}
case request
in {method: "POST", path: path, body: {name: name}}
puts "Creating user #{name} at #{path}"
end
# => "Creating user Bob at /api/users"
# Exact hash matching with **nil
case {a: 1, b: 2}
in {a: Integer, **nil}
puts "Hash has only key :a"
else
puts "Hash has additional keys"
end
# => "Hash has additional keys"
Guard clauses add conditional logic to patterns using if
or unless
. The guard evaluates after the pattern matches and can access bound variables.
case [1, 2, 3]
in [x, y, z] if x + y == z
puts "#{x} + #{y} = #{z}"
in [x, y, z] if x + z == y
puts "#{x} + #{z} = #{y}"
else
puts "No arithmetic relationship"
end
# => "No arithmetic relationship"
Advanced Usage
Alternative patterns match multiple possibilities using the pipe operator |
. Each alternative can bind the same variables, but the variables must have compatible types across alternatives.
case ["admin", {permissions: ["read", "write"]}]
in ["admin" | "superuser", {permissions: perms}]
puts "Administrative user with #{perms}"
in ["user", {permissions: perms}]
puts "Regular user with #{perms}"
end
# => "Administrative user with [\"read\", \"write\"]"
As patterns use =>
to capture the entire matched value or subpatterns while continuing to match structure. This enables accessing both the whole and parts of complex data.
response = {
status: 200,
data: {
users: [
{id: 1, name: "Alice"},
{id: 2, name: "Bob"}
]
}
}
case response
in {status: 200, data: {users: users_array} => data_section}
puts "Success: #{users_array.length} users"
puts "Full data: #{data_section}"
end
# => "Success: 2 users"
# => "Full data: {:users=>[{:id=>1, :name=>\"Alice\"}, {:id=>2, :name=>\"Bob\"}]}"
Custom objects participate in pattern matching by implementing deconstruct
for array-like matching and deconstruct_keys
for hash-like matching. These methods define how objects decompose during pattern matching.
class Point
def initialize(x, y)
@x, @y = x, y
end
def deconstruct
[@x, @y]
end
def deconstruct_keys(keys)
{x: @x, y: @y}
end
end
point = Point.new(3, 4)
case point
in Point[x, y] if x**2 + y**2 == 25
puts "Point on unit circle scaled by 5"
in Point(x:, y:)
puts "Point at (#{x}, #{y})"
end
# => "Point at (3, 4)"
Nested patterns match complex data structures by combining multiple pattern types. Ruby evaluates nested patterns depth-first, binding variables as patterns match.
api_response = {
metadata: {version: "2.1", timestamp: "2024-01-15"},
results: [
{type: "user", data: {id: 1, name: "Alice", active: true}},
{type: "user", data: {id: 2, name: "Bob", active: false}},
{type: "admin", data: {id: 3, name: "Charlie", role: "superuser"}}
]
}
case api_response
in {
metadata: {version: version},
results: [
*prefix,
{type: "admin", data: {name: admin_name, role: role}},
*suffix
]
}
puts "API v#{version}: Found admin #{admin_name} (#{role}) at position #{prefix.length}"
puts "#{prefix.length} users before, #{suffix.length} after"
end
# => "API v2.1: Found admin Charlie (superuser) at position 2"
# => "2 users before, 0 after"
Pattern matching integrates with case expressions and can return values. Each pattern branch returns the value of its last expression, enabling functional programming patterns.
def process_request(request)
case request
in {method: "GET", path: path, params: params}
"Fetching #{path} with #{params}"
in {method: "POST", path: path, body: body}
"Creating resource at #{path}: #{body}"
in {method: method, path: path}
"#{method} request to #{path}"
else
"Invalid request format"
end
end
result = process_request({method: "POST", path: "/users", body: {name: "Dave"}})
puts result
# => "Creating resource at /users: {:name=>\"Dave\"}"
Common Pitfalls
Variable scoping in pattern matching creates subtle bugs when pattern variables shadow existing variables. Ruby creates new local variables for unbound names in patterns, which can mask intended variable references.
user_id = 42
users = [{id: 1, name: "Alice"}, {id: 42, name: "Bob"}]
case users.first
in {id: user_id, name: name} # Creates new user_id variable!
puts "Found user #{name} with ID #{user_id}"
end
puts "Original user_id is still #{user_id}"
# => "Found user Alice with ID 1"
# => "Original user_id is still 42"
# Correct approach using pin operator
case users.first
in {id: ^user_id, name: name} # Uses existing user_id variable
puts "Found matching user #{name}"
else
puts "User ID does not match #{user_id}"
end
# => "User ID does not match 42"
Hash patterns match subset keys by default, which can lead to unexpected matches when expecting exact key sets. The **nil
rest pattern enforces exact matching but must appear last in the pattern.
config = {host: "localhost", port: 3000, debug: true, ssl: false}
case config
in {host: host, port: port} # Matches despite extra keys
puts "Basic config: #{host}:#{port}"
end
# => "Basic config: localhost:3000"
# Force exact matching
case config
in {host: host, port: port, **nil} # Fails due to extra keys
puts "Exact basic config"
else
puts "Config has additional keys beyond host and port"
end
# => "Config has additional keys beyond host and port"
Array length mismatches cause patterns to fail silently. Ruby requires exact length matches unless using splat patterns, but debugging failed matches requires careful attention to array lengths.
coordinates = [10, 20, 30, 40] # 4D coordinate
case coordinates
in [x, y] # Expects 2D
puts "2D: (#{x}, #{y})"
in [x, y, z] # Expects 3D
puts "3D: (#{x}, #{y}, #{z})"
in [x, y, z, w] # Expects 4D
puts "4D: (#{x}, #{y}, #{z}, #{w})"
else
puts "Unexpected coordinate format"
end
# => "4D: (10, 20, 30, 40)"
# Flexible matching with splat
case coordinates
in [x, y, *rest] if rest.empty?
puts "2D: (#{x}, #{y})"
in [x, y, z, *rest] if rest.empty?
puts "3D: (#{x}, #{y}, #{z})"
else
puts "Higher dimensional or unexpected format"
end
# => "Higher dimensional or unexpected format"
Guard clauses evaluate after pattern matching completes, so variables bound during matching are available in guards. However, guard failures cause the entire pattern to fail, not just the guard condition.
def categorize_number(n)
case n
in Integer => num if num > 0 && num.even?
"positive even: #{num}"
in Integer => num if num > 0 # This catches positive odd numbers
"positive odd: #{num}"
in Integer => num if num < 0
"negative: #{num}"
in 0
"zero"
else
"not an integer"
end
end
puts categorize_number(7) # => "positive odd: 7"
puts categorize_number(-3) # => "negative: -3"
puts categorize_number(0) # => "zero"
NoMatchingPatternError occurs when no patterns match and no else
clause exists. This error provides limited debugging information, making pattern debugging challenging without systematic testing.
def parse_response(response)
case response
in {status: 200, data: data}
{success: true, result: data}
in {status: 400..499, error: message}
{success: false, error: message}
in {status: 500..599, error: message}
{success: false, error: "Server error: #{message}"}
end
end
# This raises NoMatchingPatternError
begin
result = parse_response({status: 200}) # Missing data key
rescue NoMatchingPatternError => e
puts "Pattern matching failed: #{e.message}"
puts "Response format not recognized"
end
# => "Pattern matching failed: {:status=>200} (NoMatchingPatternError)"
# => "Response format not recognized"
Custom object pattern matching requires careful implementation of deconstruct
and deconstruct_keys
. Missing or incorrectly implemented methods cause pattern matching to fail silently.
class Rectangle
def initialize(width, height)
@width, @height = width, height
end
# Missing deconstruct methods - patterns will fail
end
rect = Rectangle.new(10, 20)
case rect
in Rectangle[w, h] # Fails silently - no deconstruct method
puts "Rectangle #{w}x#{h}"
else
puts "Could not match rectangle pattern"
end
# => "Could not match rectangle pattern"
# Add proper deconstruct methods
class Rectangle
def deconstruct
[@width, @height]
end
def deconstruct_keys(keys)
{width: @width, height: @height}
end
end
case rect
in Rectangle[w, h]
puts "Rectangle #{w}x#{h}"
end
# => "Rectangle 10x20"
Reference
Case/In Syntax
Pattern Type | Syntax | Description |
---|---|---|
Literal | in value |
Matches exact value using === |
Variable | in variable_name |
Captures matched value |
Pin | in ^existing_var |
Matches using existing variable value |
Alternative | in pattern1 | pattern2 |
Matches either pattern |
As | in pattern => variable |
Captures matched value while matching pattern |
Guard | in pattern if condition |
Adds conditional logic after pattern match |
Array Patterns
Pattern | Example | Description |
---|---|---|
Fixed length | in [a, b, c] |
Matches exactly 3 elements |
With splat | in [first, *rest] |
Captures remaining elements |
Nested | in [[x, y], z] |
Matches nested array structure |
Empty | in [] |
Matches empty array |
Single splat | in [*all] |
Captures entire array |
Hash Patterns
Pattern | Example | Description |
---|---|---|
Key matching | in {key: value} |
Matches specific key-value pairs |
Variable capture | in {key: variable} |
Captures value for key |
Rest pattern | in {key: val, **rest} |
Captures remaining keys |
Exact matching | in {key: val, **nil} |
Requires exact key set |
Nested | in {outer: {inner: val}} |
Matches nested hash structure |
Object Patterns
Method | Purpose | Return Type |
---|---|---|
deconstruct |
Array-like pattern matching | Array |
deconstruct_keys(keys) |
Hash-like pattern matching | Hash |
Pattern Matching Methods
Method | Parameters | Returns | Description |
---|---|---|---|
case expr |
Expression | Object |
Begins pattern matching expression |
in pattern |
Pattern | nil |
Defines match pattern |
else |
None | Object |
Default case when no patterns match |
Exceptions
Exception | Condition | Description |
---|---|---|
NoMatchingPatternError |
No pattern matches, no else clause | Pattern matching failed completely |
SyntaxError |
Invalid pattern syntax | Malformed pattern matching code |
Pattern Matching Protocol
Custom objects implement these methods to participate in pattern matching:
class CustomObject
def deconstruct
# Return array for array pattern matching
[@field1, @field2]
end
def deconstruct_keys(keys)
# Return hash for hash pattern matching
# keys parameter indicates which keys are needed
{key1: @field1, key2: @field2}
end
end
Operator Precedence in Patterns
Operator | Precedence | Associativity |
---|---|---|
^ (pin) |
Highest | Right |
=> (as) |
High | Left |
| (alternative) |
Medium | Left |
, (sequence) |
Low | Left |
Guard Clause Evaluation
Guard clauses evaluate after successful pattern matching:
- Pattern matches successfully
- Variables are bound
- Guard condition evaluates with bound variables
- If guard fails, pattern fails and next pattern attempts
- If guard succeeds, pattern branch executes