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) === 5 → true |
Regexp | Match check | /abc/ === "abcdef" → true |
Class | Instance check | String === "text" → true |
Proc | Call with argument | proc { |x| x > 0 } === 5 → true |
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 |