The Pin Operator

Welcome back. In the previous lesson, you matched data shapes using case and refined matches with guards. Today, you’ll add the pin operator (^) to match against an existing variable’s value instead of rebinding it. You will also:

  • See pin used in function heads, map keys, lists, and binaries.
  • Contrast pin vs guards for readability and intent.
  • Understand how pin prevents accidental rebinding.
  • Combine pin with recursion for targeted transformations.
  • Learn when pin helps and when it hurts readability.
Rebinding Prevention: Why ^ Matters

Variables in patterns normally rebind. Pin stops that.

Explanation:

  • Without pin, expected is rebound inside the pattern and the first clause matches any value.
  • With pin, the value must equal the previously bound expected, otherwise it falls through.
Match a Specific Value with ^

Explanation:

  • The caret (^) “pins” expected, so the pattern compares value to the current value of expected instead of rebinding expected.
  • Without the pin, expected would be rebound inside the pattern, which is not what we want here.
Replace Items in a List with a Pin Pattern

Using a pin directly in the pattern is often clearer than a guard for equality checks:

Explanation:

  • fn ^old_value -> new_value matches the element only when it equals old_value, no guard needed.
  • This communicates “match this exact value” at the pattern level.

For contrast, here’s the guard version you saw earlier:

Pin in Function Heads, Map Keys, Lists, and Binaries

Pinning shines in function heads and deep patterns.

Notes:

  • In handle/2, the first clause binds id from the first argument’s map, then pins the second parameter to that value.
  • %{^dynamic_key => v} matches only when dynamic_key exists as a key in the map; otherwise the clause doesn’t match.

Note: In function heads, patterns are matched and variables are bound from left to right. This means that in def handle(%{id: id} = item, ^id), the id variable is first bound from the map in the first argument, and then the second argument is matched against that already-bound id using the pin operator. Pinning in a pattern always requires the variable to have been previously bound—otherwise, you’ll get a compile-time error. This left-to-right binding order is crucial for understanding how and when you can use the pin operator in function heads.

Pin vs Guard: Readability and Match Semantics

Two ways to express equality:

Guidance:

  • Prefer pin when matching exact equality as part of the data shape (it’s declarative and fail-fast).
  • Prefer guards when you need computed conditions (e.g., x > 10, String.length(x) == 3, multiple boolean checks).
Combine with Recursion: Replace Only Leading Sentinel Elements

Use pin in list patterns to target a region precisely. Here we replace only the leading occurrences of a sentinel and stop at the first different element.

Example: replace_leading([:x, :x, :x, :y, :x], :x, :z) returns [:z, :z, :z, :y, :x].

Cautionary Notes
  • Don’t overuse pin when a literal is clearer (e.g., match on :ok directly instead of pinning a variable that happens to be :ok).
  • Prefer pin for simple equality as part of a pattern; prefer guards for computed or compound conditions.
  • Avoid pinning deep, complex patterns if it obscures intent; add @doc/@spec to document why you’re pinning dynamic state.
  • Ensure the pinned variable is already bound; pinning an unbound variable in a pattern will raise a compile error.
Summary and Next Steps

You saw how the pin operator locks a variable’s current value into a pattern to prevent rebinding, and how to use it across function heads, maps, lists, and binaries. You contrasted it with guards, learned when each is clearer, and combined pin with recursion to target only leading elements in a list. Now head to the practice section and put the pin operator to work.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal