Control flow
Match expressions
A match expression is used to match against one or more patterns and then evaluate the branch of the matched clause. All branches must evaluate to the same type. The clauses are tested sequentially, and the first clause that matches is evaluated.
match (3, "three hee hee")
case (1, "one fun fun") => "It's a 1"
case (_, "two doo loo") => "Got a 2"
case (3, description) => description // This one matches.
case _ => "There was nothing interesting :("
Guards can be added using if,
and the clause is only a match if both the pattern matches and the guard is true.
Guards can use bindings from the pattern.
match (3, 4)
case (x, y) if x > y => "x is bigger"
case (x, y) if x < y => "y is bigger"
case _ => "x and y are equal"
A match expression must always be exhaustive.
Guards interfere with the ability of the compiler to determine exhaustion.
For example, the last clause of the above example cannot have the guard x == y,
because the compiler cannot infer that the guards cover all cases.
If expressions
An if expression conditionally evaluates one of two branches and returns the result.
let x = 100;
if x > 20 then "big" else "small"
Patterns
A pattern simultaneously describes value and shape. Patterns can include:
- Names, such as
xorsecret_key, which always match. Int,Float, andStringvalue, which must match exactly.- Tuples and
Lists. - Value constructors.
The pattern _ matches anything, and it is idiomatic to mean that the value is not used.
Destructuring
Destructuring unpacks a composite value into its parts.
Tuples are destructured by position.
let (name, message) = ("Viviette", "Hello!")
Lists are destructured by position and the tail is captured with ..,
which can appear at most once in a list pattern and must be at the end of the list.
let [a, b, ..c] = [1, 2, 3, 4, 5]; // (a, b, c) == (1, 2, [3, 4, 5])
let [x, y, ..z] = [1, 2]; // (x, y, z) == (1, 2, [])
let [p, ..] = [1, 2, 3, 4, 5] // p == 1
Data constructors are destructured.
- Unnamed fields are filled left to right.
- Named arguments are filled in the remaining spot.
- Match on just the name by termininating with a colon (e.g.,
name:). - Match on the name and the subvalue with a pattern after the colon
(e.g.,
name: (x, [1, 2, ..ys])).
- Match on just the name by termininating with a colon (e.g.,
..is used to capture (and discard) the remaining fields. Positional fields are not captured by..and must be skipped with_.
type Person
case Person(
String, // Name.
Int, // Age,
email: String,
secondary_email: String,
favourite_colour: String,
)
function example(person: Person) -> String = match person
// This clause skips the age (`_`) and the email (`..`).
case Person(name, _, secondary_email:, favourite_colour: "red", ..) => secondary_email
// This clause matches on the named field by position.
case Person(name, 22, email, ..) => email
// This clause simply renames the named field using a sub-pattern.
case Person(_, _, email: primary_email, ..) => primary_email
Renaming
A match can be renamed using as. Renaming can be nested.
import std/io
/// Sums the first two values in the list. If the list is not long enough, the
/// missing values are treated as zero. The whole list is printed when its
/// length is at least 2.
function sum_first_two(values: List(Int)) -> Int :: IO = match values
case [] as list => 0
case [x] as list => x
// Matches the entire value of the list into the `list` binding.
case [x, y, ..] as list => {
io.println(list);
x + y
}
Irrefutability
A pattern is said to be irrefutable if it always matches.
For example, matching on (), x, or (a, b) will always succeed,
so these are irrefutable patterns.
If the pattern can fail (e.g., matching on an Option), then it is refutable.
Exhaustion
When matching with a match expression or function arguments, the pattern must be exhaustive,
meaning that every possible value of the match argument
must match with at least one of the patterns in the clauses.