CrackedRuby logo

CrackedRuby

Pattern Matching Operators

Overview

Pattern matching operators in Ruby provide a declarative syntax for extracting values from complex data structures and matching against specific patterns. Ruby implements pattern matching through the case/in construct and the rightward assignment operator =>, both introduced to support structural pattern matching.

The core functionality revolves around the in operator for pattern matching within case statements and the => operator for rightward assignment pattern matching. Ruby's pattern matching system works with arrays, hashes, objects, and primitive values, supporting both exact matches and variable capture.

# Basic pattern matching with case/in
case [1, 2, 3]
in [a, b, c]
  puts "#{a}, #{b}, #{c}"  # => "1, 2, 3"
end

# Rightward assignment pattern matching
[1, 2] => [x, y]
puts "#{x}, #{y}"  # => "1, 2"

Pattern matching operators support guard conditions, nested patterns, and alternative patterns. The system integrates with Ruby's existing type system and works seamlessly with custom classes that implement the deconstruct and deconstruct_keys methods.

# Hash pattern matching
user = { name: "Alice", age: 30, role: "admin" }
case user
in { name: String => n, role: "admin" }
  puts "Admin: #{n}"
end

Basic Usage

Pattern matching operators handle array destructuring through position-based matching. The in operator within case statements matches array elements by position, binding matched values to variables or matching against literal values.

numbers = [1, 2, 3, 4]
case numbers
in [first, *middle, last]
  puts "First: #{first}, Last: #{last}, Middle: #{middle}"
  # => "First: 1, Last: 4, Middle: [2, 3]"
end

# Exact value matching
case [1, 2, 3]
in [1, x, 3]
  puts "Middle value: #{x}"  # => "Middle value: 2"
in [1, 2, 4]
  puts "No match"
else
  puts "Pattern not found"
end

Hash pattern matching extracts key-value pairs using symbolic key notation. The system matches specified keys while ignoring additional keys unless explicitly restricted with **nil.

person = { name: "Bob", age: 25, city: "NYC", country: "USA" }
case person
in { name: n, age: a }
  puts "#{n} is #{a} years old"
  # => "Bob is 25 years old"
end

# Restricting additional keys
case person
in { name: String, age: Integer, **nil }
  puts "Exact match"
else
  puts "Extra keys present"  # This executes
end

The rightward assignment operator => provides inline pattern matching without case statements. This operator attempts pattern matching and raises NoMatchingPatternError when patterns fail to match.

# Array rightward assignment
[10, 20, 30] => [a, b, c]
puts "#{a} #{b} #{c}"  # => "10 20 30"

# Hash rightward assignment
{ x: 1, y: 2 } => { x: x_val, y: y_val }
puts "x=#{x_val}, y=#{y_val}"  # => "x=1, y=2"

# Failed pattern matching raises NoMatchingPatternError
begin
  [1, 2] => [a, b, c]
rescue NoMatchingPatternError => e
  puts "Pattern failed: #{e.message}"
end

Guard conditions add conditional logic to pattern matching using if clauses. Guards evaluate after successful pattern matching and must return truthy values for the pattern to succeed.

numbers = [5, 10, 15]
case numbers
in [a, b, c] if a + b == c
  puts "Sum sequence: #{a} + #{b} = #{c}"
  # => "Sum sequence: 5 + 10 = 15"
end

# Multiple guard conditions
case 42
in x if x > 0 && x % 2 == 0
  puts "Positive even number: #{x}"
end

Advanced Usage

Nested pattern matching handles complex data structures by combining multiple pattern types within single expressions. The system recursively applies pattern matching to nested elements, supporting arbitrary nesting depth.

data = {
  users: [
    { name: "Alice", contacts: [{ type: "email", value: "alice@test.com" }] },
    { name: "Bob", contacts: [{ type: "phone", value: "555-1234" }] }
  ],
  meta: { version: 2 }
}

