Language Tour
Control Flow

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 -> []
}

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"
  }
}