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?"
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 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.