Pattern Matching in Elixir
In most languages, = means assignment: the right side's value is stored into the variable on the left. In Elixir, = is the match operator, it attempts to make the left-hand pattern structurally match the right-hand value, binding any unbound variables along the way. If the shapes or literal values don't line up, Elixir raises a MatchError at runtime instead of silently proceeding. This single idea, that = checks structure rather than blindly overwriting, is what makes destructuring, function clauses, case, and with all work the same underlying way throughout the language.
Cricket analogy: The match operator is like a third umpire review, it doesn't just record whatever the fielder claims, it checks whether the claimed shape, out or not out, actually matches the replay evidence, and only binds the final decision if it fits.
Destructuring Lists and Tuples
Because = matches structure, it can pull values out of compound data in one step: {status, message} = {:ok, "saved"} binds status to :ok and message to "saved" simultaneously, and [first | rest] = [1, 2, 3] binds first to 1 and rest to [2, 3]. Elixir's idiomatic {:ok, result} and {:error, reason} tuples exist specifically so callers can pattern match on the first element to branch on success or failure. When you need a pattern to match against a variable's current value instead of rebinding it, the pin operator, ^variable, forces that comparison, which is essential inside functions where you don't want an existing variable silently overwritten.
Cricket analogy: Matching {:ok, result} versus {:error, reason} is like a DRS review returning either {:umpires_call, "not out"} or {:out, "lbw"}, the fielding side immediately branches its reaction based on which tagged outcome comes back.
iex> {status, message} = {:ok, "File saved"}
iex> status
:ok
iex> [first | rest] = [10, 20, 30]
iex> first
10
iex> rest
[20, 30]
iex> expected = 5
iex> ^expected = 5
5
iex> ^expected = 6
** (MatchError) no match of right hand side value: 6Because = is a match, not an assignment, 1 = x is valid Elixir as long as x is already bound to 1, the reverse of what most languages allow. This makes it a handy way to assert a value mid-pipeline: some_function(x) |> (fn {:ok, result} = _tagged -> result end).()
Pattern Matching in Function Heads
Elixir functions can be defined as several clauses that share a name and arity but differ in their argument patterns, and Elixir tries each clause's pattern against the call's arguments from top to bottom, running the first one that matches. This replaces long if/else or switch chains found in other languages: def handle({:ok, data}), do: ... and def handle({:error, reason}), do: ... are two completely separate clauses, and the runtime automatically dispatches to the correct one based on the shape of whatever tuple is passed in. Guard clauses, added with when, layer extra boolean conditions onto a pattern, for example def charge(amount) when amount > 0, letting a clause require both a shape match and a runtime condition before it's selected.
Cricket analogy: Multiple function clauses matching different patterns is like a bowling coach having a separate drill for a yorker, a bouncer, and a googly, the right drill is selected automatically based on which delivery type is being practiced.
defmodule Parser do
def handle({:ok, data}), do: "Got: #{data}"
def handle({:error, reason}), do: "Failed: #{reason}"
def charge(amount) when amount > 0 do
{:ok, amount}
end
def charge(_amount), do: {:error, :invalid_amount}
end
iex> Parser.handle({:ok, "hello"})
"Got: hello"
iex> Parser.handle({:error, :not_found})
"Failed: not_found"
iex> Parser.charge(-5)
{:error, :invalid_amount}Pattern Matching in case, cond, and with
The case expression tests a single value against a series of patterns in order and executes the body of the first branch that matches, making it the natural way to branch on the shape of a value returned by another function. When a sequence of operations must each succeed before moving to the next, nesting case inside case quickly becomes unreadable, which is exactly the problem the with special form solves: it chains several <- match steps in a flat block and jumps straight to an else clause the moment any single step fails to match, without needing to unwind manually through several layers of nested case statements.
Cricket analogy: with chaining multiple match steps is like a DRS appeal that must pass several sequential checks, a no-ball check, then edge detection, then ball tracking, if any single check fails, the whole review stops right there and reverts to the on-field decision.
def process(input) do
case Integer.parse(input) do
{number, ""} -> {:ok, number}
{_number, _rest} -> {:error, :trailing_characters}
:error -> {:error, :not_a_number}
end
end
def checkout(cart_id) do
with {:ok, cart} <- fetch_cart(cart_id),
{:ok, total} <- calculate_total(cart),
{:ok, receipt} <- charge_card(total) do
{:ok, receipt}
else
{:error, reason} -> {:error, reason}
end
endWithout the pin operator ^, a variable on the left side of = always rebinds, even if it already has a value: x = 5 followed by x = 10 quietly rebinds x to 10 rather than raising a MatchError. Only ^x = 10 would fail while x is still bound to 5, which is why the pin operator matters most inside function clauses matching against arguments that were bound earlier in the same scope.
- The = operator in Elixir is the match operator: it makes the left pattern match the right value, binding unbound variables.
- If the shapes don't align, Elixir raises a MatchError at runtime.
- The pin operator ^ forces a variable to be matched against its current value instead of being rebound.
- [head | tail] destructures lists; {a, b} destructures tuples by position.
- Function definitions can have multiple clauses distinguished purely by argument patterns, dispatched top to bottom.
- case tests a value against a series of patterns, running the first branch that matches.
- with chains multiple pattern matches, short-circuiting to an else branch on the first failure.
Practice what you learned
1. What does the = operator do in Elixir?
2. What happens when {a, b} = {1, 2, 3} is evaluated?
3. What is the purpose of the pin operator ^?
4. In def handle({:ok, data}) and def handle({:error, reason}), how does Elixir decide which clause runs?
5. What does with provide that a set of nested case statements does not?
Was this page helpful?
You May Also Like
Elixir Data Types
A tour of Elixir's built-in data types, integers, floats, atoms, booleans, strings, lists, tuples, and maps, and how immutability shapes the way they behave.
What Is Elixir?
An introduction to Elixir, a functional, concurrent programming language built on the Erlang VM for building scalable and fault-tolerant applications.
Your First Elixir Program
Write, run, and understand your first Elixir program, from a standalone script to a proper Mix module with functions and a test.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics