Welcome back. In the last lesson, you added guard clauses to function heads. As a reminder, guards let you attach extra conditions to a match. Today, you will use the same ideas inside a case expression. This gives you clean, readable branching based on data shape at runtime.
Explanation:
case user dotries each clause from top to bottom and picks the first match.%{name: name, age: age, role: :admin}matches maps that havename,age, androleequal to:admin. It bindsnameandageand formats the admin message.%{name: name, age: age} when age >= 18matches any map withnameandagewhere the guard confirms adulthood. This is a reminder of guards, now used insidecase.%{name: name, age: age}(no guard) catches the remaining valid user maps and labels them as minors._is the catch‑all for anything that doesn’t fit the expected shape. Order matters: more specific patterns go first.
- Map patterns match subsets: a pattern like
%{name: name, age: age}will match any map that has at least those keys, even if it has extra keys (email,id, etc.). This also matches structs that define those fields (see below). - Because subset patterns can overlap, put more specific clauses first. For example, a struct clause or a clause that matches a specific
roleshould appear before a broader “hasnameandage” clause, otherwise the broader clause will grab the input and the specific one will never run.
Example of overlap and ordering:
If you switch the two clauses above, the second one will never run because the map subset clause will match both maps and structs.
What happens:
"Alice"matches the first clause (role: :admin) ->"Admin Alice is 30 years old"."Bob"matches the second clause (has and ; guard ) -> .
You can consolidate related variants with guards:
Notes on guards here:
inin guards supports lists and ranges on the right side (e.g.,role in [:admin, :owner]orn in 1..10). You can also combine conditions withand/or.- Guard expressions are restricted. Pure operators and a limited set of functions (like
is_integer/1,is_map/1,byte_size/1) are allowed. Avoid calling arbitrary functions in guards.
- Struct vs bare map
- Tagged tuples (common for success/error)
- Binaries/strings (prefix/suffix, sizes)
- Lists (head/tail)
- Variables introduced inside a clause are local to that clause. To use a new variable after the
case, you must bind it in every clause. - Existing variables may be rebound inside a clause; the rebinding persists after the
casefor the clause that ran. Use this intentionally.
Examples:
If no clause matches and you don’t provide a catch‑all (_), Elixir raises a CaseClauseError at runtime:
Add a final _ -> ... clause when unknown or evolving inputs are possible.
- Multiple function heads:
- Prefer when branching solely on the shape/guards of function arguments at the API boundary.
- Clear, performant dispatch; keeps callers simple.
case:- Prefer when you need to match on a value computed at runtime (result of a function, nested field, intermediate pipeline value).
- Good inside a function body to keep branching local.
cond:- Prefer when you’re checking unrelated boolean conditions rather than data shape (e.g., multiple range checks, feature flags).
- Readable for pure predicate chains; no pattern matching.
with:- Prefer for sequencing multiple matches that can fail (e.g.,
{:ok, _}pipelines), short-circuiting on the first mismatch. - Reduces nested
caseexpressions; keep happy-path linear.
- Prefer for sequencing multiple matches that can fail (e.g.,
Rule of thumb: push branching to function heads when it’s about top‑level input shape; use case for runtime values or when mixing shapes; use cond for predicate-only flows; use with to linearize chained matches.
You used case to:
- Match map shapes and bind fields in place, including nested/complex patterns (structs, tuples, binaries, lists).
- Apply guards in a
caseclause to refine a match, including grouping variants withinand combining conditions. - Understand map subset matching and why clause ordering is crucial, especially with overlapping patterns and structs.
- Handle failure explicitly; without a matching clause,
caseraisesCaseClauseError, so include a catch‑all when appropriate. - Work with variable scope: new variables don’t escape a clause unless bound in all clauses; existing variables can be rebound intentionally.
- See pinned matches (
^var) insidecaseto compare against an existing value (you’ll explore pinning more in the next unit).
This approach keeps branching logic readable and robust without nested ifs while giving you precise control over data shapes at runtime.
