Pattern Matching
By Kaz Zhou, September 2020. Revised May 2021
Patterns take on many appearances, such as:
- Constants:
150
- Variables:
x
- Wildcard:
_
- Tuples:
(true, _)
- Constructors (which may contain other patterns):
- Lists:
x::xs
- Other datatypes:
Node(L,x,R)
- Lists:
Patterns can be matched against values to form bindings. Consider the following declaration:
val (x,y) = (1,2)
The result is that 1
gets bound to x
, and 2
gets bound to y
.
Pattern matching may fail. For example, the following raises exception Bind
.
val 10 = 9
Besides val
declarations, pattern matching is also used in function declarations, lambda expressions, and case expressions.
Function declarations
fun fact 0 = 1
| fact n = n * fact(n-1)
The function fact
is given an int
as input. If the input successfully matches the pattern 0
, then the function returns 1
. Otherwise, the input is matched with the pattern n
, binding the input to n
. For example, if we evaluate fact 5
, then 5
is bound to n
, so the expression becomes 5 * fact(4)
.
Each clause of the function declaration tells fact
what it should do, depending on the input. The bar, |
, acts as a separator between the two clauses.
Note that it's important for your patterns to be exhaustive. The above function is fine, because all values of type int
can match with either 0
or n
. However, suppose we had the following function:
fun fiction 1 = 1
| fiction 2 = 2
| fiction 3 = 6
There are many inputs which do not match with either 1
, 2
, or 3
. For example, fiction 4
would raise exception Match
.
A more subtle bug is when patterns are redundant. The following example has the clauses of fact
swapped.
fun factloop n = n * factloop(n-1)
| factloop 0 = 1
The second clause of factloop
never gets executed! When evaluating factloop 0
, SML will try to match 0
to each pattern, in order. Therefore, factloop 0
steps to 0 * factloop(-1)
, because 0
can match to n
. Convince yourself that factloop k
will loop forever for any k
of type int
!
Lambda expressions
(fn [] => false | x::xs => true) [1,2,3]
The lambda expression is similar to a function, as it turns an input into an output. In the example above, [1,2,3]
is the input. It doesn't match with []
, but it does match with x::xs
. Namely, 1
gets bound to x
, and the list [2,3]
gets bound to xs
. As a result of this successful pattern matching, the lambda expression returns true
.
You should still make sure your patterns are exhaustive. For example, the following expression raises exception Match
:
(fn [] => false) [1,2,3]
Case expressions
fun fact' x =
case x of
0 => 1
| n => n * fact' (n-1)
First, note that fact'
does the same thing as fact
. However, it uses an extra case
expression.
Let's consider what happens when we evaluate fact' 5
. First, 5
gets bound to x
. Then, the case
expression tries to match 5
to a pattern. In this scenario, 5
successfully pattern matches with n
, so 5
gets bound to n
. Therefore, fact' 5
evaluates to 5 * fact' 4
.
As usual, the patterns in case
expressions should be exhaustive.