In a Ruby case/in, what does the ^ pin operator do, and what goes wrong when an existing local variable is used without it?
answer
- bare names always bind
- silent overwrite, always matches
- ^ uses the current value
- ivars, globals, ^(expr) since 3.1
- same value twice in one pattern
basics
~20 sIn a pattern a bare name always binds a new value, so using an existing local like team_id silently overwrites it and matches anything. The pin ^team_id uses the variable's current value as a value pattern (tested with ===) instead; ^@ivar and ^(expr) work too.
solid answer
~40 sInside a pattern, a bare identifier is a **variable pattern**: it matches anything and binds. So with `expected_team = "T1"`, the branch `in {team: expected_team}` matches *every* team and overwrites `expected_team` with the payload's value — the reference shows exactly this trap. The pin operator `^` says "use this value as part of the pattern": `in {team: ^expected_team}` compares with `===` against the current value. Since Ruby 3.1 you can also pin instance, class and global variables (`^@team_id`) and arbitrary expressions in parentheses (`^(config.fetch(:team))`). Pins also express equality *within* one pattern: `in {user: {id:}, mentions: [*, ^id, *]}` binds `id` and then requires that same id to appear among the mentions. Constants such as `ALLOWED_TEAM` need no pin — they are value patterns already.
code
ruby · 18 linesexpected_team = "T1"
event = {team: "T9", text: "hi"}
result = case event
in {team: expected_team, text:} then "accepted"
end
p result # => "accepted" (wrong!)
p expected_team # => "T9" (overwritten)
expected_team = "T1"
result = case event
in {team: ^expected_team, text:} then "accepted"
else "rejected"
end
p result # => "rejected"
self_mention = {user: {id: "U1"}, mentions: ["U7", "U1"]}
p((self_mention in {user: {id:}, mentions: [*, ^id, *]})) # => truego deeper
Remember that a bare name in a pattern captures a new value, and that ^name is how you compare against a variable you already have.
Explain the silent-rebinding trap, pin locals, instance variables and parenthesised expressions, and note that constants need no pin.
Spot missing pins in review as security-relevant when they guard tenancy or authorisation checks, and use a pin on a name bound earlier in the pattern to require equal values.
Treat pattern-matching adoption as a review concern: agree conventions for pins versus guards so that authorisation-relevant comparisons are never left to a bare name.
## Bare names bind — always In a pattern, a lower-case identifier is a **variable pattern**. It matches any value and binds it. This is what makes `in {text:}` or `in [first, *rest]` useful, but it has a sharp edge: it applies even when a local with that name already exists. ```ruby expected_team = "T1" case {team: "T9", text: "hi"} in {team: expected_team, text:} "accepted from #{expected_team}" end # => "accepted from T9" — expected_team was overwritten ``` The branch matched a payload from the wrong workspace and clobbered the variable. Ruby's reference uses the same example and labels the real result "local variable just rewritten". No warning is issued: this is the defined meaning of a bare name in a pattern. ## The pin operator `^name` turns a variable into a **value pattern**: its current value is compared with `===`, as for any value pattern. ```ruby case {team: "T9", text: "hi"} in {team: ^expected_team, text:} "accepted" else "rejected" end # => "rejected" ``` | Pattern entry | Meaning | |---|---| | `team:` | require key, bind its value to `team` | | `team: t` | require key, bind its value to `t` | | `team: expected_team` | require key, **rebind** `expected_team` (the trap) | | `team: ^expected_team` | require key, value must `===` the current `expected_team` | | `team: ALLOWED_TEAM` | constant: a value pattern, no pin needed | | `team: ^@team_id` | pin an instance variable (3.1+) | | `team: ^(settings.fetch(:team))` | pin an arbitrary expression (3.1+) | ## What can be pinned The reference's pattern grammar lists the pinnable value patterns: - `^local_variable` - `^instance_variable`, `^class_variable`, `^global_variable` - `^(expression)` — any expression, in parentheses Ruby 3.0 supported only local variables; 3.1 added the other variable kinds and parenthesised expressions. Method calls must go through `^(...)`: a bare `^settings.team` is not a valid pin. ## Equality inside one pattern A pinned name may refer to a variable bound **earlier in the same pattern**. That expresses "these two positions hold the same value" without a guard: ```ruby case event in {user: {id:}, mentions: [*, ^id, *]} "user mentioned themselves" end ``` The reference's example does the same with a school level: it binds `school:` and then requires `level: ^school` inside a later nested hash. ## Pin versus guard Both compare against outside values, but they differ: 1. A **pin** is part of the pattern, so it participates in structural matching, works inside nested arrays and find patterns, and uses `===`. 2. A **guard** (`in {team:} if team == expected_team`) runs after the pattern matched and can express any boolean condition, but only on `case/in` branches, not on the one-line forms. Prefer the pin for simple equality against a known value; use a guard for comparisons such as `ts > cutoff`. ## Webhook example A router that accepts events only from its own workspace and channel: ```ruby def route(event) case event in {team: ^@team_id, channel: ^(@config.fetch(:channel)), type: "message", text:} handle(text) in {team: ^@team_id} :ignored_other_channel else :foreign_workspace end end ``` Without the pins, the first branch would match every message from every workspace and overwrite nothing useful — or, with local variables, overwrite the very values it was meant to check. ## Checklist - Every existing variable you mean to **compare against** needs `^`. - Constants never need `^`. - Use `^(expr)` for method calls and computations. - Reuse a name bound earlier in the pattern with `^name` to require equal values.
- Why does a constant like ALLOWED_TEAM work in a pattern without a pin?Constants are value patterns in Ruby's pattern grammar: they are evaluated and compared with `===`, never bound. Only lower-case identifiers are variable patterns that bind, so only existing local, instance, class and global variables (and expressions) need `^` to be used as values.
- How do you pin the result of a method call in a pattern?Wrap it in parentheses after the caret: `in {team: ^(settings.fetch(:team))}`. Since Ruby 3.1 `^(expression)` pins any expression; a bare `^settings.team` is not valid pin syntax. Keep such expressions cheap, since they are evaluated while matching.
A bare name in a pattern is an empty labelled box: whatever arrives is dropped in, and the old contents are thrown away. A pinned name is a photograph you hold up at the door: the arrival is compared with the picture and turned away if it differs.
saying these in an interview costs you the question
- Using an existing local variable in a pattern compares against its value.
- Ruby warns when a pattern variable shadows an existing local.
- Constants must be pinned with ^ to be used as values in a pattern.
- The pin operator only works with local variables, even in Ruby 4.0.
- A pinned value is compared with == rather than ===.