Pattern matching
Ruby 3.0 (December 2020) introduced pattern matching — case/in expressions that admit destructuring arrays, hashes, and objects in addition to value-based matching. The principal forms: array patterns ([a, b, c]), hash patterns ({ name:, age: }), find patterns ([*, x, *]), value patterns (1, :active, nil), type patterns (Integer), array-rest patterns ([head, *rest]), the pin operator (^var), and guards (if condition). The form admits substantial conciseness for substantial discrimination on data shape — particularly useful for parsing JSON, decomposing API responses, and dispatching on tagged unions. The classical case/when (treated in Conditionals) remains for value-based dispatch; the new case/in adds structural matching. Ruby 3.1+ admits the one-line form (expr => pattern) for inline assertions.
case/in
The principal form (note the in keyword instead of when):
case point
in [0, 0]
"origin"
in [x, 0]
"on x-axis at #{x}"
in [0, y]
"on y-axis at #{y}"
in [x, y]
"at (#{x}, #{y})"
end
Each in clause specifies a pattern; the first matching pattern’s body runs. Variables in the pattern are bound to the matched components.
The patterns are exhaustive by convention — case/in raises NoMatchingPatternError if no pattern matches:
case "hello"
in Integer
puts "an integer"
in Float
puts "a float"
end
# raises NoMatchingPatternError
For non-exhaustive matching, add an else:
case input
in Integer then "int"
in String then "str"
else "other"
end
Array patterns
Match arrays by structure:
case arr
in []
"empty"
in [single]
"one: #{single}"
in [a, b]
"pair: #{a}, #{b}"
in [first, *rest]
"first: #{first}; rest: #{rest.inspect}"
in [*init, last]
"init: #{init.inspect}; last: #{last}"
end
The *rest admits collecting remaining elements; conventional for variable-length matching.
The patterns admit literal values and type checks:
case command
in [:add, x, y]
x + y
in [:sub, x, y]
x - y
in [:mul, x, y]
x * y
end
case data
in [Integer => first, *_]
"first int: #{first}"
end
The Integer => first admits “match an Integer, bind to first” — type-check plus binding.
Hash patterns
Match hashes by key:
case user
in { name: }
"name: #{name}"
in { name:, age: }
"name: #{name}, age: #{age}"
in { type: :admin, name: }
"admin: #{name}"
end
The shorthand { name: } is equivalent to { name: name } — binds the value to a local of the same name.
For matching specific values:
case response
in { status: 200, body: }
process_body(body)
in { status: 404 }
not_found
in { status: 500..599, error: }
server_error(error)
end
A subtle point: hash patterns do not require all keys — extra keys are admitted:
case { name: "Alice", age: 30, email: "a@b.c" }
in { name: } # matches; ignores extra keys
puts name # "Alice"
end
For matching only specific keys (no extras), use the special { ** } pattern:
case { name: "Alice", age: 30 }
in { name:, **nil } # match only if no other keys
puts name
end
# raises NoMatchingPatternError because age is also present
Type patterns
Class names admit type-check matching:
case value
in Integer
"integer"
in Float
"float"
in String
"string"
in Array
"array"
in Hash
"hash"
in nil
"nil"
in true
"true"
in false
"false"
end
Combined with destructuring:
case data
in [Integer, *] # array starting with an integer
"int-led"
in [String, String] # exactly two strings
"two strings"
in { name: String, age: Integer }
"valid user"
end
Find patterns
The [*, target, *] admits “find target anywhere in the array”:
case [1, 2, 3, 4, 5]
in [*, 3, *]
"contains 3"
end
case ["log", "error", "Failed", "to connect"]
in [*, /^Fail/, *]
"contains a 'Fail' line"
end
Find patterns admit binding components:
case [1, 2, 3, 4, 5]
in [*pre, 3, *post]
"pre: #{pre.inspect}; post: #{post.inspect}"
end
# pre: [1, 2]; post: [4, 5]
The pin operator ^
Variables in patterns bind by default; to match against an existing variable’s value, the pin ^:
target = 42
case n
in 0 then "zero"
in ^target then "target match" # match if n == 42
in _ then "other"
end
Without ^, target would bind a new local variable (shadowing the outer); the ^ admits using the existing value.
The pin works with any expression (since Ruby 3.1):
case input
in ^(target.to_s) then "matched target as string"
end
Guards
A pattern may have an if (or unless) guard:
case n
in Integer => x if x.positive?
"positive int"
in Integer => x if x.negative?
"negative int"
in 0
"zero"
end
The guard is evaluated after the pattern matches; if false, the case is rejected and the next pattern is tried.
Wildcards and binding
The _ matches anything without binding:
case data
in [_, _, third] # ignore first two
third
end
case msg
in { type: _, payload: } # ignore type
process(payload)
end
For binding-with-pattern, =>:
case data
in [String => first, _, _]
first.upcase
in Hash => h if h.size > 5
"large hash"
end
The => name admits “match this pattern AND bind the matched value to name”.
Object patterns
For custom classes implementing deconstruct / deconstruct_keys:
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def deconstruct
[x, y] # for array patterns
end
def deconstruct_keys(keys)
{ x: x, y: y } # for hash patterns
end
end
p = Point.new(1, 2)
case p
in [a, b] # array pattern via deconstruct
"[#{a}, #{b}]"
in { x:, y: } # hash pattern via deconstruct_keys
"{x: #{x}, y: #{y}}"
end
The deconstruct is called for array patterns; deconstruct_keys for hash patterns. The mechanism admits substantial integration with custom types.
For Data (Ruby 3.2+) and Struct, the methods are auto-generated:
Point = Data.define(:x, :y)
p = Point.new(x: 1, y: 2)
case p
in { x:, y: }
puts "x: #{x}, y: #{y}"
end
Alternative patterns with |
The | admits “match any of these patterns”:
case input
in Integer | Float
"numeric"
in String | Symbol
"stringy"
in Array | Hash
"collection"
end
The principal restriction: alternative patterns may not bind variables (since the bindings would differ across alternatives).
One-line pattern matching
Ruby 3.0+ admits the one-line form expr in pattern:
data = { name: "Alice", age: 30 }
if data in { name:, age: Integer => age }
puts "Got #{name}, #{age}"
end
The form returns true if the pattern matches (and binds the variables); false otherwise.
For assertion-style matching (raises if no match), Ruby 3.0+ admits expr => pattern:
{ name: "Alice", age: 30 } => { name:, age: }
puts name # "Alice"
puts age # 30
[1, 2, 3] => [a, b, c]
puts a # 1
The => form raises NoMatchingPatternError if the pattern doesn’t match — conventional for “I expect this shape”.
Common patterns
Tagged union dispatch
def evaluate(expr)
case expr
in [:lit, n]
n
in [:add, a, b]
evaluate(a) + evaluate(b)
in [:mul, a, b]
evaluate(a) * evaluate(b)
in [:neg, x]
-evaluate(x)
end
end
evaluate([:add, [:lit, 2], [:mul, [:lit, 3], [:lit, 4]]])
# 14
The pattern admits substantial expressiveness for AST-style data.
JSON dispatch
case json_response
in { status: "success", data: { items: Array => items } }
process(items)
in { status: "error", code:, message: }
handle_error(code, message)
in { status: "redirect", location: }
follow(location)
end
The pattern admits substantial discrimination on API responses.
Decomposing structured data
case event
in { type: "click", x: Integer => x, y: Integer => y }
click(x, y)
in { type: "key", key: String => key }
press(key)
in { type: "resize", width:, height: }
resize(width, height)
end
Extracting from arrays
case parsed_csv
in [headers, *rows] if headers.all? { |h| h.is_a?(String) }
Table.new(headers, rows)
in [single_row]
Row.new(single_row)
in []
Empty.new
end
Validation with assertion-style matching
def parse_user(data)
data => { name: String => name, age: Integer => age, email: }
User.new(name: name, age: age, email: email)
rescue NoMatchingPatternError => e
raise InvalidUserError, "missing or invalid fields: #{e.message}"
end
The => form admits substantial conciseness for validation-and-extraction.
Recursive list processing
def sum_list(arr)
case arr
in []
0
in [head, *tail]
head + sum_list(tail)
end
end
sum_list([1, 2, 3, 4, 5]) # 15
The pattern admits Haskell-style list recursion.
State machine dispatch
case state
in { status: :idle }
start
in { status: :running, started_at: }
check_progress(started_at)
in { status: :done, result: }
finalise(result)
in { status: :failed, error: }
retry_or_abort(error)
end
Pinning for value-equality
expected_id = 42
case incoming
in { id: ^expected_id, payload: }
process(payload)
in { id: }
ignore(id)
end
Find pattern for substring detection
case file_contents
in [*, "ERROR", *] # contains ERROR line
alert
in [*, /WARNING/, *] # contains WARNING-pattern line
warn
end
Combined value and type matching
case input
in 0..10 => small
"small: #{small}"
in 11..100 => medium
"medium: #{medium}"
in Integer => large
"large: #{large}"
in String => s if s.match?(/^\d+$/)
"numeric string: #{s}"
end
Custom class with pattern matching
class HttpResponse
attr_reader :status, :body
def initialize(status, body)
@status = status
@body = body
end
def deconstruct
[status, body]
end
def deconstruct_keys(keys)
{ status: status, body: body }
end
end
response = HttpResponse.new(200, '{"ok": true}')
case response
in { status: 200..299, body: }
parse_success(body)
in { status: 400..499 }
client_error
in { status: 500..599 }
server_error
end
A note on case/when vs case/in
The two forms differ:
| Form | Use |
|---|---|
case/when | Value-based dispatch via === |
case/in | Structural pattern matching |
# case/when — uses ===
case n
when 1..10 then "small"
when Integer then "integer"
when /^\d+$/ then "digits"
end
# case/in — destructuring
case data
in [Integer => x, Integer => y]
"point at (#{x}, #{y})"
in { type:, value: }
"tagged: #{type}=#{value}"
end
The two coexist; when for the conventional value dispatch; in for structural matching with binding.
A note on the conventional discipline
The contemporary Ruby pattern-matching advice:
- Use
case/infor substantial structural dispatch. - Use
case/whenfor value-based dispatch. - Use the one-line
=>for inline destructuring with assertion. - Use the one-line
infor boolean shape testing. - Use array patterns (
[a, *rest, last]) for sequence destructuring. - Use hash patterns (
{ key: }) for keyword extraction. - Use the pin
^for value matching against existing variables. - Use guards (
if cond) for additional constraints. - Use
deconstructanddeconstruct_keysfor custom-class pattern matching. - Use
Data.define(Ruby 3.2+) for value objects with auto pattern matching. - Trust exhaustiveness —
NoMatchingPatternErroris conventional for unhandled cases.
The combination — case/in for structural dispatch, array/hash/value/type patterns, the pin operator for value matching, guards for additional constraints, the deconstruct protocol for custom classes, the one-line forms — is the substance of Ruby’s pattern matching. The mechanism admits substantial expressive power for parsing, dispatch, and validation; the conventional contemporary discipline reaches for it whenever the data is substantial enough that classical case/when would require elaborate if chains.