CrackedRuby logo

CrackedRuby

Data Class Pattern Matching

Data Class Pattern Matching in Ruby provides declarative syntax for extracting and matching data from complex structures using case expressions and the in operator.

Core Built-in Classes Data Class
2.11.3

Overview

Ruby's pattern matching feature matches values against patterns to extract data and control program flow. The case/in syntax replaces complex conditional logic with declarative patterns that describe data structure and content expectations.

Pattern matching operates through the case/in construct and standalone in operator. Ruby evaluates patterns from top to bottom, executing the first matching pattern's associated code. Patterns can match literal values, capture variables, destructure arrays and hashes, and validate object attributes.

case [1, 2, 3]
in [a, b, c]
  puts "#{a}, #{b}, #{c}"  # => "1, 2, 3"
end

The deconstruct and deconstruct_keys methods define how custom objects participate in pattern matching. Classes implementing these methods can be matched against array and hash patterns respectively.

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

case Point.new(1, 2)
in [x, y]
  puts "Point at #{x}, #{y}"  # => "Point at 1, 2"
end

Pattern matching integrates with Ruby's existing data types. Arrays, hashes, ranges, and regular expressions all support pattern matching without additional configuration. The feature raises NoMatchingPatternError when no pattern matches unless an else clause exists.

Ruby's implementation supports guard clauses through if expressions, pinned variables using the pin operator (^), and rest patterns for capturing remaining elements. These features create flexible matching conditions while maintaining readable code structure.

Basic Usage

Array patterns match sequential data by position. Patterns can capture specific elements, ignore positions with wildcards, or collect remaining elements using splat operators.

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

case ['apple', 'banana', 'cherry']
in [fruit, *]
  puts "First fruit: #{fruit}"  # => "First fruit: apple"
end

Hash patterns match key-value pairs and support partial matching by default. Patterns can capture values, match specific keys, or combine both approaches for flexible data extraction.

user = { name: "Alice", age: 30, city: "Boston", role: "admin" }

case user
in { name:, age: } if age >= 18
  puts "Adult user: #{name}"  # => "Adult user: Alice"
end

case user
in { role: "admin", name: admin_name }
  puts "Admin: #{admin_name}"  # => "Admin: Alice"
end

Variable patterns capture matched values into local variables. Variable names must be lowercase or underscore-prefixed to avoid constant matching. The pin operator (^) matches against existing variable values rather than capturing.

expected = 42

case 42
in ^expected
  puts "Matched expected value"  # => "Matched expected value"
end

case { status: "success", data: info }
in { status: "success", data: }
  puts "Success: #{data}"  # => "Success: info"
end

Guard clauses add conditional logic to patterns through if and unless expressions. Guards evaluate after pattern matching succeeds, allowing validation of captured values or external conditions.

case { temperature: 75, humidity: 60 }
in { temperature: t, humidity: h } if t > 70 && h < 70
  puts "Comfortable weather"  # => "Comfortable weather"
end

numbers = [1, 2, 3, 4, 5]
case numbers
in [first, *rest] if rest.sum > first * 2
  puts "Rest sum dominates: #{rest.sum} > #{first * 2}"
  # => "Rest sum dominates: 14 > 2"
end

Advanced Usage

Nested patterns match complex data structures by combining array, hash, and object patterns. Deep nesting requires careful attention to pattern structure and variable scope.

data = {
  users: [
    { id: 1, profile: { name: "Alice", preferences: { theme: "dark" } } },
    { id: 2, profile: { name: "Bob", preferences: { theme: "light" } } }
  ],
  metadata: { version: 2, created: "2024-01-01" }
}

case data
in {
  users: [
    { id: user_id, profile: { name:, preferences: { theme: } } },
    *
  ],
  metadata: { version: v }
} if v >= 2
  puts "User #{user_id}: #{name} prefers #{theme} theme"
  # => "User 1: Alice prefers dark theme"
end

