Skip to content

Pattern matching

case is the elimination form for sum types and lists; a single-constructor type may also be destructured where a parameter binds it (see patterns in argument position). For the branching if form see functions.

case … of

type Role = Web | Db
glyphsFor : Role -> List Glyph
glyphsFor role =
case role of
Web -> [ aptPackage { name = "nginx" } ]
Db -> [ aptPackage { name = "postgresql" } ]

An arm is pattern -> expr. The arms of a case are laid out by the offside rule — each arm on its own line, indented past of. A binder in a pattern is in scope in that arm’s body.

Pattern forms

PatternMatchesExample
variableanything; binds itn
wildcard _anything; binds nothing_
Int literalan equal integer0, -1
Char literalan equal Char'a'
String literalan equal String"nginx"
constructora value of that constructorJust x, Web
empty list[][]
consa non-empty list(x :: xs)
list literala list of fixed length[a, b]
tuplea tuple, element-wise(a, b), (a, b, c)
unit()()

An integer literal pattern is typed number, not hard Int, so it may match a Float scrutinee just as an integer literal expression may; a leading - is part of the pattern (-1).

A Float literal pattern is a compile error: IEEE-754 equality is unreliable, so — like Elm — Emet refuses to match on it and steers you to the comparison operators (<, >, <=, >=) in an if instead. See functions for if.

Constructor patterns

Upper p1 p2 … matches a constructor applied to sub-patterns. Constructors of built-in sums (Just / Nothing, True / False, LT / EQ / GT) and of user type declarations both work:

unwrap : Maybe Int -> Int
unwrap m =
case m of
Just n -> n
Nothing -> 0

Constructors imported from another module with Type(..) match too, and the exhaustiveness checker sees the imported type’s full constructor set. See modules.

A built glyph or filesystem entry is matchable: AptPackage, SystemdService, Filesystem, LineInFile, and the entry tags File, Directory, Symlink are match-only PascalCase patterns. See the four glyphs.

List patterns

[] matches the empty list; (x :: xs) matches a non-empty list, binding its head and tail. A [a, b, c] literal matches a list of exactly that length and desugars to nested :: ending in [].

firstOr : Int -> List Int -> Int
firstOr fallback xs =
case xs of
[] -> fallback
(x :: _) -> x

List is checked as a two-constructor sum ([] and ::), so a case on a list must cover both.

Tuple patterns

(a, b) and (a, b, c) match a tuple element-wise; () matches unit. Nested patterns compose, so (Just a, (b, c)) destructures a pair of a Maybe and a tuple in one arm.

A tuple has a single shape, so a tuple case needs no catch-all when its element patterns are exhaustive — it is non-exhaustive only when some element column is.

describe : (Bool, Bool) -> String
describe pair =
case pair of
(True, b) -> "first"
(False, True) -> "second"
(False, False) -> "neither"

Exhaustiveness and redundancy

A non-exhaustive case — one that fails to cover some value — is a compile error, reported with the missing constructors. A redundant arm — one that can never match because earlier arms already cover it — is also a compile error. Both are checked at compile time, so no arm falls through at runtime.

Patterns in argument position

A parameter of a declaration, a let binding, or a lambda may be a constructor pattern, so a value destructures where it binds:

type Config = Config { domain : String, port : Int }
domainOf : Config -> String
domainOf (Config spec) = spec.domain
withPort : Int -> Config -> Config
withPort p (Config spec) = Config { spec | port = p }
labels : List Config -> List String
labels = List.map (\(Config spec) -> spec.domain)

A parameter is a binder or a parenthesized constructor application, and nothing else. Every other pattern form on this page — tuple, unit, list, literal, and a constructor nested inside another — is a parse error in argument position, and a nullary constructor still needs its parentheses (f (Unit) = …, not f Unit = …).

The constructor must also be the only constructor of its type, which is what the narrow grammar exists to guarantee. A parameter has no sibling arms to cover the values it misses, so a pattern that can fail there would be a partial function — the thing exhaustiveness checking exists to prevent, reached by writing a binding instead of a branch. Take the whole value as a parameter and branch on it with case … of, which is checked:

nameFor : Maybe String -> String
nameFor m =
case m of
Just name -> name
Nothing -> "anonymous"

An excluded form reports a general parse error at the parameter — f (a, b) = … says found 'a' expected an expression — rather than naming argument position as the thing that does not support it.

See also