Overview
Ruby's pattern matching feature matches values against patterns to extract data and control program flow. The case
/in
syntax replaces complex conditional logic with declarative patterns that describe data structure and content expectations.
Pattern matching operates through the case
/in
construct and standalone in
operator. Ruby evaluates patterns from top to bottom, executing the first matching pattern's associated code. Patterns can match literal values, capture variables, destructure arrays and hashes, and validate object attributes.
case [1, 2, 3]
in [a, b, c]
puts "#{a}, #{b}, #{c}" # => "1, 2, 3"
end
The deconstruct
and deconstruct_keys
methods define how custom objects participate in pattern matching. Classes implementing these methods can be matched against array and hash patterns respectively.
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def deconstruct
[x, y]
end
end
case Point.new(1, 2)
in [x, y]
puts "Point at #{x}, #{y}" # => "Point at 1, 2"
end
Pattern matching integrates with Ruby's existing data types. Arrays, hashes, ranges, and regular expressions all support pattern matching without additional configuration. The feature raises NoMatchingPatternError
when no pattern matches unless an else
clause exists.
Ruby's implementation supports guard clauses through if
expressions, pinned variables using the pin operator (^
), and rest patterns for capturing remaining elements. These features create flexible matching conditions while maintaining readable code structure.
Basic Usage
Array patterns match sequential data by position. Patterns can capture specific elements, ignore positions with wildcards, or collect remaining elements using splat operators.
case [1, 2, 3, 4, 5]
in [first, *middle, last]
puts "First: #{first}, Middle: #{middle}, Last: #{last}"
# => "First: 1, Middle: [2, 3, 4], Last: 5"
end
case ['apple', 'banana', 'cherry']
in [fruit, *]
puts "First fruit: #{fruit}" # => "First fruit: apple"
end
Hash patterns match key-value pairs and support partial matching by default. Patterns can capture values, match specific keys, or combine both approaches for flexible data extraction.
user = { name: "Alice", age: 30, city: "Boston", role: "admin" }
case user
in { name:, age: } if age >= 18
puts "Adult user: #{name}" # => "Adult user: Alice"
end
case user
in { role: "admin", name: admin_name }
puts "Admin: #{admin_name}" # => "Admin: Alice"
end
Variable patterns capture matched values into local variables. Variable names must be lowercase or underscore-prefixed to avoid constant matching. The pin operator (^
) matches against existing variable values rather than capturing.
expected = 42
case 42
in ^expected
puts "Matched expected value" # => "Matched expected value"
end
case { status: "success", data: info }
in { status: "success", data: }
puts "Success: #{data}" # => "Success: info"
end
Guard clauses add conditional logic to patterns through if
and unless
expressions. Guards evaluate after pattern matching succeeds, allowing validation of captured values or external conditions.
case { temperature: 75, humidity: 60 }
in { temperature: t, humidity: h } if t > 70 && h < 70
puts "Comfortable weather" # => "Comfortable weather"
end
numbers = [1, 2, 3, 4, 5]
case numbers
in [first, *rest] if rest.sum > first * 2
puts "Rest sum dominates: #{rest.sum} > #{first * 2}"
# => "Rest sum dominates: 14 > 2"
end
Advanced Usage
Nested patterns match complex data structures by combining array, hash, and object patterns. Deep nesting requires careful attention to pattern structure and variable scope.
data = {
users: [
{ id: 1, profile: { name: "Alice", preferences: { theme: "dark" } } },
{ id: 2, profile: { name: "Bob", preferences: { theme: "light" } } }
],
metadata: { version: 2, created: "2024-01-01" }
}
case data
in {
users: [
{ id: user_id, profile: { name:, preferences: { theme: } } },
*
],
metadata: { version: v }
} if v >= 2
puts "User #{user_id}: #{name} prefers #{theme} theme"
# => "User 1: Alice prefers dark theme"
end
Custom object patterns through deconstruct_keys
enable domain-specific matching. Methods should return hash-like objects with symbol keys matching expected pattern attributes.
class Request
attr_reader :method, :path, :headers, :body
def initialize(method, path, headers = {}, body = nil)
@method, @path, @headers, @body = method, path, headers, body
end
def deconstruct_keys(keys)
{ method: @method, path: @path, headers: @headers, body: @body }
end
end
request = Request.new("POST", "/api/users",
{ "Content-Type" => "application/json" },
'{"name": "Charlie"}')
case request
in Request[method: "POST", path: /^\/api\//, headers: { "Content-Type" => ct }]
puts "API POST request with #{ct}"
# => "API POST request with application/json"
end
Pattern composition creates reusable matching logic through method extraction and pattern variables. Complex patterns benefit from decomposition into smaller, testable components.
def api_request_pattern(path_prefix)
->(req) {
case req
in { method: "GET" | "POST", path: /^#{Regexp.escape(path_prefix)}/ }
true
else
false
end
}
end
def authenticated_pattern
->(req) {
case req
in { headers: { "Authorization" => /^Bearer / } }
true
else
false
end
}
end
request = { method: "POST", path: "/api/users", headers: { "Authorization" => "Bearer token123" } }
case request
in req if api_request_pattern("/api").call(req) && authenticated_pattern.call(req)
puts "Authenticated API request" # => "Authenticated API request"
end
Alternative patterns using the |
operator match multiple possibilities within single pattern branches. Alternatives must bind the same variables to maintain consistent scope.
case { event: "user_login", user_id: 123, timestamp: Time.now }
in { event: "user_login" | "user_signup", user_id:, timestamp: }
puts "User activity for ID #{user_id} at #{timestamp}"
end
case [1, "two", 3.0]
in [Integer => a, String => b, Float => c] | [String => a, Integer => b, Symbol => c]
puts "Mixed types: #{a.class}, #{b.class}, #{c.class}"
# => "Mixed types: Integer, String, Float"
end
Common Pitfalls
Variable binding in patterns creates new local variables that shadow existing variables with the same names. This behavior differs from variable assignment and can cause unexpected overwrites.
name = "Original"
data = { user: "Alice" }
case data
in { user: name } # Creates new local variable, shadows existing
puts name # => "Alice"
end
puts name # => "Alice" (original value lost)
# Use pin operator to match existing variable
name = "Original"
case data
in { user: ^name } # Matches against existing variable
puts "Matched original name"
else
puts "Different name: #{data[:user]}" # => "Different name: Alice"
end
Partial hash matching can produce unexpected matches when patterns don't account for additional keys. Hash patterns match subsets by default, potentially causing overly permissive matching.
user = { name: "Alice", age: 30, role: "admin", active: false }
case user
in { role: "admin" }
puts "Admin access granted" # Matches despite active: false
end
# Add specific exclusion patterns
case user
in { role: "admin", active: false }
puts "Inactive admin - access denied" # => "Inactive admin - access denied"
in { role: "admin" }
puts "Active admin - access granted"
end
Pattern matching precedence follows declaration order, not specificity. Broad patterns placed before specific patterns prevent specific matches from executing.
value = { type: "special", data: [1, 2, 3] }
case value
in { type: String } # Broad pattern matches first
puts "Generic type handler" # => "Generic type handler"
in { type: "special", data: Array } # Never reached
puts "Special type handler"
end
# Reorder patterns for correct precedence
case value
in { type: "special", data: Array }
puts "Special type handler" # => "Special type handler"
in { type: String }
puts "Generic type handler"
end
Rest pattern behavior in arrays captures remaining elements but behaves differently in various contexts. Multiple rest patterns in single patterns cause syntax errors.
# Valid rest patterns
case [1, 2, 3, 4, 5]
in [first, *middle, last]
puts "Middle: #{middle}" # => "Middle: [2, 3, 4]"
end
case [1]
in [first, *rest]
puts "Rest: #{rest}" # => "Rest: []" (empty array, not nil)
end
# Invalid: multiple rest patterns
# case [1, 2, 3, 4, 5]
# in [*start, middle, *end] # SyntaxError
# end
NoMatchingPatternError occurs when no patterns match and no else clause exists. This exception type requires explicit handling in production code unlike other control flow constructs.
def process_event(event)
case event
in { type: "click", element: }
handle_click(element)
in { type: "keypress", key: }
handle_keypress(key)
# Missing else clause
end
rescue NoMatchingPatternError
logger.warn("Unhandled event type: #{event}")
default_handler(event)
end
# Better approach with explicit else
def process_event_safe(event)
case event
in { type: "click", element: }
handle_click(element)
in { type: "keypress", key: }
handle_keypress(key)
else
logger.warn("Unhandled event type: #{event}")
default_handler(event)
end
end
Reference
Pattern Matching Operators
Operator | Syntax | Purpose | Example |
---|---|---|---|
case /in |
case value in pattern |
Multi-pattern matching | case [1,2] in [a,b] |
in |
value in pattern |
Single pattern test | [1,2] in [a,b] |
=> |
pattern => variable |
Pattern with capture | [1,2] => [a,b] |
^ |
^variable |
Pin operator | in ^expected_value |
* |
*rest |
Splat/rest pattern | in [first, *rest] |
** |
**rest |
Double splat | in { a:, **rest } |
` | ` | `pattern1 | pattern2` |
Array Pattern Syntax
Pattern | Matches | Variables | Example |
---|---|---|---|
[a, b, c] |
Exact length, all elements | a , b , c |
[1, 2, 3] |
[a, *rest] |
At least one element | a , rest |
[1, 2, 3, 4] |
[*start, a] |
At least one element | start , a |
[1, 2, 3, 4] |
[a, *middle, b] |
At least two elements | a , middle , b |
[1, 2, 3, 4] |
[] |
Empty array | None | [] |
[a, *] |
At least one element | a |
[1, 2, 3] |
Hash Pattern Syntax
Pattern | Matches | Variables | Example |
---|---|---|---|
{ a:, b: } |
Keys present | a , b |
{ a: 1, b: 2, c: 3 } |
{ a: value } |
Key with specific value | value |
{ a: "test" } |
{ a: String } |
Key with type match | None | { a: "text" } |
{ **rest } |
Capture remaining | rest |
{ a: 1, b: 2 } |
{} |
Empty hash | None | {} |
{ a:, **nil } |
Exact key match only | a |
{ a: 1 } (not { a: 1, b: 2 } ) |
Guard Clause Syntax
Syntax | Purpose | Example |
---|---|---|
if condition |
Pattern succeeds if condition true | in [a, b] if a > b |
unless condition |
Pattern succeeds if condition false | in { status: } unless status.nil? |
if var.method |
Call methods on captured variables | in [*items] if items.any? |
Common Pattern Examples
# Literal matching
case value
in 42
in "hello"
in true
end
# Type matching
case value
in String
in Integer
in Array
end
# Variable capture
case [1, 2, 3]
in [a, b, c] # a=1, b=2, c=3
end
# Mixed literal and capture
case { name: "Alice", age: 30 }
in { name: "Alice", age: } # age=30
end
# Nested structures
case { users: [{ id: 1, name: "Alice" }] }
in { users: [{ id:, name: }] } # id=1, name="Alice"
end
# Alternative patterns
case value
in String | Symbol
in Integer | Float
end
# Regular expressions
case "hello world"
in /^hello/
end
# Ranges
case 5
in 1..10
end
Error Types
Error | Cause | Prevention |
---|---|---|
NoMatchingPatternError |
No pattern matches, no else clause | Add else clause or rescue |
SyntaxError |
Invalid pattern syntax | Check pattern structure |
NameError |
Reference to undefined variable in pattern | Use existing variables or pin operator |
Custom Object Methods
Method | Returns | Purpose | Required For |
---|---|---|---|
deconstruct |
Array | Array pattern matching | in [a, b] syntax |
deconstruct_keys(keys) |
Hash | Hash pattern matching | in { a:, b: } syntax |
Performance Characteristics
Operation | Time Complexity | Notes |
---|---|---|
Literal matching | O(1) | Direct comparison |
Array pattern | O(n) | Linear in array size |
Hash pattern | O(k) | Linear in pattern key count |
Nested patterns | O(depth × width) | Recursive structure traversal |
Guard evaluation | Variable | Depends on guard condition |