Custom object patterns through deconstruct_keys enable domain-specific matching. Methods should return hash-like objects with symbol keys matching expected pattern attributes.

class Request
  attr_reader :method, :path, :headers, :body
  
  def initialize(method, path, headers = {}, body = nil)
    @method, @path, @headers, @body = method, path, headers, body
  end
  
  def deconstruct_keys(keys)
    { method: @method, path: @path, headers: @headers, body: @body }
  end
end

request = Request.new("POST", "/api/users", 
                      { "Content-Type" => "application/json" },
                      '{"name": "Charlie"}')

case request
in Request[method: "POST", path: /^\/api\//, headers: { "Content-Type" => ct }]
  puts "API POST request with #{ct}"
  # => "API POST request with application/json"
end

Pattern composition creates reusable matching logic through method extraction and pattern variables. Complex patterns benefit from decomposition into smaller, testable components.

def api_request_pattern(path_prefix)
  ->(req) {
    case req
    in { method: "GET" | "POST", path: /^#{Regexp.escape(path_prefix)}/ }
      true
    else
      false
    end
  }
end

def authenticated_pattern
  ->(req) {
    case req
    in { headers: { "Authorization" => /^Bearer / } }
      true
    else
      false
    end
  }
end

request = { method: "POST", path: "/api/users", headers: { "Authorization" => "Bearer token123" } }

case request
in req if api_request_pattern("/api").call(req) && authenticated_pattern.call(req)
  puts "Authenticated API request"  # => "Authenticated API request"
end

Alternative patterns using the | operator match multiple possibilities within single pattern branches. Alternatives must bind the same variables to maintain consistent scope.

case { event: "user_login", user_id: 123, timestamp: Time.now }
in { event: "user_login" | "user_signup", user_id:, timestamp: }
  puts "User activity for ID #{user_id} at #{timestamp}"
end

case [1, "two", 3.0]
in [Integer => a, String => b, Float => c] | [String => a, Integer => b, Symbol => c]
  puts "Mixed types: #{a.class}, #{b.class}, #{c.class}"
  # => "Mixed types: Integer, String, Float"
end

Common Pitfalls

Variable binding in patterns creates new local variables that shadow existing variables with the same names. This behavior differs from variable assignment and can cause unexpected overwrites.

name = "Original"
data = { user: "Alice" }

case data
in { user: name }  # Creates new local variable, shadows existing
  puts name  # => "Alice"
end

puts name  # => "Alice" (original value lost)

# Use pin operator to match existing variable
name = "Original"
case data
in { user: ^name }  # Matches against existing variable
  puts "Matched original name"
else
  puts "Different name: #{data[:user]}"  # => "Different name: Alice"
end

Partial hash matching can produce unexpected matches when patterns don't account for additional keys. Hash patterns match subsets by default, potentially causing overly permissive matching.

user = { name: "Alice", age: 30, role: "admin", active: false }

case user
in { role: "admin" }
  puts "Admin access granted"  # Matches despite active: false
end

# Add specific exclusion patterns
case user
in { role: "admin", active: false }
  puts "Inactive admin - access denied"  # => "Inactive admin - access denied"
in { role: "admin" }
  puts "Active admin - access granted"
end

Pattern matching precedence follows declaration order, not specificity. Broad patterns placed before specific patterns prevent specific matches from executing.

value = { type: "special", data: [1, 2, 3] }

case value
in { type: String }  # Broad pattern matches first
  puts "Generic type handler"  # => "Generic type handler"
in { type: "special", data: Array }  # Never reached
  puts "Special type handler"
end

# Reorder patterns for correct precedence
case value
in { type: "special", data: Array }
  puts "Special type handler"  # => "Special type handler"
in { type: String }
  puts "Generic type handler"
end

Rest pattern behavior in arrays captures remaining elements but behaves differently in various contexts. Multiple rest patterns in single patterns cause syntax errors.

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

case [1]
in [first, *rest]
  puts "Rest: #{rest}"  # => "Rest: []" (empty array, not nil)
end

# Invalid: multiple rest patterns
# case [1, 2, 3, 4, 5]
# in [*start, middle, *end]  # SyntaxError
# end

NoMatchingPatternError occurs when no patterns match and no else clause exists. This exception type requires explicit handling in production code unlike other control flow constructs.

def process_event(event)
  case event
  in { type: "click", element: }
    handle_click(element)
  in { type: "keypress", key: }
    handle_keypress(key)
  # Missing else clause
  end
rescue NoMatchingPatternError
  logger.warn("Unhandled event type: #{event}")
  default_handler(event)
end

# Better approach with explicit else
def process_event_safe(event)
  case event
  in { type: "click", element: }
    handle_click(element)
  in { type: "keypress", key: }
    handle_keypress(key)
  else
    logger.warn("Unhandled event type: #{event}")
    default_handler(event)
  end
end

Reference

Pattern Matching Operators

Operator Syntax Purpose Example
case/in case value in pattern Multi-pattern matching case [1,2] in [a,b]
in value in pattern Single pattern test [1,2] in [a,b]
=> pattern => variable Pattern with capture [1,2] => [a,b]
^ ^variable Pin operator in ^expected_value
* *rest Splat/rest pattern in [first, *rest]
** **rest Double splat in { a:, **rest }
` ` `pattern1 pattern2`

Array Pattern Syntax

Pattern Matches Variables Example
[a, b, c] Exact length, all elements a, b, c [1, 2, 3]
[a, *rest] At least one element a, rest [1, 2, 3, 4]
[*start, a] At least one element start, a [1, 2, 3, 4]
[a, *middle, b] At least two elements a, middle, b [1, 2, 3, 4]
[] Empty array None []
[a, *] At least one element a [1, 2, 3]

Hash Pattern Syntax

Pattern Matches Variables Example
{ a:, b: } Keys present a, b { a: 1, b: 2, c: 3 }
{ a: value } Key with specific value value { a: "test" }
{ a: String } Key with type match None { a: "text" }
{ **rest } Capture remaining rest { a: 1, b: 2 }
{} Empty hash None {}
{ a:, **nil } Exact key match only a { a: 1 } (not { a: 1, b: 2 })

Guard Clause Syntax

Syntax Purpose Example
if condition Pattern succeeds if condition true in [a, b] if a > b
unless condition Pattern succeeds if condition false in { status: } unless status.nil?
if var.method Call methods on captured variables in [*items] if items.any?

Common Pattern Examples

# Literal matching
case value
in 42
in "hello"  
in true
end

# Type matching
case value
in String
in Integer
in Array
end

# Variable capture
case [1, 2, 3]
in [a, b, c]  # a=1, b=2, c=3
end

# Mixed literal and capture
case { name: "Alice", age: 30 }
in { name: "Alice", age: }  # age=30
end

# Nested structures
case { users: [{ id: 1, name: "Alice" }] }
in { users: [{ id:, name: }] }  # id=1, name="Alice"
end

# Alternative patterns
case value
in String | Symbol
in Integer | Float
end

# Regular expressions
case "hello world"
in /^hello/
end

# Ranges
case 5
in 1..10
end

Error Types

Error Cause Prevention
NoMatchingPatternError No pattern matches, no else clause Add else clause or rescue
SyntaxError Invalid pattern syntax Check pattern structure
NameError Reference to undefined variable in pattern Use existing variables or pin operator

Custom Object Methods

Method Returns Purpose Required For
deconstruct Array Array pattern matching in [a, b] syntax
deconstruct_keys(keys) Hash Hash pattern matching in { a:, b: } syntax

Performance Characteristics

Operation Time Complexity Notes
Literal matching O(1) Direct comparison
Array pattern O(n) Linear in array size
Hash pattern O(k) Linear in pattern key count
Nested patterns O(depth × width) Recursive structure traversal
Guard evaluation Variable Depends on guard condition