Pattern Matching in 'case' Statements

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.

Classifying Users with 'case'

Explanation:

  • case user do tries each clause from top to bottom and picks the first match.
  • %{name: name, age: age, role: :admin} matches maps that have name, age, and role equal to :admin. It binds name and age and formats the admin message.
  • %{name: name, age: age} when age >= 18 matches any map with name and age where the guard confirms adulthood. This is a reminder of guards, now used inside case.
  • %{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 Subset Matching and Why Ordering is Crucial
  • 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 role should appear before a broader “has name and age” 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 ) -> .
Grouping Variants with Guards

You can consolidate related variants with guards:

Notes on guards here:

  • in in guards supports lists and ranges on the right side (e.g., role in [:admin, :owner] or n in 1..10). You can also combine conditions with and/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.
Matching Other Data Shapes Inside 'case'
  • Struct vs bare map
  • Tagged tuples (common for success/error)
  • Binaries/strings (prefix/suffix, sizes)
  • Lists (head/tail)
Variables: Scoping and Rebinding in 'case'
  • 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 case for the clause that ran. Use this intentionally.

Examples:

Failure Modes When No Clause Matches

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.

Choosing Between Matching Methods
  • 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 case expressions; keep happy-path linear.

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.

Summary and Next Steps

You used case to:

  • Match map shapes and bind fields in place, including nested/complex patterns (structs, tuples, binaries, lists).
  • Apply guards in a case clause to refine a match, including grouping variants with in and 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, case raises CaseClauseError, 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) inside case to 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.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal