CrackedRuby logo

CrackedRuby

Pin Operator in Pattern Matching

Overview

The pin operator (^) in Ruby pattern matching prevents variable binding and instead matches against the current value of an existing variable. When Ruby encounters ^variable in a pattern, it compares the matched value against the variable's current content rather than assigning the matched value to a new variable binding.

Pattern matching in Ruby uses the case/in syntax, where patterns can contain literals, variable bindings, or pinned variables. Without the pin operator, Ruby treats identifiers in patterns as new variable bindings. The pin operator changes this behavior by referencing existing variable values.

x = 10
case [5, 10]
in [a, ^x]  # Matches because second element equals x (10)
  puts "Found #{a} and #{x}"
end
# => Found 5 and 10

The pin operator works with local variables, instance variables, class variables, and global variables. Ruby evaluates the pinned expression at pattern matching time, creating a constraint that the matched value must equal the pinned variable's current value.

@value = "test"
case ["hello", "test"]
in [greeting, ^@value]
  puts "Greeting: #{greeting}"
end
# => Greeting: hello

Pattern matching with pin operators follows Ruby's standard variable scoping rules. The pinned variable must exist in the current scope when the pattern matching executes, or Ruby raises a NameError.

Basic Usage

Pin operators compare matched values against existing variable content. The most common usage involves matching array or hash elements against previously defined values.

expected_status = 200
response = { status: 200, body: "OK" }

case response
in { status: ^expected_status, body: }
  puts "Success with body: #{body}"
in { status: error_code, body: }
  puts "Error #{error_code}: #{body}"
end
# => Success with body: OK

Ruby supports pinning different variable types within the same pattern. Each pinned variable creates an independent constraint that the pattern must satisfy.

min_score = 80
max_score = 100
student_scores = [85, 92, 78, 96]

student_scores.each do |score|
  case score
  in ^min_score..^max_score
    puts "#{score} is in acceptable range"
  else
    puts "#{score} is outside acceptable range"
  end
end
# => 85 is in acceptable range
# => 92 is in acceptable range
# => 78 is outside acceptable range
# => 96 is in acceptable range

The pin operator works with complex nested structures. Ruby evaluates each pinned variable independently when checking pattern matches.

user_id = 42
role = "admin"

users_data = [
  { id: 42, role: "admin", name: "Alice" },
  { id: 43, role: "user", name: "Bob" },
  { id: 42, role: "user", name: "Charlie" }
]

users_data.each do |user|
  case user
  in { id: ^user_id, role: ^role, name: }
    puts "Found admin user: #{name}"
  in { id: ^user_id, role: other_role, name: }
    puts "Found user #{name} with role: #{other_role}"
  else
    puts "No match for user: #{user}"
  end
end
# => Found admin user: Alice
# => No match for user: {:id=>43, :role=>"user", :name=>"Bob"}
# => Found user Charlie with role: user

Pin operators accept method calls and complex expressions. Ruby evaluates these expressions once during pattern matching setup.

class Config
  def self.max_retries
    3
  end
end

attempts = [1, 2, 3, 4]

attempts.each do |attempt|
  case attempt
  in ^Config.max_retries
    puts "Maximum retries reached"
  in n if n < Config.max_retries
    puts "Attempt #{n}, continuing"
  else
    puts "Exceeded maximum retries"
  end
end
# => Attempt 1, continuing
# => Attempt 2, continuing
# => Maximum retries reached
# => Exceeded maximum retries

Advanced Usage

Pin operators combine with Ruby's pattern matching features to create sophisticated matching logic. Ruby evaluates complex pinned expressions including method chains, arithmetic operations, and conditional statements.

class DatabaseConnection
  attr_reader :host, :port, :database

  def initialize(host, port, database)
    @host, @port, @database = host, port, database
  end

  def connection_string
    "#{@host}:#{@port}/#{@database}"
  end
end

primary_db = DatabaseConnection.new("localhost", 5432, "production")
replica_db = DatabaseConnection.new("replica", 5432, "production")

connections = [
  { type: "primary", host: "localhost", port: 5432, db: "production" },
  { type: "replica", host: "replica", port: 5432, db: "production" },
  { type: "cache", host: "localhost", port: 6379, db: "0" }
]

connections.each do |conn|
  case conn
  in { type: "primary", host: ^primary_db.host, port: ^primary_db.port, db: ^primary_db.database }
    puts "Primary database connection validated"
  in { type: "replica", host: ^replica_db.host, port: ^replica_db.port, db: ^replica_db.database }
    puts "Replica database connection validated"
  in { type:, host:, port:, db: }
    puts "Unknown connection type #{type}: #{host}:#{port}/#{db}"
  end
