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
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. 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 :("
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 when
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 when 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 typea
whenSome
; otherwise, it returnsNone
from the function. - For
Result a e
, it evaluates toa
whenOk
; otherwise, it returnsErr 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 ()
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 shapeBool
with the valueTrue
. - The pattern
[1, 2, 3]
matches the shapeList Int
of[_, _, _]
with respective values1
,2
, and3
. - 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.
- Let and require bindings (must be irrefutable for let bindings).
let x = 5
let (message, number) = ("hello", 22) - Match expressions.
Some 1001 match
Some x -> x
None -> 0 - 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.
- Match on name.
let Point { x, y } = p
assert.eq (x, y) (0, 1) - Ignore some fields.
let Point { y, .. } = p
assert.eq y 1 - 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
Renaming a field of a constructor in a pattern is not achieved with as
. Refer
to destructuring.