CrackedRuby logo

CrackedRuby

Pattern Matching in Depth

Overview

Pattern matching in Ruby allows you to match values against patterns and extract data from complex structures in a single operation. Introduced as experimental in Ruby 2.7 and stabilized in Ruby 3.0, it brings functional programming concepts to Ruby's object-oriented paradigm.

The core syntax uses the case/in construct, similar to traditional case/when statements but with structural matching capabilities:

case value
in pattern
  # matched
else
  # no match
end

Pattern matching works with arrays, hashes, objects, and primitive values, supporting deep structural matching and variable binding:

data = { name: "Alice", scores: [95, 87, 92] }

case data
in { name: String => name, scores: [first, *rest] }
  puts "#{name}'s first score: #{first}, others: #{rest}"
end
# Output: Alice's first score: 95, others: [87, 92]

The feature integrates with Ruby's existing type system and supports custom pattern matching through the deconstruct and deconstruct_keys methods.

Basic Usage

Literal Value Matching

Pattern matching supports direct value comparison, similar to traditional case statements:

case status
in 200
  "Success"
in 404
  "Not Found"
in 500..599
  "Server Error"
end

Variable Binding

Use identifiers to capture matched values:

case response
in { status: code, body: content }
  puts "Status: #{code}, Content: #{content}"
end

Variables bound in patterns are available in the matched branch and follow Ruby's scoping rules.

Array Patterns

Arrays support positional matching with splat operators:

case numbers
in []
  "Empty array"
in [first]
  "Single element: #{first}"
in [first, second]
  "Two elements: #{first}, #{second}"
in [first, *rest]
  "First: #{first}, Rest: #{rest}"
in [*init, last]
  "All but last: #{init}, Last: #{last}"
end

Array patterns match exact length unless using splat operators. The splat can appear anywhere in the pattern:

case data
in [*prefix, middle, *suffix] if prefix.length == suffix.length
  puts "Symmetric array with center: #{middle}"
end

Hash Patterns

Hash patterns match key-value pairs and support partial matching:

case user
in { name: String, age: Integer => user_age } if user_age >= 18
  "Adult user"
in { name: String, age: Integer }
  "Minor user"
in { name: String }
  "User with no age"
end

Hash patterns are "open" by default - they match hashes containing additional keys:

user = { name: "Bob", age: 25, email: "bob@example.com" }

case user
in { name: name, age: age }
  puts "#{name} is #{age} years old"
  # Matches even though email key exists
end

Use **nil to require exact hash matching:

case config
in { host: host, port: port, **nil }
  # Matches only if hash contains exactly host and port keys
end

Advanced Usage

Nested Patterns

Patterns can be arbitrarily nested to match complex data structures:

case api_response
in {
  status: "success",
  data: {
    users: [{ name: String, posts: [*, { title: title }, *] }, *]
  }
}
  puts "Found post titled: #{title}"
end

This pattern matches responses where status is "success", data contains a users array, and the first user has posts with at least one containing a title.

Guard Clauses

Add conditions to patterns using if or unless:

case coordinates
in { x: x, y: y } if x > 0 && y > 0
  "First quadrant"
in { x: x, y: y } if x.zero? || y.zero?
  "On an axis"
in { x: x, y: y }
  "Other quadrants"
end

Guard clauses have access to bound variables and are evaluated after pattern matching succeeds.

Alternative Patterns

Use | to specify multiple acceptable patterns:

case value
in String | Symbol => str
  puts str.to_s.upcase
in Integer | Float => num
  puts num.abs
end

Alternative patterns must bind the same variables:

case data
in { type: "user", name: name } | { type: "admin", username: name }
  puts "Hello, #{name}"
end

Pin Operator

Use ^ to match against existing variable values instead of binding:

expected_status = 200

case response
in { status: ^expected_status, body: body }
  puts "Got expected status with body: #{body}"
end

Without the pin operator, expected_status would be rebound to the matched value.

Object Pattern Matching

Implement pattern matching for custom classes:

class Person
  attr_reader :name, :age, :email

  def initialize(name, age, email)
    @name, @age, @email = name, age, email
  end

  # For array-style pattern matching: Person(name, age)
  def deconstruct
    [name, age]
  end

  # For hash-style pattern matching: Person[name:, age:]
  def deconstruct_keys(keys)
    case keys
    when [:name] then { name: name }
    when [:age] then { age: age }
    when [:name, :age] then { name: name, age: age }
    when nil then { name: name, age: age, email: email }
    else {}
    end
  end
end

person = Person.new("Charlie", 30, "charlie@example.com")

case person
in Person(name, age) if age >= 18
  puts "Adult: #{name}"
end

case person
in Person[name: name, age: age] if age >= 65
  puts "Senior: #{name}"
in Person[name: name]
  puts "Person: #{name}"
end

The deconstruct_keys method receives the keys requested by the pattern or nil for all available keys.

Error Handling & Debugging

NoMatchingPatternError

Pattern matching raises NoMatchingPatternError if no patterns match:

begin
  case value
  in Integer
    "number"
  in String
    "text"
  end
rescue NoMatchingPatternError => e
  puts "No pattern matched: #{e.message}"
