Skip to main content

Control flow

Do expressions

A do expression allows more than one expression to appear in sequence.

main: () -> ()
main () = do
io.println "First"
io.println "Second"
let message = "Third"
io.println message

A do expression takes on the value of its last evaluated expression.

main: () -> ()
main () = do
let five = do
let a = 2
let b = 3
a + b
assert.eq five 5

If expressions

An if expression is the if ... then ... else ... construct used to make a decision on a Boolean.

let x = 10
let description = if x > 0 then "positive" else "zero or negative"

The else clause is always required. In cases where else is not needed or several conditions must be checked, it is better to use a when expression.

When expressions

When expressions conditionally evaluate part of a program based on a Boolean condition. A when expression consists of one or more clauses, having a condition and body to evaluate when the condition is satisfied. The first condition that is satisfied has its body evaluated.

let x = 10
when
x > 1000 -> io.println "x is really big!"
x == 100 ->
io.println "x is 100."
io.println "(Isn't that neat?)"
// This is the catch-all if no conditions are satisfied.
else -> io.println "There is nothing interesting about x."

when x == 123456 -> io.println "What are the chances?"
info

While True can be used in place of else as the catch-all (and some languages do), using else is clearer as it presents the same semantics as if ... then ... else.

When used in expression position, when expressions take on the value of the last evaluated expression. In these cases, the clauses must all evaluate to values of the same type, and there must be a catch-all.

let x = 0
io.println when
x > 0 -> "positive"
x == 0 -> "zero" // This is evaluated.
else -> "negative"

Match expressions

A match expression is used to match against one or more patterns. When a match is found, any names in the pattern are bound to the corresponding parts of the match value to be used in the body. The first match that is satisfied has its body evaluated. Unlike a when expression, a match expression must always be exhaustive, meaning that the clauses cover every possible value.

let value = (3, "three hee hee")
value match
(1, "one fun fun") -> io.println "its 1"
(_, "two doo loo") -> io.println "got a 2"
(3, description) -> io.println description // This one matches.
_ -> io.println "There was nothing interesting :("

In general, match expressions have the same properties as if expressions. A match expression in value position evaluates to its last evaluated expression, and in this case, all clauses must evaluate to the same type.

A match case may have a guard using if to add a condition. A catch-all pattern (e.g., _) must be added when guards are used. The compiler cannot check for exhaustion when guards are involved. To the programmer, it is obvious that either the first or second arm matches, but this is difficult for the compiler to determine.

let description = "hello" match
message if length message > 10 -> "long message"
_ -> "short message"
assert.eq description "short message"

Require expressions

A require expression is similar to a let expression; however, the pattern can be refutable (i.e., it is not guaranteed to match). When the pattern does not match, the program will panic.

let value = Some 123
require Some x = value // This matches.
// Uncommenting the next line will cause the program to panic.
// require None = value

Try expressions

Try expressions are useful for accessing and propagating values. Anything that has the Try implicit can be used in a try expression. In the standard library, Option and Result have Try implicits.

A try expression works in the following way:

  • For Option a, it evaluates to a value of type a when Some; otherwise, it returns None from the function.
  • For Result a e, it evaluates to a when Ok; otherwise, it returns Err e from the function.

For example, the following typechecks.

import std/io

print_if_ok: Result String e -> Result () e
print_if_ok result = do
let message = try result
io.println message
Ok ()

This desugars to roughly the following.

print_if_ok: Result String e -> Result () e
print_if_ok result = do
let message = result match
Ok value -> value
error -> return error
io.println message
Ok ()
tip

Where try is particularly useful is in a pipe.

import std/math
import std/stream

/// Squares the first odd number in a list.
square_first_odd: List Int -> Option Int
square_first_odd numbers = numbers
| try stream.find \x -> x % 2 == 1
| math.square
| Some

Patterns

A pattern simultaneously describes value and shape. Patterns can include, for example, x, True, [1, 2, 3], and ("hello", x). These patterns can be broken down:

  • The pattern x matches anything.
  • The pattern True matches the shape Bool with the value True.
  • The pattern [1, 2, 3] matches the shape List Int of [_, _, _] with respective values 1, 2, and 3.
  • The pattern ("hello", x) matches the shape (String, _) where the first value is "hello" and the second is any value.

The pattern _ matches anything, and it is idiomatic to mean that the value is not used.

Where patterns are used

Patterns appear in several locations.

  1. Let and require bindings (must be irrefutable for let bindings).
    let x = 5
    let (message, number) = ("hello", 22)
  2. Match expressions.
    Some 1001 match
    Some x -> x
    None -> 0
  3. Function arguments.
    sum_pairs: (Int, Int) -> (Int, Int) -> (Int, Int)
    sum_pairs (a, b) (x, y) = (a + x, b + y)

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.

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 and guards.

Values in patterns

Patterns can include value literals.

(3, 1) match
(3, _) -> "first is 3"
(2, 2) -> "both are 2"
_ -> "something else"

Integers, floats, strings, tuples, lists, and value constructors can appear in patterns.

Destructuring patterns

Destructuring unpacks a composite value into its parts.

Destructuring tuples

Tuples are destructured by position.

let (name, message) = ("Viviette", "Hello!")

Destructuring lists

Lists can be destructured using .., which can appear at most once in a list pattern and must be at the end of the list.

// Capture the tail.
let [a, b, ..c] = [1, 2, 3, 4, 5]
assert.eq (a, b, c) (1, 2, [3, 4, 5])

// It can also be empty.
let [x, y, ..z] = [1, 2]
assert.eq (x, y, z) (1, 2, [])

// The name can also be omitted.
let [p, ..] = [1, 2, 3, 4, 5]
assert.eq p 1

Destructuring tuple constructors

Tuple constructors are matched by position. Consider the following type consisting of only tuple constructors.

type Data =
One Int,
Two Int Int,
Three Int Int Int

The constructors are matched based on position.

let data = Three 4 5 6
data match
One x -> x
Two x y -> x + y
Three x y z -> x + y + z // This clause matches.

Destructuring record constructors

Record constructors can use .. to omit fields and : to rename others in value constructors. Field names can appear in any order; however, the .. must appear last. For example, consider the following Point type.

type Point = Point { x: Int, y: Int }

A point value is created with the following.

let p = Point { x: 0, y: 1 }

The value of p can be matched in several ways.

  1. Match on name.
    let Point { x, y } = p
    assert.eq (x, y) (0, 1)
  2. Ignore some fields.
    let Point { y, .. } = p
    assert.eq y 1
  3. Rename fields.
    let Point { x: first, y: second } = p
    assert.eq (first, second) (0, 1)
    // assert.eq (x, y) (0, 1) // Uncommenting is an error since only `first`
    // and `second` are bound.

Pattern renaming

A match can be renamed using as. Renaming can be nested.

(3, 4) match
(x, 4 as y) as pair -> do
assert.eq x 3
assert.eq y 4
assert.eq pair (3, 4)
_ -> unreachable

Any name binding to the left of as is in scope after the pattern match.

5 match
a as b -> do
assert.eq a 5
assert.eq b 5
tip

Renaming a field of a constructor in a pattern is not achieved with as. Refer to destructuring.