CrackedRuby logo

CrackedRuby

Pattern Matching with case/in

Overview

Pattern matching in Ruby uses the case/in syntax to match values against patterns, enabling destructuring and conditional logic based on data shape and content. Ruby's pattern matching system supports literal values, variable binding, array patterns, hash patterns, and object patterns.

The pattern matching mechanism evaluates each pattern sequentially until finding a match. When a pattern matches, Ruby executes the corresponding code block and binds any capture variables. If no patterns match, Ruby raises a NoMatchingPatternError.

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

Ruby implements pattern matching through the === operator and specialized matching protocols. Objects can customize their matching behavior by implementing deconstruct for array patterns and deconstruct_keys for hash patterns.

case {name: "Alice", age: 30}
in {name: String => n, age: Integer => a}
  puts "Name: #{n}, Age: #{a}"
end
# => "Name: Alice, Age: 30"

Pattern matching works with built-in types including arrays, hashes, strings, numbers, and custom objects. The system distinguishes between value patterns that match specific values and variable patterns that capture matched values.

Basic Usage

Ruby's pattern matching syntax begins with case followed by the expression to match. Each pattern branch uses in instead of when. The simplest patterns match literal values directly.

status_code = 404

case status_code
in 200
  puts "Success"
in 404
  puts "Not Found"
in 500
  puts "Server Error"
else
  puts "Other status"
end
# => "Not Found"

Variable patterns capture the matched value and bind it to a variable name. Ruby distinguishes between existing variables and new pattern variables based on whether the variable exists in the current scope.

x = 10

case [1, 2, 3]
in [a, b, c]  # Creates new variables a, b, c
  puts "#{a}, #{b}, #{c}"
end

case [1, 2, 3]  
in [^x, y, z]  # Pin operator ^ uses existing variable x
  puts "First element is #{x}"
else
  puts "First element is not #{x}"
end
# => "First element is not 10"

Array patterns destructure arrays and match individual elements. The splat operator * captures remaining elements into an array. Patterns can specify exact lengths or use flexible matching.

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

case ["GET", "/users", {id: 123}]
in [method, path, params]
  puts "#{method} request to #{path} with #{params}"
end
# => "GET request to /users with {:id=>123}"

Hash patterns match hash keys and capture values. Ruby matches only the specified keys, ignoring additional keys unless using the **nil rest pattern to require exact matches.

request = {method: "POST", path: "/api/users", body: {name: "Bob"}}

case request
in {method: "POST", path: path, body: {name: name}}
  puts "Creating user #{name} at #{path}"
end
# => "Creating user Bob at /api/users"

# Exact hash matching with **nil
case {a: 1, b: 2}
in {a: Integer, **nil}
  puts "Hash has only key :a"
else  
  puts "Hash has additional keys"
end
# => "Hash has additional keys"

Guard clauses add conditional logic to patterns using if or unless. The guard evaluates after the pattern matches and can access bound variables.

case [1, 2, 3]
in [x, y, z] if x + y == z
  puts "#{x} + #{y} = #{z}"
in [x, y, z] if x + z == y  
  puts "#{x} + #{z} = #{y}"
else
  puts "No arithmetic relationship"
end
# => "No arithmetic relationship"

Advanced Usage

Alternative patterns match multiple possibilities using the pipe operator |. Each alternative can bind the same variables, but the variables must have compatible types across alternatives.

case ["admin", {permissions: ["read", "write"]}]
in ["admin" | "superuser", {permissions: perms}]
  puts "Administrative user with #{perms}"
in ["user", {permissions: perms}]  
  puts "Regular user with #{perms}"
end
# => "Administrative user with [\"read\", \"write\"]"

As patterns use => to capture the entire matched value or subpatterns while continuing to match structure. This enables accessing both the whole and parts of complex data.

response = {
  status: 200,
  data: {
    users: [
      {id: 1, name: "Alice"},
      {id: 2, name: "Bob"}  
    ]
  }
}

case response
in {status: 200, data: {users: users_array} => data_section}
  puts "Success: #{users_array.length} users"
  puts "Full data: #{data_section}"
end
# => "Success: 2 users"
# => "Full data: {:users=>[{:id=>1, :name=>\"Alice\"}, {:id=>2, :name=>\"Bob\"}]}"

Custom objects participate in pattern matching by implementing deconstruct for array-like matching and deconstruct_keys for hash-like matching. These methods define how objects decompose during pattern matching.

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