end
# => Primary database connection validated
# => Replica database connection validated
# => Unknown connection type cache: localhost:6379/0

Pattern matching with pin operators supports guard clauses and additional constraints. Ruby processes pinned variables before evaluating guard conditions.

def process_api_response(response, expected_codes: [200, 201], max_retries: 3)
  retry_count = 0

  case response
  in { status: ^expected_codes => status, data:, retry_after: } if retry_count < max_retries
    puts "Success #{status}: #{data}"
  in { status: 429, retry_after: delay } if retry_count < max_retries && delay < 60
    puts "Rate limited, retrying after #{delay}s"
    retry_count += 1
  in { status: error_code, message: } if error_code >= 400
    puts "Error #{error_code}: #{message}"
  else
    puts "Unexpected response format: #{response}"
  end
end

# Test different response scenarios
process_api_response({ status: 200, data: "success" })
# => Success 200: success

process_api_response({ status: 429, retry_after: 30 })
# => Rate limited, retrying after 30s

process_api_response({ status: 500, message: "Internal Server Error" })
# => Error 500: Internal Server Error

Ruby allows pinning computed values and method results within pattern matching. The pin operator evaluates expressions once when the pattern matching begins.

class EventProcessor
  def initialize
    @handlers = {}
    @processed_count = 0
  end

  def register_handler(event_type, &block)
    @handlers[event_type] = block
  end

  def process_events(events)
    events.each do |event|
      case event
      in { type: event_type, timestamp: ts } if ts > Time.now - 3600
        case event_type
        in ^@handlers.keys => type
          puts "Processing recent #{type} event"
          @handlers[type].call(event)
          @processed_count += 1
        else
          puts "No handler for event type: #{event_type}"
        end
      in { type:, timestamp: old_timestamp }
        puts "Skipping old #{type} event from #{old_timestamp}"
      else
        puts "Invalid event format: #{event}"
      end
    end
  end
end

processor = EventProcessor.new
processor.register_handler("user_signup") { |e| puts "New user: #{e[:user_id]}" }
processor.register_handler("purchase") { |e| puts "Purchase: $#{e[:amount]}" }

events = [
  { type: "user_signup", timestamp: Time.now, user_id: 123 },
  { type: "purchase", timestamp: Time.now - 7200, amount: 49.99 },
  { type: "invalid_event", timestamp: Time.now }
]

processor.process_events(events)
# => Processing recent user_signup event
# => New user: 123
# => Skipping old purchase event from [timestamp]
# => No handler for event type: invalid_event

Pin operators work with Ruby's splat operators and rest patterns. Ruby evaluates pinned values before applying splat matching logic.

def validate_request_sequence(requests, required_start: "auth", required_end: "logout")
  case requests
  in [^required_start, *middle_requests, ^required_end]
    puts "Valid request sequence with #{middle_requests.length} middle requests"
    middle_requests.each_with_index do |req, idx|
      puts "  #{idx + 1}: #{req}"
    end
    true
  in [^required_start, *rest]
    puts "Missing logout, found: #{rest}"
    false
  in [*start, ^required_end]
    puts "Missing auth, found: #{start}"
    false
  else
    puts "Invalid sequence: #{requests}"
    false
  end
end

# Test different sequences
validate_request_sequence(["auth", "data_fetch", "update", "logout"])
# => Valid request sequence with 2 middle requests
# =>   1: data_fetch
# =>   2: update

validate_request_sequence(["auth", "data_fetch"])
# => Missing logout, found: ["data_fetch"]

validate_request_sequence(["login", "data_fetch", "logout"])
# => Missing auth, found: ["login", "data_fetch"]

Common Pitfalls

Pin operator variables must exist in the current scope when pattern matching executes. Ruby raises NameError when attempting to pin undefined variables, which commonly occurs when variable names are misspelled or out of scope.

def process_user_data(user)
  # expected_role is not defined
  case user
  in { role: ^expected_role, name: }  # NameError: undefined local variable
    puts "Found expected role user: #{name}"
  end
end

# Correct approach - define the variable first
def process_user_data(user)
  expected_role = "admin"
  case user
  in { role: ^expected_role, name: }
    puts "Found expected role user: #{name}"
  end
end

Ruby evaluates pinned expressions once when pattern matching begins, not during each pattern check. This behavior causes confusion when pinned variables reference mutable objects that change during matching.

counter = [1]

values = [1, 2, 3]
values.each do |value|
  case value
  in ^counter.first
    puts "Matched: #{value}"
    counter[0] += 1  # This change doesn't affect remaining matches
  else
    puts "No match: #{value} != #{counter.first}"
  end
