Overview
Pattern matching in Ruby allows you to match values against patterns and extract data from complex structures in a single operation. Introduced as experimental in Ruby 2.7 and stabilized in Ruby 3.0, it brings functional programming concepts to Ruby's object-oriented paradigm.
The core syntax uses the case
/in
construct, similar to traditional case
/when
statements but with structural matching capabilities:
case value
in pattern
# matched
else
# no match
end
Pattern matching works with arrays, hashes, objects, and primitive values, supporting deep structural matching and variable binding:
data = { name: "Alice", scores: [95, 87, 92] }
case data
in { name: String => name, scores: [first, *rest] }
puts "#{name}'s first score: #{first}, others: #{rest}"
end
# Output: Alice's first score: 95, others: [87, 92]
The feature integrates with Ruby's existing type system and supports custom pattern matching through the deconstruct
and deconstruct_keys
methods.
Basic Usage
Literal Value Matching
Pattern matching supports direct value comparison, similar to traditional case statements:
case status
in 200
"Success"
in 404
"Not Found"
in 500..599
"Server Error"
end
Variable Binding
Use identifiers to capture matched values:
case response
in { status: code, body: content }
puts "Status: #{code}, Content: #{content}"
end
Variables bound in patterns are available in the matched branch and follow Ruby's scoping rules.
Array Patterns
Arrays support positional matching with splat operators:
case numbers
in []
"Empty array"
in [first]
"Single element: #{first}"
in [first, second]
"Two elements: #{first}, #{second}"
in [first, *rest]
"First: #{first}, Rest: #{rest}"
in [*init, last]
"All but last: #{init}, Last: #{last}"
end
Array patterns match exact length unless using splat operators. The splat can appear anywhere in the pattern:
case data
in [*prefix, middle, *suffix] if prefix.length == suffix.length
puts "Symmetric array with center: #{middle}"
end
Hash Patterns
Hash patterns match key-value pairs and support partial matching:
case user
in { name: String, age: Integer => user_age } if user_age >= 18
"Adult user"
in { name: String, age: Integer }
"Minor user"
in { name: String }
"User with no age"
end
Hash patterns are "open" by default - they match hashes containing additional keys:
user = { name: "Bob", age: 25, email: "bob@example.com" }
case user
in { name: name, age: age }
puts "#{name} is #{age} years old"
# Matches even though email key exists
end
Use **nil
to require exact hash matching:
case config
in { host: host, port: port, **nil }
# Matches only if hash contains exactly host and port keys
end
Advanced Usage
Nested Patterns
Patterns can be arbitrarily nested to match complex data structures:
case api_response
in {
status: "success",
data: {
users: [{ name: String, posts: [*, { title: title }, *] }, *]
}
}
puts "Found post titled: #{title}"
end
This pattern matches responses where status is "success", data contains a users array, and the first user has posts with at least one containing a title.
Guard Clauses
Add conditions to patterns using if
or unless
:
case coordinates
in { x: x, y: y } if x > 0 && y > 0
"First quadrant"
in { x: x, y: y } if x.zero? || y.zero?
"On an axis"
in { x: x, y: y }
"Other quadrants"
end
Guard clauses have access to bound variables and are evaluated after pattern matching succeeds.
Alternative Patterns
Use |
to specify multiple acceptable patterns:
case value
in String | Symbol => str
puts str.to_s.upcase
in Integer | Float => num
puts num.abs
end
Alternative patterns must bind the same variables:
case data
in { type: "user", name: name } | { type: "admin", username: name }
puts "Hello, #{name}"
end
Pin Operator
Use ^
to match against existing variable values instead of binding:
expected_status = 200
case response
in { status: ^expected_status, body: body }
puts "Got expected status with body: #{body}"
end
Without the pin operator, expected_status
would be rebound to the matched value.
Object Pattern Matching
Implement pattern matching for custom classes:
class Person
attr_reader :name, :age, :email
def initialize(name, age, email)
@name, @age, @email = name, age, email
end
# For array-style pattern matching: Person(name, age)
def deconstruct
[name, age]
end
# For hash-style pattern matching: Person[name:, age:]
def deconstruct_keys(keys)
case keys
when [:name] then { name: name }
when [:age] then { age: age }
when [:name, :age] then { name: name, age: age }
when nil then { name: name, age: age, email: email }
else {}
end
end
end
person = Person.new("Charlie", 30, "charlie@example.com")
case person
in Person(name, age) if age >= 18
puts "Adult: #{name}"
end
case person
in Person[name: name, age: age] if age >= 65
puts "Senior: #{name}"
in Person[name: name]
puts "Person: #{name}"
end
The deconstruct_keys
method receives the keys requested by the pattern or nil
for all available keys.
Error Handling & Debugging
NoMatchingPatternError
Pattern matching raises NoMatchingPatternError
if no patterns match:
begin
case value
in Integer
"number"
in String
"text"
end
rescue NoMatchingPatternError => e
puts "No pattern matched: #{e.message}"
end
Always include an else
clause or ensure patterns are exhaustive:
case value
in Integer
"number"
in String
"text"
else
"other type"
end
Pattern Matching with Rescue
Handle exceptions within pattern matching:
case data
in { url: url }
begin
fetch_data(url)
rescue Net::TimeoutError
"Request timed out"
end
else
"Invalid data format"
end
Debugging Complex Patterns
Use verbose patterns for debugging complex matches:
case complex_data
in {
metadata: { version: version },
content: [first_item, *rest]
} => matched_data
puts "Matched version: #{version}"
puts "First item: #{first_item}"
puts "Remaining items: #{rest.length}"
puts "Full match: #{matched_data}"
end
The => variable
syntax captures the entire matched value for inspection.
Validation Patterns
Create patterns specifically for data validation:
def validate_user_data(data)
case data
in {
name: String => name,
email: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
age: Integer => age
} if name.length > 0 && age.between?(13, 120)
{ valid: true, user: data }
else
{ valid: false, errors: ["Invalid user data format"] }
end
end
Performance & Memory
Pattern Complexity Impact
Simple patterns perform better than complex nested patterns:
# Faster - direct hash access
case data
in { status: 200 }
handle_success
end
# Slower - deep nested matching
case data
in {
response: {
metadata: { status: 200, headers: { content_type: /json/ } }
}
}
handle_success
end
Pattern Ordering Strategy
Order patterns from most specific to least specific for optimal performance:
case value
in { type: "premium", features: [*, "advanced", *] } # Most specific first
"Premium with advanced features"
in { type: "premium" } # Less specific
"Premium user"
in { type: String } # Least specific
"Basic user"
end
Memory Considerations
Pattern matching creates temporary objects during matching. For high-frequency operations, consider traditional conditionals:
# Memory-efficient for simple cases
if hash[:status] == 200 && hash[:data]
process_success(hash[:data])
end
# Pattern matching alternative
case hash
in { status: 200, data: data }
process_success(data)
end
Benchmarking Pattern Performance
Consider pattern complexity when processing large datasets:
require 'benchmark'
data = Array.new(10000) { { status: rand(100..599), data: "content" } }
Benchmark.bm do |x|
x.report("if/else") do
data.each { |item| item[:status] == 200 ? "success" : "error" }
end
x.report("pattern") do
data.each do |item|
case item
in { status: 200 }
"success"
else
"error"
end
end
end
end
Production Patterns
API Response Processing
Pattern matching excels at handling varied API responses with complex structures:
class APIClient
def handle_response(response)
case JSON.parse(response.body, symbolize_names: true)
in { success: true, data: data }
Result.success(data)
in { success: false, error: { code: code, message: message } }
Result.error("API Error #{code}: #{message}")
in { errors: [{ field: field, message: message }, *] }
Result.validation_error(field, message)
in { status: status } if (400..499).include?(status)
Result.client_error("Client error: #{status}")
in { status: status } if (500..599).include?(status)
Result.server_error("Server error: #{status}")
else
Result.error("Unknown response format")
end
end
end
Configuration Processing
Handle complex configuration scenarios with nested validation:
class ConfigProcessor
def process(config)
case config
in {
database: {
adapter: "postgresql",
host: String => host,
port: Integer => port,
credentials: { username: String, password: String }
},
cache: { type: "redis", url: String => cache_url },
**rest
}
setup_postgres_with_redis(host, port, cache_url, rest)
in {
database: { adapter: "sqlite", file: String => file },
**rest
}
setup_sqlite(file, rest)
in { database: { adapter: adapter } }
raise ConfigError, "Unsupported database adapter: #{adapter}"
in { database: nil }
raise ConfigError, "Database configuration required"
else
raise ConfigError, "Invalid configuration format"
end
end
private
def setup_postgres_with_redis(host, port, cache_url, options)
# Implementation with additional options handling
end
end
Event Processing Systems
Pattern matching works exceptionally well for event-driven architectures:
class EventProcessor
def handle(event)
case event
in {
type: "user.created",
data: { id: String => user_id, email: String => email },
metadata: { source: source }
}
send_welcome_email(user_id, email)
track_user_source(user_id, source)
in {
type: "payment.succeeded",
data: { amount: Integer => amount, currency: "USD" },
metadata: { user_id: user_id }
} if amount > 10000
flag_large_payment(user_id, amount)
in {
type: /\Aadmin\./,
data: data,
metadata: { admin_id: admin_id }
}
AdminEventHandler.handle(event, admin_id)
in { type: type, **rest }
logger.warn "Unhandled event type: #{type}"
logger.debug "Event data: #{rest}"
else
logger.error "Invalid event format: #{event}"
end
end
end
Data Transformation Pipelines
Use pattern matching for complex data transformation workflows:
class DataTransformer
def transform(records)
records.map do |record|
case record
in {
type: "customer",
data: {
personal: { name: name, age: Integer => age },
contact: { email: email }
}
} if age >= 18
build_adult_customer(name, age, email)
in {
type: "customer",
data: {
personal: { name: name, age: Integer => age },
guardian: { name: guardian_name, email: email }
}
} if age < 18
build_minor_customer(name, age, guardian_name, email)
in { type: "business", data: { company: company, **business_data } }
build_business_customer(company, business_data)
else
raise TransformationError, "Unknown record format: #{record}"
end
end
end
end
Common Pitfalls
Variable Scoping Issues
Variables bound in patterns can shadow existing variables:
name = "Alice"
case data
in { name: name } # This rebinds the local variable
puts name # Uses the matched value, not "Alice"
end
puts name # Now contains the matched value
Use the pin operator to avoid rebinding:
name = "Alice"
case data
in { name: ^name } # Matches against existing value
puts "Found Alice"
end
puts name # Still "Alice"
Hash Key Type Sensitivity
Hash patterns are sensitive to key types and will not match mismatched key types:
data = { "name" => "Bob", "age" => 25 }
case data
in { name: name } # Won't match - looking for symbol keys
puts name
end
case data
in { "name" => name } # Matches - correct key type
puts name
end
Convert keys consistently when working with external data:
case JSON.parse(json_string, symbolize_names: true)
in { name: name, age: age } # Now works with symbol keys
puts "#{name} is #{age} years old"
end
Array Pattern Matching Subtleties
Understand the difference between array patterns and splat usage:
case [[1, 2], [3, 4]]
in [first, second] # Matches two nested arrays
# first = [1, 2], second = [3, 4]
in [*elements] # Matches all elements as array
# elements = [[1, 2], [3, 4]]
in [first, *rest] # Matches first element, rest as array
# first = [1, 2], rest = [[3, 4]]
end
Pattern Exhaustiveness Problems
Pattern matching doesn't warn about non-exhaustive patterns, which can lead to runtime errors:
# Dangerous - might raise NoMatchingPatternError
def categorize(value)
case value
in String
"text"
in Integer if value > 0
"positive number"
# Missing: negative integers, floats, other types
end
end
# Safe version with exhaustive patterns
def categorize(value)
case value
in String
"text"
in Integer if value > 0
"positive number"
in Integer
"non-positive number"
in Numeric
"other numeric type"
else
"non-numeric type"
end
end
Guard Clause Gotchas
Guard clauses can access bound variables but may create unexpected behavior:
case data
in { count: count } if count > 0
puts "Positive count: #{count}"
in { count: count }
puts "Non-positive count: #{count}"
end
# The second pattern will only match if the first guard fails
# But `count` is still bound from the first pattern attempt
Reference
Pattern Matching Operators
Operator | Usage | Description |
---|---|---|
in |
case x in pattern |
Pattern matching case clause |
=> |
pattern => var |
Capture entire matched value |
^ |
^variable |
Pin operator - match against existing value |
| |
pattern1 | pattern2 |
Alternative patterns |
* |
[first, *rest] |
Array splat pattern |
** |
{a: 1, **rest} |
Hash splat pattern |
**nil |
{a: 1, **nil} |
Exact hash matching |
Pattern Types
Pattern Type | Syntax | Example |
---|---|---|
Literal | value |
42 , "hello" , :symbol |
Variable | identifier |
name , count |
Array | [pattern, ...] |
[first, second] , [*, last] |
Hash | {key: pattern, ...} |
{name: String, age: Integer} |
Object | Class(...) or Class[...] |
Date(y, m, d) , Person[name:] |
Guard | pattern if condition |
Integer => n if n > 0 |
Alternative | pattern | pattern |
String | Symbol |
Pin | ^variable |
^expected_value |
Custom Pattern Matching Methods
Method | Purpose | Return Value |
---|---|---|
deconstruct |
Array-style patterns Class(...) |
Array of values |
deconstruct_keys(keys) |
Hash-style patterns Class[...] |
Hash with requested keys |
Exception Types
Exception | Raised When |
---|---|
NoMatchingPatternError |
No pattern matches the value |
TypeError |
Object doesn't respond to deconstruct methods |
Built-in Classes Supporting Pattern Matching
Class | Array Pattern | Hash Pattern | Example |
---|---|---|---|
Date |
Date(year, month, day) |
No | Date(2023, 12, 25) |
Time |
Time(year, month, day, hour, min, sec) |
No | Time(2023, 12, 25, 10, 30, 0) |
MatchData |
Array of captures | No | MatchData[full_match, *captures] |
Performance Guidelines
Guideline | Reason | Example |
---|---|---|
Order patterns specific to general | Reduces unnecessary matching attempts | Most restrictive patterns first |
Avoid deep nesting in loops | Creates temporary objects | Use simple patterns in hot code paths |
Use guards judiciously | Evaluated after pattern matching | Only when pattern logic insufficient |
Pin variables for value matching | Avoids variable rebinding | ^expected_value vs expected_value |
Prefer traditional conditionals for simple checks | Lower overhead | if hash[:key] == value vs pattern matching |
Ruby Version Support
Ruby Version | Pattern Matching Support |
---|---|
2.7.0 | Experimental (warnings enabled) |
3.0.0+ | Stable, full support |
3.1.0+ | Improved performance optimizations |
3.2.0+ | Additional syntax improvements |