end

Always include an else clause or ensure patterns are exhaustive:

case value
in Integer
  "number"
in String
  "text"
else
  "other type"
end

Pattern Matching with Rescue

Handle exceptions within pattern matching:

case data
in { url: url }
  begin
    fetch_data(url)
  rescue Net::TimeoutError
    "Request timed out"
  end
else
  "Invalid data format"
end

Debugging Complex Patterns

Use verbose patterns for debugging complex matches:

case complex_data
in {
  metadata: { version: version },
  content: [first_item, *rest]
} => matched_data

  puts "Matched version: #{version}"
  puts "First item: #{first_item}"
  puts "Remaining items: #{rest.length}"
  puts "Full match: #{matched_data}"
end

The => variable syntax captures the entire matched value for inspection.

Validation Patterns

Create patterns specifically for data validation:

def validate_user_data(data)
  case data
  in {
    name: String => name,
    email: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
    age: Integer => age
  } if name.length > 0 && age.between?(13, 120)
    { valid: true, user: data }
  else
    { valid: false, errors: ["Invalid user data format"] }
  end
end

Performance & Memory

Pattern Complexity Impact

Simple patterns perform better than complex nested patterns:

# Faster - direct hash access
case data
in { status: 200 }
  handle_success
end

# Slower - deep nested matching
case data
in {
  response: {
    metadata: { status: 200, headers: { content_type: /json/ } }
  }
}
  handle_success
end

Pattern Ordering Strategy

Order patterns from most specific to least specific for optimal performance:

case value
in { type: "premium", features: [*, "advanced", *] }  # Most specific first
  "Premium with advanced features"
in { type: "premium" }                               # Less specific
  "Premium user"
in { type: String }                                  # Least specific
  "Basic user"
end

Memory Considerations

Pattern matching creates temporary objects during matching. For high-frequency operations, consider traditional conditionals:

# Memory-efficient for simple cases
if hash[:status] == 200 && hash[:data]
  process_success(hash[:data])
end

# Pattern matching alternative
case hash
in { status: 200, data: data }
  process_success(data)
end

Benchmarking Pattern Performance

Consider pattern complexity when processing large datasets:

require 'benchmark'

data = Array.new(10000) { { status: rand(100..599), data: "content" } }

Benchmark.bm do |x|
  x.report("if/else") do
    data.each { |item| item[:status] == 200 ? "success" : "error" }
  end

  x.report("pattern") do
    data.each do |item|
      case item
      in { status: 200 }
        "success"
      else
        "error"
      end
    end
  end
end

Production Patterns

API Response Processing

Pattern matching excels at handling varied API responses with complex structures:

class APIClient
  def handle_response(response)
    case JSON.parse(response.body, symbolize_names: true)
    in { success: true, data: data }
      Result.success(data)

    in { success: false, error: { code: code, message: message } }
      Result.error("API Error #{code}: #{message}")

    in { errors: [{ field: field, message: message }, *] }
      Result.validation_error(field, message)

    in { status: status } if (400..499).include?(status)
      Result.client_error("Client error: #{status}")

    in { status: status } if (500..599).include?(status)
      Result.server_error("Server error: #{status}")

    else
      Result.error("Unknown response format")
    end
  end
end

Configuration Processing

Handle complex configuration scenarios with nested validation:

class ConfigProcessor
  def process(config)
    case config
    in {
      database: {
        adapter: "postgresql",
        host: String => host,
        port: Integer => port,
        credentials: { username: String, password: String }
      },
      cache: { type: "redis", url: String => cache_url },
      **rest
    }
      setup_postgres_with_redis(host, port, cache_url, rest)

    in {
      database: { adapter: "sqlite", file: String => file },
      **rest
    }
      setup_sqlite(file, rest)

    in { database: { adapter: adapter } }
      raise ConfigError, "Unsupported database adapter: #{adapter}"

    in { database: nil }
      raise ConfigError, "Database configuration required"

    else
      raise ConfigError, "Invalid configuration format"
    end
  end

  private

  def setup_postgres_with_redis(host, port, cache_url, options)
    # Implementation with additional options handling
  end
end

Event Processing Systems

Pattern matching works exceptionally well for event-driven architectures:

class EventProcessor
  def handle(event)
    case event
    in {
      type: "user.created",
      data: { id: String => user_id, email: String => email },
      metadata: { source: source }
    }
      send_welcome_email(user_id, email)
      track_user_source(user_id, source)

    in {
      type: "payment.succeeded",
      data: { amount: Integer => amount, currency: "USD" },
      metadata: { user_id: user_id }
    } if amount > 10000
      flag_large_payment(user_id, amount)

    in {
      type: /\Aadmin\./,
      data: data,
      metadata: { admin_id: admin_id }
    }
      AdminEventHandler.handle(event, admin_id)

    in { type: type, **rest }
      logger.warn "Unhandled event type: #{type}"
      logger.debug "Event data: #{rest}"

    else
      logger.error "Invalid event format: #{event}"
    end
  end
end

Data Transformation Pipelines

Use pattern matching for complex data transformation workflows:

