The Problem People Feel First
You come to Elixir with an assignment model in your head. You see = and your hands move faster than your brain. Then a match fails, or a function head doesn’t run, and you think you made a mistake.
Pattern matching is not about storing a value. It’s about selecting a shape. When the shape doesn’t fit, the match fails, and that failure is useful. It’s how Elixir chooses which path to take.
The mental model is simple: = is a match operator. Pattern matching selects structure and binds names. The pin operator turns a binding into a test.
Matching Is Selection
Bruce says this in class in a way that changes how you read code:
“The equal operator is the match operator. It makes the thing on the left match the thing on the right.” — Bruce Tate
If the left side is a pattern, Elixir tries to make the right side fit that pattern. If it fits, names on the left become bindings. If it doesn’t, Elixir doesn’t pretend. It just says no.
That “no” is not a bug. It’s selection. You can design patterns that only match the structures you want. When the shape is wrong, the failure is the filter.
Here’s an example with a tuple to help you understand what I’m talking about:
{:ok, value} = {:ok, 42}
# value == 42
{:ok, value} = {:error, :timeout}
# ** (MatchError) no match of right hand side value
In most languages, you’d protect yourself from that error. In Elixir, you lean into it. That match failure is telling you the structure wasn’t what you asked for.
🎯 Join Groxio's Newsletter
Weekly lessons on Elixir, system design, and AI-assisted development -- plus stories from our training and mentoring sessions.
We respect your privacy. No spam, unsubscribe anytime.
Destructuring Is How You Pull Value Out
Once you treat matching as selection, destructuring starts to make sense! You are not mutating data, you are extracting parts from a structure that already exists.
Consider a list. You can pull out head and tail in one move:
[head | tail] = [10, 20, 30]
# head == 10
# tail == [20, 30]
You didn’t “assign” head and tail. You matched a list shape, and Elixir bound names to the pieces. This is why complex data feels manageable in Elixir. You can write down the structure you want, and the language hands you the right parts when the shape fits.
The Pin Operator Is a Test
Now the part people misread. The pin operator (^) is not about preventing reassignment. It turns a binding into a test. It says, “Match against the existing value, don’t rebind it.”
Here’s a small example where the meaning clicks:
status = :ok
case {:ok, 200} do
{^status, code} -> {:matched, code}
{status, code} -> {:rebound, status, code}
end
# => {:matched, 200}
Without the pin, the first clause would always match and rebind status. With the pin, Elixir treats status as a value to compare against. That’s the difference between creating a binding and testing one.
So the pin operator doesn’t “freeze” a variable. It tells the matcher to stop binding and start checking.
Practical Technique: Let Match Failure Choose the Path
Here’s the technique I want you to try today: move pattern matching into function heads or case clauses, and let match failure select the right branch. It keeps the rules close to the data.
Instead of this:
def handle(result) do
if elem(result, 0) == :ok do
value = elem(result, 1)
# your work here
Logger.info("Success: #{value}")
{:ok, value}
else
reason = elem(result, 1)
# your work here
Logger.error("Failed: #{reason}")
{:error, reason}
end
end
Do this with function heads:
def handle({:ok, value}) do
# your work here
Logger.info("Success: #{value}")
{:ok, value}
end
def handle({:error, reason}) do
# your work here
Logger.error("Failed: #{reason}")
{:error, reason}
end
Or with a case statement:
def handle(result) do
case result do
{:ok, value} ->
# your work here
Logger.info("Success: #{value}")
{:ok, value}
{:error, reason} ->
# your work here
Logger.error("Failed: #{reason}")
{:error, reason}
end
end
The pattern matching versions don’t inspect and branch, they declare the shapes they know how to handle. If you pass a shape they don’t recognize, you’ll get a function clause error or a case clause error, which is exactly the signal you want.
A simple rule of thumb: match on structure first, then name the parts. It keeps the code honest and makes failures obvious.
Why This Fits Elixir
Elixir’s philosophy leans toward explicit structure over implicit state. Pattern matching supports that. You describe the shape you are willing to accept, and the runtime enforces it. That helps make impossible states unrepresentable.
This is also why tagged tuples show up everywhere in Elixir. The {:ok, result} and {:error, reason} pattern is a structure that pattern matching can select. We’ll dig into that more in the following weeks. Notice how natural it feels: match the tag, bind the value, and let the shape drive the flow.
Closing
Pattern matching is not assignment. It’s a selection tool that binds names when the structure fits. Match failure is not an error to avoid. It is a signal that the shape wasn’t what you asked for, and you should handle that explicitly.
Once you see = as a match, Elixir becomes simpler. You stop trying to push values into boxes and start choosing shapes. And once you internalize pattern matching, the rest of Elixir’s design (supervision trees, pipelines, tagged tuples) starts making sense because it all builds on the same principle.
We cover this in our Learning Elixir course if you want to see how these pieces connect.
📚 Build Mental Models for Production Elixir
This comes from our Learning Elixir course, where Bruce teaches the mental models behind production architecture decisions through real-world scenarios. Learn to use pattern matching as a design tool so your code stays explicit, maintainable, and correct. Available via monthly subscription -- try it for one month.
— Paulo & Bruce