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.
Variables in patterns normally rebind. Pin stops that.
Explanation:
- Without pin,
expectedis 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.
Explanation:
- The caret (
^) “pins”expected, so the pattern comparesvalueto the current value ofexpectedinstead of rebindingexpected. - Without the pin,
expectedwould be rebound inside the pattern, which is not what we want here.
Using a pin directly in the pattern is often clearer than a guard for equality checks:
Explanation:
fn ^old_value -> new_valuematches the element only when it equalsold_value, no guard needed.- This communicates “match this exact value” at the pattern level.
For contrast, here’s the guard version you saw earlier:
Pinning shines in function heads and deep patterns.
Notes:
- In
handle/2, the first clause bindsidfrom the first argument’s map, then pins the second parameter to that value. %{^dynamic_key => v}matches only whendynamic_keyexists 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.
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).
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].
- 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.
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.