point = Point.new(3, 4)

case point
in Point[x, y] if x**2 + y**2 == 25
  puts "Point on unit circle scaled by 5"
in Point(x:, y:)
  puts "Point at (#{x}, #{y})"
end
# => "Point at (3, 4)"

Nested patterns match complex data structures by combining multiple pattern types. Ruby evaluates nested patterns depth-first, binding variables as patterns match.

api_response = {
  metadata: {version: "2.1", timestamp: "2024-01-15"},
  results: [
    {type: "user", data: {id: 1, name: "Alice", active: true}},
    {type: "user", data: {id: 2, name: "Bob", active: false}},
    {type: "admin", data: {id: 3, name: "Charlie", role: "superuser"}}
  ]
}

case api_response
in {
     metadata: {version: version},
     results: [
       *prefix,
       {type: "admin", data: {name: admin_name, role: role}},
       *suffix
     ]
   }
  puts "API v#{version}: Found admin #{admin_name} (#{role}) at position #{prefix.length}"
  puts "#{prefix.length} users before, #{suffix.length} after"
end
# => "API v2.1: Found admin Charlie (superuser) at position 2"
# => "2 users before, 0 after"

Pattern matching integrates with case expressions and can return values. Each pattern branch returns the value of its last expression, enabling functional programming patterns.

def process_request(request)
  case request
  in {method: "GET", path: path, params: params}
    "Fetching #{path} with #{params}"
  in {method: "POST", path: path, body: body}
    "Creating resource at #{path}: #{body}"
  in {method: method, path: path}
    "#{method} request to #{path}"
  else
    "Invalid request format"
  end
end

result = process_request({method: "POST", path: "/users", body: {name: "Dave"}})
puts result
# => "Creating resource at /users: {:name=>\"Dave\"}"

Common Pitfalls

Variable scoping in pattern matching creates subtle bugs when pattern variables shadow existing variables. Ruby creates new local variables for unbound names in patterns, which can mask intended variable references.

user_id = 42
users = [{id: 1, name: "Alice"}, {id: 42, name: "Bob"}]

case users.first
in {id: user_id, name: name}  # Creates new user_id variable!
  puts "Found user #{name} with ID #{user_id}"
end

puts "Original user_id is still #{user_id}"
# => "Found user Alice with ID 1"
# => "Original user_id is still 42"

# Correct approach using pin operator
case users.first
in {id: ^user_id, name: name}  # Uses existing user_id variable
  puts "Found matching user #{name}"
else
  puts "User ID does not match #{user_id}"
end
# => "User ID does not match 42"

Hash patterns match subset keys by default, which can lead to unexpected matches when expecting exact key sets. The **nil rest pattern enforces exact matching but must appear last in the pattern.

config = {host: "localhost", port: 3000, debug: true, ssl: false}

case config
in {host: host, port: port}  # Matches despite extra keys
  puts "Basic config: #{host}:#{port}"
end
# => "Basic config: localhost:3000"

# Force exact matching
case config  
in {host: host, port: port, **nil}  # Fails due to extra keys
  puts "Exact basic config"
else
  puts "Config has additional keys beyond host and port"
end
# => "Config has additional keys beyond host and port"

Array length mismatches cause patterns to fail silently. Ruby requires exact length matches unless using splat patterns, but debugging failed matches requires careful attention to array lengths.

coordinates = [10, 20, 30, 40]  # 4D coordinate

case coordinates
in [x, y]  # Expects 2D
  puts "2D: (#{x}, #{y})"
in [x, y, z]  # Expects 3D  
  puts "3D: (#{x}, #{y}, #{z})"
in [x, y, z, w]  # Expects 4D
  puts "4D: (#{x}, #{y}, #{z}, #{w})"
else
  puts "Unexpected coordinate format"
end
# => "4D: (10, 20, 30, 40)"

# Flexible matching with splat
case coordinates
in [x, y, *rest] if rest.empty?
  puts "2D: (#{x}, #{y})"  
in [x, y, z, *rest] if rest.empty?
  puts "3D: (#{x}, #{y}, #{z})"
else
  puts "Higher dimensional or unexpected format"
end
# => "Higher dimensional or unexpected format"

Guard clauses evaluate after pattern matching completes, so variables bound during matching are available in guards. However, guard failures cause the entire pattern to fail, not just the guard condition.

