4.10. Pattern Matching
A pattern describes the shape of a value using the same notation you would use to build it. Morloc matches values against patterns in two ways.
Irrefutable patterns destructure a value into named parts at binding
positions: lambda parameters, function-definition arguments, let left-hand
sides, and do-block ← binds. Every well-typed receiver matches, so these
patterns contain only variable names, wildcards, and structural constructors — no literals, no alternatives. That is what makes them irrefutable.
Refutable patterns dispatch on the shape of a value through a list of
|-clauses. A clause can fail to match, because a literal matches only itself,
so clauses are tried in order and the first that matches wins. Clauses appear
either in a function’s definition, dispatching on its arguments, or in a match
expression, dispatching on any value you hand it.
Patterns covers the related but distinct topic of pattern functions — .0, .[i:j], and friends — which extract and rebuild data rather than bind
names.
4.10.1. Irrefutable patterns
The supported shapes:
-
variable —
xbinds the whole value -
wildcard —
_matches without binding -
tuple —
(x, y)binds each component. Full arity is required; use wildcards for positions you want to ignore, as in(x, _, _) -
record —
{a = x, b = y}binds fields by name. Extra fields are ignored, order does not matter, and the receiver only has to have the keys the pattern mentions. This is structural, or row-polymorphic, matching — the same rule as the.(.a, .b)group getter -
as-pattern —
label@atombindslabelto the whole receiver and destructures further throughatom -
nesting is free:
(x, {a = y, b = _}, q@(l, r))
All four binding sites take them:
-- lambda parameter
first = \ (a, b) -> a
-- function-definition argument
snd (_, y) = y
-- let-binding
demo pair = let (a, b) = pair in a
-- do-block bind
useIt = do
(a, b) <- readPair
a
Records mix in cleanly:
record Pair = Pair { a :: Int, b :: Int }
-- field-polymorphic: any record with keys 'a' and 'b'
pickA {a = x, b = _} = x
-- nested
combine :: (Int, Pair) -> Int
combine (n, {a = p, b = q}) = n + p + q
$ ./match pickA '{"a":5,"b":6}'
5
$ ./match combine '[1,{"a":2,"b":3}]'
6
Wildcards
_ matches without binding. In a let left-hand side or a do bind, the
right-hand side is still evaluated, so effects still fire; in a lambda or
function-argument position the slot is accepted and discarded.
-- discard the first tuple element
snd (_, y) = y
-- do-bind: the effect fires, the value is discarded
main = do
_ <- setup
work
As-patterns
label@atom binds label to the whole receiver and destructures through
atom, so both are in scope:
tag p@(x, y) = (p, x + y)
$ ./match tag '[3,4]'
[[3,4],7]
There must be no whitespace around @. Write p@(x, y), never p @ (x, y):
pmx.loc:6:7: unexpected operator '@'
|
6 | tag p @ (x, y) = (p, x + y)
| ^
This matches the tight-binding style of Morloc’s other qualifier operators — . for namespaces, : for group labels. An @name in a fresh position (start
of line, after whitespace, after a delimiter) still means an intrinsic such as
@stdout.
Record patterns on let and do need parentheses
let and do both accept an explicit { right after the keyword as an
alternative to layout-based blocks:
let { a = 1; b = 2 } in a -- explicit-brace form of a two-binding let
do { readValue; useIt } -- explicit-brace form of a do-block
So a record pattern in those positions has to be parenthesized, or its { is
read as the start of a bindings block:
-- required
let ({a = p, b = q}) = mkPair in p
do
({a = p, b = q}) <- fetch
p
Without the parentheses you get a parse error that does not obviously point at the real problem — the parser is inside a bindings block by then and is complaining about the comma:
pmx.loc:9:18: unexpected ','
|
9 | demo = let {a = p, b = q} = mkPair in p
| ^
expected one of: '}', ';'
Function-definition and lambda positions are unaffected: neither \ nor a
function name is a layout keyword, so foo {a = x, b = y} = x and
\ {a = x, b = y} → x parse without parentheses. Tuple and as-patterns on
let and do are also fine unparenthesized, because they do not start with
{.
4.10.2. Refutable patterns
A function can dispatch on the shape of its arguments by giving several
|-clauses instead of one body. Each clause lists one pattern per argument,
then = and a result. Clauses are tried top to bottom, and the first whose
patterns all match wins:
fibonacci :: Int -> Int
fibonacci | 0 = 1
| 1 = 1
| n = fibonacci (n - 1) + fibonacci (n - 2)
A multi-argument function carries one pattern per argument per clause:
ackermann :: Int -> Int -> Int
ackermann | 0 n = n + 1
| m 0 = ackermann (m - 1) 1
| m n = ackermann (m - 1) (ackermann m (n - 1))
$ ./match fibonacci 10
89
$ ./match ackermann 2 3
9
A clause pattern may take any irrefutable shape — variable, wildcard, tuple,
record, as-pattern — plus one more: a literal. An Int, Real, Str, or
Bool value matches only itself, and that is what makes a clause refutable.
There is a second refutable shape, the constructor pattern, which matches one
alternative of a sum type. It is covered in Sum types, together with the
data declaration that creates the constructors, and it works in every position
described here.
greet :: Str -> Str
greet | "en" = "hello"
| "fr" = "bonjour"
| _ = "hi"
$ ./match greet fr
"bonjour"
$ ./match greet de
"hi"
Literals nest inside structural patterns, so you can pin part of a compound value and bind the rest:
-- match a pair whose first element is 0, bind the second
firstZero :: (Int, Int) -> Int
firstZero | (0, n) = n
| (m, n) = m + n
|
|
A literal pattern compiles to an equality test, so the argument’s type
needs an Eq instance in scope — the same requirement as writing x == 0
yourself. The standard library provides Eq for the primitive types.
|
4.10.3. match expressions
A definition’s clauses dispatch on that definition’s arguments. When the value
you want to dispatch on is one you computed, there is no argument to hang
clauses on. match takes the value directly, then the same |-clause list:
statusText :: Int -> Str
statusText code = match code // 100
| 2 = "success"
| 3 = "redirect"
| 4 = "client error"
| 5 = "server error"
| _ = "unknown"
$ ./match statusText 200
"success"
$ ./match statusText 404
"client error"
$ ./match statusText 999
"unknown"
The clauses match the status class, code // 100, not code. A clause list
cannot: it only sees the argument, so this would mean inventing a second
function that takes the class, naming it, and calling it.
The scrutinee is a full expression, so match code // 100 needs no
parentheses. Anywhere an expression is allowed, a match is allowed — in one
branch of a guard:
describeStatus :: Int -> Str
describeStatus code
? code < 100 = "not a status code"
: match code // 100
| 2 = "success"
| 4 = "client error"
| 5 = "server error"
| _ = "other"
$ ./match describeStatus 42
"not a status code"
$ ./match describeStatus 503
"server error"
or inside a lambda, where there is no definition head at all:
labels :: [Int] -> [Str]
labels = map (\c -> match c // 100 | 2 = "ok" | _ = "not ok")
$ ./match labels '[200,404,201]'
["ok","not ok","ok"]
A do-block statement is the other common home; matching on a bound result is
how a fallible call is consumed, which Failure and recovery covers along
with the type it produces.
When you are dispatching on a plain argument, keep the clause form. It says the same thing with less punctuation, and it is what the rest of this chapter uses.
Where a clause list ends
A match has no closing keyword. Its clause list runs until something appears
that cannot begin another clause, and | can always begin another clause. So
a comma, a closing bracket, the : of a guard, the end of a do statement,
where, and the end of a definition all end the list, and two match
expressions sit side by side in a tuple with no help:
pairUp :: Int -> Int -> (Str, Str)
pairUp x y = (match x | 0 = "a" | _ = "b", match y | 0 = "c" | _ = "d")
What does not end the list is a | belonging to something enclosing. A
match written inside another match’s arm swallows that outer arm’s
remaining clauses, and the error lands on the outer `match, which has now
lost its catch-all:
nest2.loc:4:14: `|` patterns for 'match' are not exhaustive; a literal pattern cannot cover its type, so add a final catch-all clause (a variable or '_')
|
4 | nested x y = match x
| ^
The same happens inside a definition’s clause body, where the inner match
absorbs the next clause of the definition. There the clause it swallowed has
one pattern per argument, so the error names the arity rule instead:
nest.loc:4:14: each `match` clause takes exactly one pattern, but this one has 2
|
4 | both | 0 y = match y | 1 = "a" | _ = "b"
| ^
Parenthesize the inner match and both compile:
both :: Int -> Int -> Str
both | 0 y = (match y | 1 = "a" | _ = "b")
| x _ = "c"
Parentheses are also required to pass a match as an argument, since it is
not an atom:
paren.loc:6:21: unexpected 'match'
|
6 | noParens x = double match x | 0 = 1 | _ = 2
| ^
Write double (match x | 0 = 1 | _ = 2) instead.
Each match clause takes exactly one pattern, because there is one value
being matched. That is the only structural difference from a definition’s
clause list; the patterns themselves, the top-to-bottom order, and the
exhaustiveness requirement below are the same.
Why there is no closing keyword
Languages with this construct usually bracket it: ML and Haskell write case e
of and close the alternatives with layout or braces, Rust and Scala use {}.
Morloc reuses the |-clause list it already has for definitions instead,
which keeps one notation for one idea and costs a terminator.
The cost is the case above. A clause list that ends at "the next thing that cannot be a clause" is unambiguous to parse — the grammar resolves the conflict by continuing the innermost list, which is what greedy gathering means — but it is not always what a reader expects when two lists are adjacent, and indentation does not disambiguate. Parentheses do, and are the only tool for it.
4.10.4. Exhaustiveness
Every clause of a term belongs to one definition and the last clause is the
fall-through, so a |-match must be exhaustive. That holds when the final
clause is irrefutable — a variable or a _ catch-all:
classify :: Int -> Str
classify | 0 = "zero"
| _ = "nonzero"
or when the clauses of a single Bool argument already cover both cases:
invert :: Bool -> Bool
invert | True = False
| False = True
Anything else is rejected at compile time, with the fix named:
pmx.loc:6:1: `|` patterns for 'stuck' are not exhaustive; add a final catch-all clause (a variable or '_')
|
6 | stuck | 0 = "a"
| ^
A match is held to the same requirement, and its clauses are reported against
the word match rather than a definition’s name:
mne.loc:4:16: `|` patterns for 'match' are not exhaustive; a literal pattern cannot cover its type, so add a final catch-all clause (a variable or '_')
|
4 | sizeLabel xs = match (size xs)
| ^
4.10.5. Guards inside a clause
A clause body may itself be a ?/: guard (see Conditionals), so one
definition can match on an argument’s shape and then branch on a condition.
Variables bound by the clause pattern are in scope in the guard:
foo :: Int -> Int
foo | 0 = 0 -- literal-pattern clause
| x ? x < 10 = 1 -- variable pattern, guard as the body
: 2
$ ./match foo 0
0
$ ./match foo 5
1
$ ./match foo 50
2