class DataTransformer
  def transform(records)
    records.map do |record|
      case record
      in {
        type: "customer",
        data: {
          personal: { name: name, age: Integer => age },
          contact: { email: email }
        }
      } if age >= 18
        build_adult_customer(name, age, email)

      in {
        type: "customer",
        data: {
          personal: { name: name, age: Integer => age },
          guardian: { name: guardian_name, email: email }
        }
      } if age < 18
        build_minor_customer(name, age, guardian_name, email)

      in { type: "business", data: { company: company, **business_data } }
        build_business_customer(company, business_data)

      else
        raise TransformationError, "Unknown record format: #{record}"
      end
    end
  end
end

Common Pitfalls

Variable Scoping Issues

Variables bound in patterns can shadow existing variables:

name = "Alice"

case data
in { name: name }  # This rebinds the local variable
  puts name        # Uses the matched value, not "Alice"
end

puts name          # Now contains the matched value

Use the pin operator to avoid rebinding:

name = "Alice"

case data
in { name: ^name }  # Matches against existing value
  puts "Found Alice"
end

puts name           # Still "Alice"

Hash Key Type Sensitivity

Hash patterns are sensitive to key types and will not match mismatched key types:

data = { "name" => "Bob", "age" => 25 }

case data
in { name: name }  # Won't match - looking for symbol keys
  puts name
end

case data
in { "name" => name }  # Matches - correct key type
  puts name
end

Convert keys consistently when working with external data:

case JSON.parse(json_string, symbolize_names: true)
in { name: name, age: age }  # Now works with symbol keys
  puts "#{name} is #{age} years old"
end

Array Pattern Matching Subtleties

Understand the difference between array patterns and splat usage:

case [[1, 2], [3, 4]]
in [first, second]        # Matches two nested arrays
  # first = [1, 2], second = [3, 4]

in [*elements]            # Matches all elements as array
  # elements = [[1, 2], [3, 4]]

in [first, *rest]         # Matches first element, rest as array
  # first = [1, 2], rest = [[3, 4]]
end

Pattern Exhaustiveness Problems

Pattern matching doesn't warn about non-exhaustive patterns, which can lead to runtime errors:

# Dangerous - might raise NoMatchingPatternError
def categorize(value)
  case value
  in String
    "text"
  in Integer if value > 0
    "positive number"
  # Missing: negative integers, floats, other types
  end
end

# Safe version with exhaustive patterns
def categorize(value)
  case value
  in String
    "text"
  in Integer if value > 0
    "positive number"
  in Integer
    "non-positive number"
  in Numeric
    "other numeric type"
  else
    "non-numeric type"
  end
end

Guard Clause Gotchas

Guard clauses can access bound variables but may create unexpected behavior:

case data
in { count: count } if count > 0
  puts "Positive count: #{count}"
in { count: count }
  puts "Non-positive count: #{count}"
end

# The second pattern will only match if the first guard fails
# But `count` is still bound from the first pattern attempt

Reference

Pattern Matching Operators

Operator Usage Description
in case x in pattern Pattern matching case clause
=> pattern => var Capture entire matched value
^ ^variable Pin operator - match against existing value
| pattern1 | pattern2 Alternative patterns
* [first, *rest] Array splat pattern
** {a: 1, **rest} Hash splat pattern
**nil {a: 1, **nil} Exact hash matching

Pattern Types

Pattern Type Syntax Example
Literal value 42, "hello", :symbol
Variable identifier name, count
Array [pattern, ...] [first, second], [*, last]
Hash {key: pattern, ...} {name: String, age: Integer}
Object Class(...) or Class[...] Date(y, m, d), Person[name:]
Guard pattern if condition Integer => n if n > 0
Alternative pattern | pattern String | Symbol
Pin ^variable ^expected_value

Custom Pattern Matching Methods

Method Purpose Return Value
deconstruct Array-style patterns Class(...) Array of values
deconstruct_keys(keys) Hash-style patterns Class[...] Hash with requested keys

Exception Types

Exception Raised When
NoMatchingPatternError No pattern matches the value
TypeError Object doesn't respond to deconstruct methods

Built-in Classes Supporting Pattern Matching

Class Array Pattern Hash Pattern Example
Date Date(year, month, day) No Date(2023, 12, 25)
Time Time(year, month, day, hour, min, sec) No Time(2023, 12, 25, 10, 30, 0)
MatchData Array of captures No MatchData[full_match, *captures]

Performance Guidelines

Guideline Reason Example
Order patterns specific to general Reduces unnecessary matching attempts Most restrictive patterns first
Avoid deep nesting in loops Creates temporary objects Use simple patterns in hot code paths
Use guards judiciously Evaluated after pattern matching Only when pattern logic insufficient
Pin variables for value matching Avoids variable rebinding ^expected_value vs expected_value
Prefer traditional conditionals for simple checks Lower overhead if hash[:key] == value vs pattern matching

Ruby Version Support

Ruby Version Pattern Matching Support
2.7.0 Experimental (warnings enabled)
3.0.0+ Stable, full support
3.1.0+ Improved performance optimizations
3.2.0+ Additional syntax improvements