Control flow
Blocks
Every block in Aiken is an expression. All expressions in the block are executed, and the result of the last expression is returned.
let value: Bool = {
"Hello"
42 + 12
False
}
value == False
Expression blocks can be used instead of parenthesis to change the precedence of operations.
let celsius = { fahrenheit - 32 } * 5 / 9
Matching
The when *expr* is
expression is the most common kind of flow control in Aiken code. It
allows us to say "if the data has this shape then do that", which we call
pattern matching.
Here we match on an Int
and return a specific string for the values 0, 1,
and 2. The final pattern n
matches any other value that did not match any of
the previous patterns.
when some_number is {
0 -> "Zero"
1 -> "One"
2 -> "Two"
n -> "Some other number" // This matches anything
}
Aiken's when *expr* is
is an expression, meaning it returns a value and can be used
anywhere we would use a value. For example, we can name the value evaluated from a when expression with a let
binding.
type Answer {
Yes
No
}
let answer = Yes
let description =
when answer is {
Yes -> "It's true!"
No -> "It's not yes."
}
description == "It's true!"
If-Else
Pattern matching on a Bool
value is discouraged and if / else
expressions should be use instead.
let some_bool = True
if some_bool {
"It's true!"
} else {
"It's not true."
}
Note that, while it may look like an imperative instruction: if this then do that or else do that, it is in fact one single expression. This means, in particular, that the return types of both branches have to match.
Incidentally, you can have as many conditional else/if
branches as you need:
fn fibonnaci(n: Int) -> Int {
if n == 0 {
0
} else if n == 1 {
1
} else {
fibonnaci(n-2) + fibonnaci(n-1)
}
}
Destructuring
A when *expr* is
expression can be used to destructure values that
contain other values, such as tuples and lists.
when xs is {
[] -> "This list is empty"
[a] -> "This list has 1 element"
[a, b] -> "This list has 2 elements"
_other -> "This list has more than 2 elements"
}
It's not just the top level data structure that can be pattern matched,
contained values can also be matched. This gives when
the ability to
concisely express flow control that might be verbose without pattern matching.
when xs is {
[[]] -> "The only element is an empty list"
[[], ..] -> "The 1st element is an empty list"
[[4], ..] -> "The 1st element is a list of the number 4"
other -> "Something else"
}
Matching single-variant constructors
Aiken lets you pattern match on values from types that have a single
constructor using a let
-binding directly. This is the case for any Tuple
, for example, or any custom type with a single constructor.
type Foo {
foo: Int
}
let (a, b, c) = (1, 2, 3)
let Foo { foo } = Foo { foo: 42 }
Assigning names to sub-patterns
Sometimes when pattern matching we want to assign a name to a value while
specifying its shape at the same time. We can do this using the as
keyword.
when xs is {
[[_, ..] as inner_list] -> inner_list
_other -> []
}
Clause guards
The if
keyword can be used to add a guard expression to a when clause. Both
the patterns have to match and the guard has to evaluate to True
for the
clause to match. The guard expression can check for equality or ordering for
Int
.
when xs is {
[a, b, c] if a == b && b != c -> "ok"
_other -> "ko"
}
Error & Todo
Sometimes, you need to halt the evaluation of your program because you've
reached a case that is considered invalid or simply because you haven't yet
finished developing some logic. Aiken provides two convenient keywords for
that: error
and todo
.
When encountered, both will halt the evaluation of your program which will be considered failed. They differ in their semantic i.e. how the compiler behaves towards them.
In fact, todo
will trigger warnings at compilation to remind you of those
unfinished parts. error
will not, as it is assumed to be a desired break
point. Note that the warning also includes the expected type of the expression
that needs to replace the todo
. This can be a useful way of asking the
compiler what type is needed if you are ever unsure.
Let's see an example for both to grasp the difference:
fn favourite_number() -> Int {
// The type annotations says this returns an Int, but we don't need
// to implement it yet.
todo
}
fn expect_some_value(opt: Option<a>) -> a {
when opt is {
Some(a) -> a
None -> error // We want this to fail when we encounter 'None'.
}
}
When this code is built Aiken will type check and compile the code to ensure it
is valid, and the todo
or error
will be replaced with code that crashes the
program if that function is run.
Giving a reason
A String
message can be given as a form of documentation. The message will be
traced when the todo
or error
code is evaluated. Note that this is likely
the only place where you will encounter the type String
and this is because
the message needs to be printable -- unlike most ByteArray
which are often
plain gibberish.
fn favourite_number() -> Int {
todo @"Believe in the you that believes in yourself!"
}
fn expect_some_value(opt: Option<a>) -> a {
when opt is {
Some(a) -> a
None -> error @"Option has no value"
}
}