CrackedRuby logo

CrackedRuby

Pattern Matching Methods

Pattern matching methods in Ruby provide structural data matching through case/in syntax and supporting deconstruction protocols.

Core Built-in Classes Regexp and MatchData
2.7.2

Overview

Pattern matching in Ruby operates through the case/in syntax introduced in Ruby 2.7, providing structural matching against data patterns rather than simple equality comparisons. The feature integrates with Ruby's existing case statement but adds pattern-based matching that can destructure arrays, hashes, objects, and primitive values.

Ruby implements pattern matching through several core mechanisms. The case/in construct serves as the primary interface, while objects can participate in pattern matching by implementing #deconstruct for positional patterns and #deconstruct_keys for keyword patterns. The pattern matching system evaluates patterns sequentially and binds matched values to local variables within the scope of each in clause.

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

The pattern matching system supports multiple pattern types including value patterns that match exact values, variable patterns that bind to any value, array patterns that destructure sequences, hash patterns that match key-value structures, and object patterns that use custom deconstruction methods.

# Multiple pattern types in one case
data = { name: "John", age: 30, city: "NYC" }

case data
in { name: String => n, age: (18..65) => a }
  puts "Adult: #{n}, age #{a}"
in { name: String => n, age: (0..17) => a }
  puts "Minor: #{n}, age #{a}"
end
# => Adult: John, age 30

Ruby's pattern matching integrates with existing language features including guards, alternative patterns, and variable pinning. The system maintains lexical scoping rules where variables bound in patterns become available within their respective in clause but not outside the case statement.

Basic Usage

The fundamental pattern matching syntax uses case followed by an expression and one or more in clauses that define patterns to match against. Each pattern can bind variables, check values, or combine multiple conditions.

Value patterns match exact values using the same equality semantics as regular case statements. These patterns work with any object that responds to #=== including classes, regular expressions, and ranges.

# Value patterns
case "hello"
in "hello"
  puts "Exact match"
in /^h/
  puts "Regex match"
end
# => Exact match

case 42
in Integer
  puts "Integer type"
in (40..50)
  puts "Range match"
end
# => Integer type

Variable patterns bind the matched value to a local variable. Ruby creates these variables within the scope of the matching in clause. Underscore variables (_) act as wildcards that match any value without binding.

# Variable binding
case [1, 2, 3]
in [first, _, last]
  puts "First: #{first}, Last: #{last}"
end
# => First: 1, Last: 3

Array patterns destructure array-like objects that respond to #deconstruct. The pattern can specify exact length, use splat operators for variable length, or combine both approaches.

# Array pattern variations
data = [1, 2, 3, 4, 5]

case data
in [a, b]
  puts "Exactly two: #{a}, #{b}"
in [first, *middle, last]
  puts "First: #{first}, Middle: #{middle}, Last: #{last}"
in [head, *tail]
  puts "Head: #{head}, Tail: #{tail}"
end
# => First: 1, Middle: [2, 3, 4], Last: 5

Hash patterns match hash-like objects that respond to #deconstruct_keys. These patterns can specify required keys, optional keys, or use double splat to capture remaining keys.

# Hash patterns
person = { name: "Alice", age: 25, city: "Boston", country: "USA" }

case person
in { name: n, age: a, **rest }
  puts "Person: #{n}, #{a} years old"
  puts "Other info: #{rest}"
end
# => Person: Alice, 25 years old
# => Other info: {:city=>"Boston", :country=>"USA"}

Multiple patterns within a single in clause use the | operator for alternative matching. The first matching pattern binds its variables, and subsequent alternatives in the same clause do not execute.

# Alternative patterns
case [1, 2]
in [a, b] | [a, b, c]
  puts "Two or three elements: #{a}, #{b}"
end
# => Two or three elements: 1, 2

Advanced Usage

Advanced pattern matching combines multiple pattern types, uses guards for conditional logic, implements variable pinning for exact matches, and integrates with custom objects through deconstruction protocols.

Guard clauses add conditional logic to patterns using the if keyword. Ruby evaluates guards after pattern matching succeeds, and the pattern only matches when both the pattern and guard condition are true.

# Guard clauses
data = [10, 20, 30]

case data
in [a, b, c] if a + b > c
  puts "Sum of first two exceeds third: #{a} + #{b} > #{c}"
in [a, b, c] if a < b && b < c
  puts "Ascending sequence: #{a} < #{b} < #{c}"
end
# => Ascending sequence: 10 < 20 < 30

Variable pinning uses the ^ operator to match against existing variable values rather than binding new variables. This technique checks that the matched value equals the pinned variable's current value.

# Variable pinning
expected_status = "success"
response = { status: "success", data: [1, 2, 3] }

case response
in { status: ^expected_status, data: results }
  puts "Expected status matched, data: #{results}"