case data
in { users: [{ name: first_name, contacts: [{ type: "email", value: email }] }, *], meta: { version: v } }
  puts "First user: #{first_name}, Email: #{email}, Version: #{v}"
  # => "First user: Alice, Email: alice@test.com, Version: 2"
end

Alternative patterns using the pipe operator | allow matching against multiple possible patterns within single branches. Ruby evaluates alternatives left-to-right, binding variables from the first successful match.

def process_response(response)
  case response
  in { status: "success", data: String => content } |
     { status: "ok", body: String => content }
    puts "Content: #{content}"
  in { status: "error" | "failure", message: msg }
    puts "Error: #{msg}"
  in { code: 200..299, response: data }
    puts "HTTP success: #{data}"
  else
    puts "Unknown response format"
  end
end

process_response({ status: "success", data: "Hello" })
# => "Content: Hello"

process_response({ status: "failure", message: "Not found" })
# => "Error: Not found"

Object pattern matching requires classes to implement deconstruct for array-like matching or deconstruct_keys for hash-like matching. These methods define how objects expose their internal structure for pattern matching.

class Point
  attr_reader :x, :y
  
  def initialize(x, y)
    @x, @y = x, y
  end
  
  def deconstruct
    [x, y]
  end
  
  def deconstruct_keys(keys)
    { x: x, y: y }
  end
end

class Rectangle
  attr_reader :top_left, :bottom_right
  
  def initialize(top_left, bottom_right)
    @top_left, @bottom_right = top_left, bottom_right
  end
  
  def deconstruct_keys(keys)
    { top_left: top_left, bottom_right: bottom_right }
  end
end

point = Point.new(10, 20)
case point
in Point(x, y) if x > 0 && y > 0
  puts "Positive quadrant: (#{x}, #{y})"
end

rectangle = Rectangle.new(Point.new(0, 0), Point.new(10, 10))
case rectangle
in Rectangle(top_left: Point(0, 0), bottom_right: Point(w, h))
  puts "Rectangle from origin: #{w}x#{h}"
end

Variable pinning using the pin operator ^ matches against existing variable values rather than binding new variables. This prevents accidental variable shadowing and enables matching against computed values.

target = 42
values = [10, 42, 30]

case values
in [a, ^target, b]
  puts "Found target #{target} between #{a} and #{b}"
  # => "Found target 42 between 10 and 30"
end

# Pinning with expressions
threshold = 50
numbers = [60, 45, 70]

case numbers
in [a, b, c] if a > ^(threshold + 10) && c > ^threshold
  puts "First #{a} and third #{c} exceed thresholds"
  # => "First 60 and third 70 exceed thresholds"
end

Common Pitfalls

Variable scoping in pattern matching creates new local variables that persist outside the pattern matching construct. Variables bound within patterns remain accessible after the case statement completes, potentially overwriting existing variables with the same names.

x = "original"
data = [1, 2, 3]

case data
in [x, y, z]
  puts "Inside: x=#{x}, y=#{y}, z=#{z}"
  # => "Inside: x=1, y=2, z=3"
end

puts "Outside: x=#{x}"  # => "Outside: x=1" (overwrote original!)

# Use different variable names or pin operator to avoid this
x = "original"
case data
in [a, b, c]  # Different names
  puts "Values: #{a}, #{b}, #{c}"
end
puts "Preserved: x=#{x}"  # => "Preserved: x=original"

Hash pattern matching ignores unspecified keys by default, which can lead to unexpected matches when expecting exact structure validation. Use **nil to explicitly reject additional keys.

config = { host: "localhost", port: 3000, debug: true, timeout: 30 }

# This matches despite extra keys
case config
in { host: h, port: p }
  puts "Matched: #{h}:#{p}"
  # => "Matched: localhost:3000"
end

# Strict matching rejects extra keys
case config
in { host: h, port: p, **nil }
  puts "Exact match"
else
  puts "Extra keys found"  # This executes
end

Array length mismatches cause pattern failures even when using splat operators incorrectly. The splat operator captures remaining elements but cannot create elements that don't exist.

short_array = [1, 2]

case short_array
in [a, b, c]
  puts "Three elements"