def categorize_number(n)
  case n
  in Integer => num if num > 0 && num.even?
    "positive even: #{num}"
  in Integer => num if num > 0  # This catches positive odd numbers
    "positive odd: #{num}"
  in Integer => num if num < 0
    "negative: #{num}"  
  in 0
    "zero"
  else
    "not an integer"
  end
end

puts categorize_number(7)   # => "positive odd: 7"  
puts categorize_number(-3)  # => "negative: -3"
puts categorize_number(0)   # => "zero"

NoMatchingPatternError occurs when no patterns match and no else clause exists. This error provides limited debugging information, making pattern debugging challenging without systematic testing.

def parse_response(response)
  case response
  in {status: 200, data: data}
    {success: true, result: data}
  in {status: 400..499, error: message}  
    {success: false, error: message}
  in {status: 500..599, error: message}
    {success: false, error: "Server error: #{message}"}
  end
end

# This raises NoMatchingPatternError
begin
  result = parse_response({status: 200})  # Missing data key
rescue NoMatchingPatternError => e
  puts "Pattern matching failed: #{e.message}"
  puts "Response format not recognized"
end
# => "Pattern matching failed: {:status=>200} (NoMatchingPatternError)"
# => "Response format not recognized"

Custom object pattern matching requires careful implementation of deconstruct and deconstruct_keys. Missing or incorrectly implemented methods cause pattern matching to fail silently.

class Rectangle
  def initialize(width, height)
    @width, @height = width, height
  end
  
  # Missing deconstruct methods - patterns will fail
end

rect = Rectangle.new(10, 20)

case rect
in Rectangle[w, h]  # Fails silently - no deconstruct method
  puts "Rectangle #{w}x#{h}"
else
  puts "Could not match rectangle pattern"
end
# => "Could not match rectangle pattern"

# Add proper deconstruct methods
class Rectangle
  def deconstruct
    [@width, @height]
  end
  
  def deconstruct_keys(keys)
    {width: @width, height: @height}
  end
end

case rect
in Rectangle[w, h]
  puts "Rectangle #{w}x#{h}"  
end
# => "Rectangle 10x20"

Reference

Case/In Syntax

Pattern Type Syntax Description
Literal in value Matches exact value using ===
Variable in variable_name Captures matched value
Pin in ^existing_var Matches using existing variable value
Alternative in pattern1 | pattern2 Matches either pattern
As in pattern => variable Captures matched value while matching pattern
Guard in pattern if condition Adds conditional logic after pattern match

Array Patterns

Pattern Example Description
Fixed length in [a, b, c] Matches exactly 3 elements
With splat in [first, *rest] Captures remaining elements
Nested in [[x, y], z] Matches nested array structure
Empty in [] Matches empty array
Single splat in [*all] Captures entire array

Hash Patterns

Pattern Example Description
Key matching in {key: value} Matches specific key-value pairs
Variable capture in {key: variable} Captures value for key
Rest pattern in {key: val, **rest} Captures remaining keys
Exact matching in {key: val, **nil} Requires exact key set
Nested in {outer: {inner: val}} Matches nested hash structure

Object Patterns

Method Purpose Return Type
deconstruct Array-like pattern matching Array
deconstruct_keys(keys) Hash-like pattern matching Hash

Pattern Matching Methods

Method Parameters Returns Description
case expr Expression Object Begins pattern matching expression
in pattern Pattern nil Defines match pattern
else None Object Default case when no patterns match

Exceptions

Exception Condition Description
NoMatchingPatternError No pattern matches, no else clause Pattern matching failed completely
SyntaxError Invalid pattern syntax Malformed pattern matching code

Pattern Matching Protocol

Custom objects implement these methods to participate in pattern matching:

class CustomObject
  def deconstruct
    # Return array for array pattern matching
    [@field1, @field2]
  end
  
  def deconstruct_keys(keys)
    # Return hash for hash pattern matching  
    # keys parameter indicates which keys are needed
    {key1: @field1, key2: @field2}
  end
end

Operator Precedence in Patterns

Operator Precedence Associativity
^ (pin) Highest Right
=> (as) High Left
| (alternative) Medium Left
, (sequence) Low Left

Guard Clause Evaluation

Guard clauses evaluate after successful pattern matching:

  1. Pattern matches successfully
  2. Variables are bound
  3. Guard condition evaluates with bound variables
  4. If guard fails, pattern fails and next pattern attempts
  5. If guard succeeds, pattern branch executes