Hero image for "Pattern Matching Isn't Syntax Sugar — It's a Different Theory of Control Flow"

Pattern Matching Isn't Syntax Sugar — It's a Different Theory of Control Flow


Lesson 21: What Elixir's approach to branching reveals about how we think about data


Most programmers learn control flow as a sequence of questions: Is this value equal to X? Is it greater than Y? Does this condition hold? The code interrogates data from the outside. Elixir inverts this. You describe the shape of what you expect, and the runtime tells you whether reality matches. That inversion is small enough to miss and significant enough to change how you design programs.

Elixir's pattern matching is the clearest expression of a design philosophy inherited from Erlang and refined for a broader audience. Understanding why it works the way it does — and what it costs — is worth the excavation.


The Problem with Asking Questions About Data

Consider how most languages handle branching on structured data. You receive a value, then write a series of if statements or switch cases that inspect its properties one at a time. The structure of the data and the structure of the code are separate things. You have to mentally translate between them.

As Owais' Places notes in a recent breakdown of Elixir's type system, Elixir takes a different approach: "you model data with tagged tuples, maps, and structs, then branch by shape." The pattern match is the branch. You write the shape you expect, and if the data fits that shape, execution proceeds — and the relevant pieces are already bound to names you can use.

This is what "declarative" means in practice. You declare what success looks like. The runtime handles the matching logic. The code reads like a description of the data, not an interrogation of it.


Shape-Based Dispatch, Not Value-Based Inspection

The practical consequence shows up most clearly in function definitions. In Elixir, you can write multiple clauses of the same function, each matching a different shape of input. A function handling API responses doesn't need an internal if/else tree — it has separate clauses for {:ok, result} and {:error, reason}. The dispatch happens at the function boundary, before the body executes.

This is a design decision with real weight. When control flow lives in the function signature rather than the body, each clause becomes independently readable. You can look at one clause and understand exactly what shape of data it handles, without tracing through a branching tree to figure out which conditions led there.

The Medium essay by Krzyś on BEAM and OTP captures the experience of encountering this after years of conventional languages: the guilt of realizing that a forty-year-old technology had already solved problems you'd been treating as open questions. The BEAM's approach to process isolation and message passing gets most of the attention in that piece, but the same observation applies to pattern matching — it's not a new idea dressed up in modern syntax. It's a mature answer to a question most languages are still asking.


Where the Type System Enters

Pattern matching in Elixir has always worked at runtime — the shape check happens when the code runs, not when it compiles. That's been a deliberate tradeoff: flexibility and expressiveness over compile-time guarantees.

That tradeoff is now shifting. The Elixir Forum's coverage of the BEAM There, Done That podcast episode with researchers Guillaume Duboc and Annette Bieniusa describes the type system arriving in Elixir 1.20 as one that "types the functional core: functions, pattern matching, unions, intersections, negations." Pattern matching is the first thing the new type system reaches for — because it's the primary mechanism through which Elixir programs express what shapes of data they expect.

The system is currently in warning mode rather than rejection mode, following the same philosophy as Dialyzer: when a warning fires, the design goal is that it reflects a definite bug the vast majority of the time. Strict compile-time rejection is a future step. But the direction is clear — the type system is being built around pattern matching, not added on top of it. That's only possible because pattern matching was already doing the structural work.


What This Teaches Programmers Who'll Never Write Elixir

The lesson here generalizes. When you write a switch statement or an if/elif chain, you're asking questions about data. When you write a pattern match, you're describing data. The first approach puts the logic in your code. The second puts the logic in the shape of the data itself.

José Valim, Elixir's creator, built the language to bring Erlang's concurrency model to a wider audience — but the pattern matching came along as part of the package, and it may be the more transferable idea. You don't need Elixir to think in shapes. You need to notice that most of your if statements are really just asking "does this data look like X?" — and that there are languages where you can say that directly.

The next time you find yourself writing nested conditionals to unpack a response object, ask whether you're interrogating data or describing it. The gap between those two framings is where Elixir lives.