else
  puts "Length mismatch"  # This executes
end

# Splat handles variable lengths correctly
case short_array
in [a, *rest]
  puts "First: #{a}, Rest: #{rest}"
  # => "First: 1, Rest: [2]"
end

# But splat cannot match specific minimum lengths
case [1]
in [a, b, *rest]
  puts "At least two elements"
else
  puts "Too few elements"  # This executes
end

Type pattern matching requires understanding Ruby's type system and class hierarchy. Patterns match against exact classes unless using inheritance-aware comparisons.

class Animal; end
class Dog < Animal; end

dog = Dog.new

case dog
in Animal
  puts "Matched Animal"  # This executes (inheritance works)
end

case dog
in Dog
  puts "Matched Dog"  # This also executes (exact class)
end

# String matching gotcha
case "123"
in String => s if s.match?(/^\d+$/)
  puts "Numeric string: #{s}"
end

# But this fails
case 123
in String
  puts "Not a string"
else
  puts "Type mismatch"  # This executes
end

NoMatchingPatternError exceptions occur with rightward assignment when patterns fail, but case statements fall through to else clauses. This behavioral difference can cause unexpected program termination.

data = [1, 2]

# Case statement handles failure gracefully
case data
in [a, b, c]
  puts "Three elements"
else
  puts "Pattern failed gracefully"
end

# Rightward assignment raises exception
begin
  data => [x, y, z]
rescue NoMatchingPatternError => e
  puts "Exception: #{e.message}"
  # => "Exception: [1, 2]: [x, y, z]"
end

# Guard failures also cause exceptions with rightward assignment
begin
  [5, 10] => [a, b] if a > b
rescue NoMatchingPatternError
  puts "Guard condition failed"
end

Reference

Pattern Matching Operators

Operator Context Purpose Example
in case statement Pattern matching branch case x; in [a, b]; end
=> Rightward assignment Inline pattern matching [1, 2] => [a, b]
^ Pattern Pin variable value in [^x, y]
* Array pattern Splat remaining elements in [first, *rest]
** Hash pattern Splat remaining key-values in {a: 1, **rest}
**nil Hash pattern Reject additional keys in {a: 1, **nil}
| Pattern Alternative patterns in String | Integer

Array Pattern Syntax

Pattern Matches Binds
[a, b, c] Exactly 3 elements a, b, c
[first, *rest] 1+ elements first, rest (array)
[*init, last] 1+ elements init (array), last
[a, *middle, b] 2+ elements a, middle (array), b
[] Empty array None
[*, x] Any length, capture last x

Hash Pattern Syntax

Pattern Matches Binds
{a: x, b: y} Hash with keys :a, :b (+ others) x, y
{a: x, **rest} Hash with key :a (+ others) x, rest (hash)
{a: x, **nil} Hash with exactly key :a x
{} Empty hash or any hash None
{**nil} Exactly empty hash None

Guard Conditions

Syntax Behavior
in pattern if condition Pattern matches AND condition true
in pattern unless condition Pattern matches AND condition false
case x; in a if a > 0; end Guard accesses bound variables
[1, 2] => [a, b] if a < b Exception if guard fails

Custom Class Integration

Method Purpose Return Type Usage
deconstruct Array-like pattern matching Array in ClassName(a, b)
deconstruct_keys(keys) Hash-like pattern matching Hash in ClassName(x: a)

Exception Handling

Exception Raised When Context
NoMatchingPatternError Pattern match fails Rightward assignment =>
NoMatchingPatternError Guard condition fails Rightward assignment with guard

Type Patterns

Pattern Type Syntax Example
Class match ClassName in String
Value match literal in 42
Variable bind variable in x
Pin variable ^variable in ^x
Guard pattern pattern if expr in x if x > 0

Evaluation Order

  1. Pattern structure matching (array length, hash keys, object type)
  2. Value binding or literal comparison
  3. Guard condition evaluation (if present)
  4. Variable assignment (on successful match)
  5. Exception raising (rightward assignment failures)