in { status: status, data: results }
  puts "Different status: #{status}, data: #{results}"
end
# => Expected status matched, data: [1, 2, 3]

Nested patterns combine multiple pattern types within complex data structures. Ruby evaluates nested patterns recursively, binding variables at each level of the structure.

# Complex nested patterns
complex_data = {
  users: [
    { name: "John", profile: { age: 30, skills: ["ruby", "python"] } },
    { name: "Jane", profile: { age: 25, skills: ["javascript", "go"] } }
  ],
  metadata: { version: "1.0", created_at: "2024-01-01" }
}

case complex_data
in { 
     users: [
       { name: first_name, profile: { age: (25..35) => age, skills: [*langs] } },
       *other_users
     ],
     metadata: { version: version }
   }
  puts "First user: #{first_name}, age #{age}"
  puts "Languages: #{langs}"
  puts "Version: #{version}"
  puts "Other users: #{other_users.size}"
end
# => First user: John, age 30
# => Languages: ["ruby", "python"] 
# => Version: 1.0
# => Other users: 1

Custom objects participate in pattern matching by implementing deconstruction methods. The #deconstruct method returns an array for positional patterns, while #deconstruct_keys accepts a keys array and returns a hash for keyword patterns.

# Custom deconstruction
class Point
  attr_reader :x, :y, :z

  def initialize(x, y, z = 0)
    @x, @y, @z = x, y, z
  end

  def deconstruct
    [x, y, z]
  end

  def deconstruct_keys(keys)
    { x: x, y: y, z: z }.slice(*keys)
  end
end

point = Point.new(10, 20, 5)

case point
in Point[x, y, 0]
  puts "2D point: (#{x}, #{y})"
in Point[x, y, z] if z > 0
  puts "3D point: (#{x}, #{y}, #{z})"
end
# => 3D point: (10, 20, 5)

case point
in Point(x: px, y: py) if px > py
  puts "X dominates: #{px} > #{py}"
end
# => X dominates: 10 > 20 (this won't match due to condition)

Pattern matching integrates with Ruby's assignment operators for one-liner destructuring without full case statements. The => operator assigns matched patterns directly to variables.

# One-liner pattern assignment
data = { name: "Bob", scores: [85, 90, 88] }
data => { name: person_name, scores: [first_score, *other_scores] }

puts "#{person_name} first score: #{first_score}"
puts "Other scores: #{other_scores}"
# => Bob first score: 85
# => Other scores: [90, 88]

Common Pitfalls

Pattern matching contains several behaviors that frequently cause confusion, particularly around variable scoping, pattern evaluation order, and performance characteristics. Understanding these edge cases prevents subtle bugs in production code.

Variable binding in pattern matching creates new local variables within each in clause, but these variables do not persist outside the case statement. This scoping behavior differs from variable assignment in other Ruby constructs and can lead to NameError when accessing pattern variables outside their scope.

# Variable scoping gotcha
data = [1, 2, 3]

case data
in [a, b, c]
  puts "Inside pattern: #{a}, #{b}, #{c}"
end

# This raises NameError
begin
  puts "Outside pattern: #{a}"
rescue NameError => e
  puts "Error: #{e.message}"
end
# => Inside pattern: 1, 2, 3
# => Error: undefined local variable or method `a' for main:Object

Pattern evaluation occurs in the order patterns appear, and Ruby stops at the first successful match. Placing general patterns before specific patterns can mask more precise matching logic, leading to unexpected behavior.

# Pattern order importance
value = { type: "user", role: "admin", name: "Alice" }

# Incorrect order - general pattern matches first
case value
in { type: "user", **rest }
  puts "General user pattern matched: #{rest}"
in { type: "user", role: "admin", name: name }
  puts "Admin user: #{name}"  # This never executes
end
# => General user pattern matched: {:role=>"admin", :name=>"Alice"}

# Correct order - specific patterns first
case value
in { type: "user", role: "admin", name: name }
  puts "Admin user: #{name}"
in { type: "user", **rest }
  puts "General user pattern matched: #{rest}"
end
# => Admin user: Alice

Hash pattern matching can behave unexpectedly when keys are missing or have nil values. Ruby distinguishes between missing keys and keys with nil values, which affects pattern matching behavior.

# Nil vs missing key behavior
data_with_nil = { name: "John", age: nil }
data_missing_key = { name: "John" }

# Both patterns match differently
case data_with_nil
in { name: n, age: a }
  puts "With nil - Name: #{n}, Age: #{a.inspect}"
end

case data_missing_key  
in { name: n, age: a }
  puts "Missing key - Name: #{n}, Age: #{a.inspect}"
rescue NoMatchingPatternError => e
  puts "Pattern failed: #{e.message}"
