Pattern Matching
Pattern matching is the core of Nanyx control flow. It's a powerful feature that lets you destructure data, test conditions, and handle different cases in a type-safe, exhaustive manner.
Match Expressions
The most common way to pattern match in Nanyx is with a match expression:
def oddOrEven = { n ->
match n % 2
| 0 -> "even"
| _ -> "odd"
}The _ pattern matches anything, acting as a catch-all.
Matching on Tag Unions
Pattern matching is especially powerful with tag unions:
def describe: Option(int) -> string = { opt ->
match opt
| #some(x) -> "Got value: {x}"
| #none -> "No value"
}
-- Multiple tag variants
def area: Shape -> float = { shape ->
match shape
| #circle(r) -> 3.14159 * r * r
| #rectangle(w, h) -> w * h
| #triangle(b, h) -> 0.5 * b * h
}Matching on Records
You can destructure records in pattern matches:
def greet: Person -> string = { person ->
match person
| (name = "Alice", age = age) -> "Hi Alice, age {age}!"
| (name = name, age = a) if a >= 18 -> "Hello adult {name}"
| (name = name, _) -> "Hi young {name}"
}Matching on Literals
Match specific values:
def classify: int -> string = { x ->
match x
| 0 -> "zero"
| 1 -> "one"
| n if n < 0 -> "negative"
| n if n > 100 -> "large"
| _ -> "other"
}Comparison Patterns
Use comparison operators directly in patterns:
def sign: int -> string = { x ->
match x
| < 0 -> "negative"
| > 0 -> "positive"
| _ -> "zero"
}List Destructuring
Pattern match on lists with head/tail patterns:
rec sumList: list(int) -> int = {
| [] -> 0
| [head, ...tail] -> head + sumList(tail)
}Guards
Add conditions to patterns with if:
def classifyNumber: int -> string = {
| 0 -> "zero"
| 1..10 -> "small"
| n if n > 10 -> "really big!"
| _ -> "unknown"
}Pattern Matching in Function Definitions
Functions can pattern match directly on their arguments:
rec sumList
: list(int) -> int
= {
| [] -> 0
| [head, ...tail] -> head + sumList(tail)
}
-- Multiple arguments
def divide
: int, int -> Result(int, #divideByZero)
= {
| _, 0 -> #error(#divideByZero)
| x, y -> #ok(x / y)
}Exhaustiveness
The compiler checks that your patterns are exhaustive - they must cover all possible cases:
-- Compiler error: missing pattern for #none
def bad: Option(int) -> int = { opt ->
match opt
| #some(x) -> x
-- Missing: | #none -> ...
}Nested Patterns
Patterns can be arbitrarily nested:
def process: list(Option(int)) -> int = { data ->
match data
| [#some(x), #some(y), ...] -> x + y
| [#some(x), #none, ...] -> x
| _ -> 0
}Labeled Patterns
Use labels when destructuring to keep the original value:
def [[x, ...xs] list1, [y, ...ys] list2] = [[1,2,3], [4,5,6]]
x -- => 1
xs -- => [2,3]
list1 -- => [1,2,3]Total vs Partial Patterns
Some patterns always match (total), others might fail (partial):
-- Total pattern - always matches
def (x, y) = (4, 5)
-- Partial pattern - compiler error if used in definition
-- def (4, y) = record -- Error! Use match instead
-- Partial patterns must use match or if
if record is (4, y) ->
println(y)Negated patterns
But having to repeat the type name Customer is a bit anti-DRY.
We can instead match against anything except #nil using the not modifier to the pattern:
match getCustomer()
| (not #nil) customer -> println("Hello, {customer.name}")
| #nil -> println("No customer found")But this feels a bit laborious. Nanyx has the concept of custom patterns, which we can use to
introduce a some pattern that matches anything except #nil:
pattern Some = not #nilThis feels much better, and the standard library of Nanyx includes such a pattern
Note that the above is a simple pattern alias; it could have been written as
pattern Some = {
| (not #nil) s -> (true, s)
| _ -> false
}It can then be used like this:
match someFunction()
| #some data -> data
| #nil -> "no data"Custom patterns with outputs
Some patterns can have both inputs and outputs; e.g. this regex pattern:
pattern Regex = { r, s ->
if Regex(r).match(s) ->
s
else ->
#nil
}def fizzbuzz { range ->
range \map { i ->
match i % 3, i % 5
| 0, 0 -> "fizzbuzz"
| 0, _ -> "fizz"
| _, 0 -> "buzz"
| else -> string(i)
}
}Using a custom pattern, we can reduce the Fizzbuzz example above to:
pattern DivisibleBy | _ = { divisor, x ->
if x \mod divisor == 0 -> true
else -> false
}def fizzbuzz { range ->
range \map {
| DivisibleBy(3) & DivisibleBy(5) -> "fizzbuzz"
| DivisibleBy(3) -> "fizz"
| DivisibleBy(5) -> "buzz"
| else -> "{i}"
}
}There is also an except keyword, which works like match except that it does
not necessarily need to match all cases; any unmatched values are simply returned
type AD = #a | #b | #c | #d
def f: AD -> string = { x ->
x except
| #a -> "a"
| #b -> "b" -- Since we've only matched #a and #b, #c and #d will be returned as-is
-- The whole function now has a return value of `string | #c | #d`
}When using except, if the result has been narrowed down to a single tag from a tag union
then it is automatically unwrapped:
def f: (#a(string) | #b) -> string = { x ->
def s =
x except
| #b -> "No string"
-- s is of type string
-- Notice how the #a has disappeared because it was the only tag remaining
}The except keyword is especially useful for handling errors. For example, given the function
def getCustomer
: (id: string) -> #some(Customer) | #notFound
= { id ->
if ... -> #notFound
else -> #some(Customer(...))
}we can use except to handle the error case in a more functional way:
def f = {
def customer = getCustomer("some_id") except
| #notFound -> return #customerDoesNotExist
... -- Do something with `customer`
}Pattern matching is a powerful tool for working with data. It allows you to both test an expression to see if it contains certain data and, at the same, to extract other pieces of data from the expression.
The syntax for pattern matching is to pipe an expression to a series of case arms, like so:
def f = {
| 1 -> "one"
| 2 -> "two"
| 3 -> "three"
| _ -> "lots"
}Active patterns are functions that can be used in a | arm. They are written as a normal function, but with the name encased in brackets
The simplest active pattern is a function that returns a boolean. If the function returns true, the match arm is executed. Patterns can be partial or total
pattern Even | _ = { x -> x % 2 == 0 }
pattern Even | Odd = { x ->
if x % 2 == 0 -> Even
else -> Odd
}
def f: int -> string = { x -> match x
| Even -> "even"
| Odd -> "odd" -- This pattern is known to be complete, so we don't need a catch-all arm
}Active patterns can also return a value. This value is then bound to the name of the active pattern in the match arm.
This pattern is also partial (indicated by the ?), meaning that it might not match
pattern Regex | _ = { r, x ->
if Text.regexMatch(r, x) is [y, ...] ->
Regex(y)
}def f = {
| Regex("^[0-9]+$", d) -> "a number -> {d}"
| Regex("^[a-z]+$", s) -> "a string -> {s}"
| _ -> "something else"
}It's also possible to match using an arbitrary function:
match someInt()
| 0 -> "zero"
| { < 5 } -> "small"
| { in 5..10 } -> "medium"
| { x -> x > 10 } -> "big"It's possible in Nanyx to pattern match on a partial tag union. This is what allows many of the standard library functions to act on both an optional value and a result. There's a convention that any tag union that includes a #some(t) variant is considered an "optional T" value and there are many functions that operate on these:
def maybeString: (#some(string) | _) -> string = {
| #some(s) -> s
| _ -> "Nothing was found"
}In the above function the "rest" parameter that represents the rest of the union is unused
so we can use the discard syntax _.
We can give it a proper type parameter if we'd like to use it elsewhere in the function signature. For example, in this optional map function:
def mapOptional: (#some(a) | r), (a -> b) -> (#some(b) | r) = {
| #some(v), f -> #some(f(v))
| other -> other
}
def x: #some(string) | #nil = ???
def x' = x \mapOptional { s -> s.length } -- x': #some(int) | #nil
def y: #some(int) | #noValue | #infinity = ???
def y' = y \mapOptional { \float } -- y': #some(float) | #noValue | #infinity
def z: #some(Customer) | #err(string) = ???
def z' = z \mapOptional { .id } -- z': #some(CustomerId) | #err(string)
Notice how we have specified that the union of the first argument should include #some(a) but we have placed no limits on what the other tags in the union can be. That allows us to map one of the tags whilst leaving all others as-is.