Using the Pin Operator

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.

defmodule Matcher do
  def rebind_demo(expected) do
    case 20 do
      expected -> {:rebound, expected}   # This matches anything and rebinds `expected` to 20
      _ -> :no
    end
  end

  def pin_demo(expected) do
    case 20 do
      ^expected -> :equal
      _ -> {:not_equal, expected}        # `expected` remains unchanged
    end
  end
end

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 ^

defmodule Matcher do
  def match_value(value, expected) do
    case value do
      ^expected -> "Match found!"
      _ -> "No match"
    end
  end
end

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:

defmodule Matcher do
  def update_list_with_pin(list, old_value, new_value) do
    Enum.map(list, fn
      ^old_value -> new_value
      other -> other
    end)
  end
end

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:

defmodule Matcher do
  def update_list_with_guard(list, old_value, new_value) do
    Enum.map(list, fn
      x when x == old_value -> new_value
      other -> other
    end)
  end
end

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

Pinning shines in function heads and deep patterns.

defmodule Matcher do
  # Function head: accept only when the map's :id equals the second argument.
  def handle(%{id: id} = item, ^id), do: {:ok, item}
  def handle(_item, _id), do: :mismatch

  # Map key: match a dynamic key that must already be bound.
  def fetch_value(dynamic_key, %{^dynamic_key => v}), do: {:ok, v}
  def fetch_value(_, _), do: :error

  # List: check whether a list starts with a sentinel value.
  def starts_with?(sentinel, [^sentinel | _]), do: true
  def starts_with?(_, _), do: false

  # Binaries: accept only when the binary begins with a given prefix.
  def split_prefix(prefix, <<^prefix::binary, rest::binary>>), do: {:ok, rest}
  def split_prefix(_, _), do: :nomatch
end

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:

defmodule Matcher do
  def eq_with_pin(v, expected) do
    case v do
      ^expected -> :eq
      _ -> :neq
    end
  end

  def eq_with_guard(v, expected) do
    case v do
      x when x == expected -> :eq
      _ -> :neq
    end
  end
end

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.

defmodule Matcher do
  def replace_leading(list, sentinel, new) do
    case list do
      [^sentinel | t] -> [new | replace_leading(t, sentinel, new)]
      _ -> list
    end
  end
end

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