end
# => Matched: 1
# => No match: 2 != 1
# => No match: 3 != 1

Pin operators create equality constraints using === comparison, not == comparison. This distinction affects matching behavior with ranges, regular expressions, and classes.

number_range = 1..10
text_pattern = /hello/

test_cases = [5, "hello world", 15]

test_cases.each do |test_case|
  case test_case
  in ^number_range
    puts "#{test_case} matches range"
  in ^text_pattern
    puts "#{test_case} matches pattern"
  else
    puts "#{test_case} matches nothing"
  end
end
# => 5 matches range
# => hello world matches pattern
# => 15 matches nothing

Pinned variables in nested patterns can create unexpected matching behavior when the same variable appears multiple times. Ruby evaluates each pin independently, which may not match developer expectations.

target = "test"

data = {
  primary: { value: "test", backup: "test" },
  secondary: { value: "other", backup: "test" }
}

case data
in { primary: { value: ^target, backup: ^target } }
  puts "Primary has matching values"
in { primary: { value: ^target, backup: }, secondary: { backup: ^target } }
  puts "Primary value matches, both have matching backup"
  puts "Primary backup: #{backup}"
else
  puts "No matches found"
end
# => Primary has matching values

Pin operators don't create new variable bindings, which causes confusion when patterns appear to assign values but actually create constraints. Variables used without pin operators create new bindings that shadow outer scope variables.

original_value = "important"

case ["test", "important"]
in [new_binding, original_value]  # Creates new local variable, shadows outer
  puts "new_binding: #{new_binding}"
  puts "original_value: #{original_value}"  # This is the new local variable
end

puts "outer original_value: #{original_value}"  # Still "important"

# To match against the outer variable, use pin operator
case ["test", "important"]
in [new_binding, ^original_value]  # Matches against outer scope variable
  puts "Matched against outer scope value"
end

Ruby's pattern matching with pin operators interacts unexpectedly with block scope variables. Variables defined in blocks have different scoping rules that affect pin operator behavior.

def find_matching_items(items, &block)
  items.each do |item|
    case item
    in { type: category } if block.call(category)
      # category is only available within this pattern
      puts "Found matching item: #{item}"
    end
  end
end

target_category = "electronics"
items = [
  { type: "electronics", name: "laptop" },
  { type: "books", name: "ruby guide" }
]

find_matching_items(items) do |category|
  # Cannot use ^target_category here in some contexts
  category == target_category
end

Reference

Pin Operator Syntax

Syntax Description Example
^variable Match against local variable value in ^x
^@instance_var Match against instance variable value in ^@name
^@@class_var Match against class variable value in ^@@config
^$global_var Match against global variable value in ^$debug
^method_call Match against method return value in ^calculate_max
^expr.chain Match against chained expression value in ^user.name

Pattern Matching Contexts

Context Pin Support Notes
Array patterns Yes in [^a, ^b, *rest]
Hash patterns Yes in { key: ^value, **rest }
Value patterns Yes in ^expected_value
Range patterns Yes in ^min..^max
Guard clauses No Use variables directly in guards
Alternative patterns Yes in ^a | ^b

Variable Types and Pinning

Variable Type Syntax Availability Example
Local variables ^var Current scope only ^counter
Instance variables ^@var Within instance methods ^@status
Class variables ^@@var Within class/module context ^@@default
Global variables ^$var Everywhere ^$DEBUG
Constants Direct reference No pin needed CONSTANT

Evaluation Timing

Expression Type Evaluation Time Mutability Impact
Simple variables Pattern match start Changes ignored
Method calls Pattern match start Changes ignored
Complex expressions Pattern match start Changes ignored
Block variables Each pattern check Dynamic evaluation

Common Error Patterns

Error Cause Solution
NameError Undefined pinned variable Define variable before matching
NoMatchingPatternError No pattern matches Add catch-all pattern or handle errors
Incorrect matches Using ^ incorrectly Review pin operator placement
Scope issues Variable not in scope Move variable definition or change scope

Comparison Behavior

Pin operators use === (case equality) for matching, not == (equality). This affects behavior with different object types:

Object Type === Behavior Matching Result
Range Contains check (1..10) === 5true
Regexp Match check /abc/ === "abcdef"true
Class Instance check String === "text"true
Proc Call with argument proc { |x| x > 0 } === 5true

Performance Considerations

Aspect Impact Recommendation
Expression evaluation Once per pattern match Pin complex calculations
Variable lookup Minimal overhead Pin frequently accessed variables
Method calls Evaluated once Pin expensive method results
Nested patterns Linear with depth Keep patterns reasonably shallow