end
# => With nil - Name: John, Age: nil
# => Pattern failed: {name: "John"} (NoMatchingPatternError)

Performance degradation can occur with complex nested patterns or frequent pattern matching operations. Ruby evaluates each pattern sequentially and performs structural comparisons, which can become expensive with deep nesting or large data structures.

# Performance consideration with deep nesting
large_nested = {
  level1: {
    level2: {
      level3: {
        level4: { data: (1..1000).to_a }
      }
    }
  }
}

# This pattern requires deep structural traversal
case large_nested
in { level1: { level2: { level3: { level4: { data: [first, *rest] } } } } }
  puts "Deep match successful, first: #{first}, count: #{rest.size}"
end
# => Deep match successful, first: 1, count: 999

Alternative patterns using | can create variable binding conflicts when different alternatives bind different variable names. Ruby requires consistent variable names across all alternatives in the same in clause.

# Variable binding conflicts in alternatives
data = [1, 2]

# This causes a syntax error
begin
  eval <<~RUBY
    case data
    in [a, b] | [x, y, z]
      puts "Alternative matched"
    end
  RUBY
rescue SyntaxError => e
  puts "Syntax error: #{e.message}"
end
# => Syntax error: illegal variable name in alternative pattern

# Correct approach with consistent variables
case data
in [a, b] | [a, b, c]
  puts "Variables consistent: #{a}, #{b}"
  puts "Third element: #{c}" if defined?(c)
end
# => Variables consistent: 1, 2

Pattern exhaustiveness warnings do not exist in Ruby's pattern matching implementation. Non-matching patterns raise NoMatchingPatternError unless an else clause handles unmatched cases, which can cause runtime failures in production.

# Missing exhaustive coverage
def process_shape(shape)
  case shape
  in { type: "circle", radius: r }
    "Circle with radius #{r}"
  in { type: "rectangle", width: w, height: h }
    "Rectangle #{w}x#{h}"
  # Missing triangle case
  end
end

begin
  result = process_shape({ type: "triangle", base: 10, height: 5 })
rescue NoMatchingPatternError => e
  puts "Unhandled case: #{e.message}"
end
# => Unhandled case: {:type=>"triangle", :base=>10, :height=>5} (NoMatchingPatternError)

Reference

Pattern Matching Syntax

Construct Syntax Description
case/in statement case expr; in pattern; end Primary pattern matching construct
Pattern assignment expr => pattern Direct assignment with pattern matching
Value pattern in 42, in "string" Matches exact values using ===
Variable pattern in var Binds matched value to variable
Wildcard pattern in _ Matches any value without binding
Array pattern in [a, b, c] Destructures arrays via #deconstruct
Hash pattern in { key: var } Destructures hashes via #deconstruct_keys
Alternative pattern in pattern1 | pattern2 Matches first successful alternative
Guard clause in pattern if condition Adds conditional logic to patterns
Variable pinning in ^var Matches against existing variable value

Array Pattern Operators

Pattern Example Description
Fixed length [a, b, c] Matches exactly three elements
Splat operator [first, *rest] Captures remaining elements
Middle splat [first, *middle, last] Captures middle elements
Empty splat [*] Matches any array length
Nested arrays [[a, b], [c, d]] Matches nested array structures

Hash Pattern Operators

Pattern Example Description
Specific keys { name: n, age: a } Matches specific keys
Double splat { key: val, **rest } Captures remaining keys
Nested hashes { user: { name: n } } Matches nested hash structures
Key existence { key: } Checks key exists (Ruby 3.1+)

Deconstruction Methods

Method Parameters Returns Purpose
#deconstruct None Array Provides positional pattern data
#deconstruct_keys Array<Symbol> or nil Hash Provides keyword pattern data

Exception Classes

Exception Raised When Recovery
NoMatchingPatternError No pattern matches and no else clause Add else clause or additional patterns
SyntaxError Invalid pattern syntax Fix pattern syntax
NameError Accessing pattern variables outside scope Move variable access inside pattern scope

Performance Characteristics

Pattern Type Time Complexity Notes
Value patterns O(1) Uses === operator
Variable patterns O(1) Direct assignment
Array patterns O(n) Iterates through elements
Hash patterns O(k) Depends on number of keys
Nested patterns O(depth × elements) Recursive evaluation
Guards O(pattern + condition) Evaluates after pattern match

Method Integration

Context Method Behavior
Custom objects #deconstruct Returns array for in Object[...] patterns
Custom objects #deconstruct_keys(keys) Returns hash for in Object(key: val) patterns
Arrays Built-in Uses array indexing for patterns
Hashes Built-in Uses hash key lookup for patterns
Ranges #=== Provides inclusion testing for value patterns
Classes #=== Provides type checking for value patterns