Pattern matching: the = you've been reading wrong

LLM-authored, human-reviewed

Foundations

In JavaScript, = assigns: it takes a value on the right and stores it in a variable on the left. In Elixir, the same symbol means something else entirely. = is a match: it asks whether the two sides agree, and the variables on the left get bound to whatever makes them agree. Read = this way and Elixir code becomes easier to follow. It also explains the recursion exercises on this site, where base cases and recursive steps use patterns instead of if statements.

The match, not the assignment

x = 5

It looks like assignment. It is a match: the left side, a variable, matches anything, so the match succeeds and binds x to 5. That is why it looks like assignment. A lone variable matches anything, so the general match machinery acts like assignment in this one case. The difference appears when the left side is not a bare variable:

{1, x} = {1, 2}   # succeeds: x = 2
{1, x} = {2, 2}   # fails: 1 does not match 2

The left side is a pattern: a shape with holes. The match checks whether the right side has that shape, then binds the holes. This is how the language makes decisions. Where JavaScript writes if (a === 1) { ... }, Elixir matches the value against the pattern 1. Where JavaScript writes const x = arr[0], Elixir matches the list against [head | tail] and gets both the first element and the rest in one step.

Clauses are ordered dispatch

The recursion exercises’ solutions are two lines:

def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)

Each def is a clause, and each clause’s arguments are patterns. When you call sum([1, 2, 3]), Elixir tries the clauses in order: does [1, 2, 3] match []? No. Does it match [head | tail]? Yes - head = 1, tail = [2, 3] - so that clause runs. A call to sum([]) matches the first clause immediately and never reaches the second.

Order matters. Put the base case first because the first matching clause wins. If you put the recursive clause first, sum([]) would match it, try to peel a head off the empty list, and crash. Clause order is the function’s logic: check the base case before the step, just as an induction proof states the base case before the inductive step. That is why the site’s recursion exercises insist on “base case first”: pattern matching enforces the rule.

Destructuring: one match, several bindings

The pattern [head | tail] does three things in one step: it checks that the list is non-empty, binds head to the first element, and binds tail to the rest. The same idea works with nested shapes:

{name, {age, city}} = {"ana", {29, "lisbon"}}

One match pulls the whole structure apart and names every piece. The recursion exercises use this constantly. member/2 needs the head to compare and the tail to recurse, and one pattern gives it both:

def member([x | _tail], x), do: true
def member([_head | tail], x), do: member(tail, x)

The second clause does something a variable alone cannot: it uses the same variable name twice. [x | _tail], x matches only when the head and the second argument are the same value. In an assignment language you would write a check. Here, the pattern is the check. The underscore names - _tail, _head - are the pattern for “some value I do not need”.

The two halves of the recursion skeleton

Pattern matching makes the base-case/step skeleton of recursion literal. The base case is a pattern for the smallest input ([]), the step is a pattern for the general shape ([head | tail]), and Elixir dispatches between them by matching. There is no if anywhere in sum/1; the pattern makes the decision. The induction article’s base case and inductive step have the same relationship: the base pattern, the general pattern, and the guarantee that every input matches exactly one of them.

The pattern [head | tail] also carries the descent. Matching it gives you the tail, and recursing on the tail is what guarantees termination - the same smaller-input descent the recursion article describes. Pattern matching and recursion are not two ideas that happen to coexist in Elixir. The language was built so that the shape of the data and the shape of the code are the same shape.

Why this matters here

The recursion write exercises are pattern matching lessons wearing a recursion costume: write the base clause, write the step clause, put the base first. The induction bridge exercises show the same structure from the proof side. And the reading-elixir-as-a-js-developer article - the on-ramp that gets you from JavaScript to this site’s Elixir -

  • ends where this article begins: the = sign is not what you thought, and every Elixir function you read from now on is a set of patterns waiting to match.

Where to go next

Related exercises

← Back to articles