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
Piping
Operator | Precedence |
---|---|
|> | 0 |
Aiken provides syntax for passing the result of one function to the arguments
of another function: the pipe operator (|>
). This is similar in functionality
to the same operator in Elixir, Elm or F#.
The pipe operator allows you to chain function calls without using a lot of
parenthesis and nesting. For a simple example, consider the following
implementation of an imaginary string.reverse
in Aiken:
string_builder.to_string(string_builder.reverse(string_builder.from_string(string)))
This can be expressed more naturally using the pipe operator, eliminating the need to track parenthesis closure.
string
|> string_builder.from_string
|> string_builder.reverse
|> string_builder.to_string
Each line of this expression applies the function to the result of the previous line. This works easily because each of these functions takes only one argument. Syntax is available to substitute specific arguments of functions that take more than one argument; for more, look below in the section "Function capturing".
Looping through recursion
Aiken is a functional programming language, and as such, doesn't provide any control flow for loops. Instead, Aiken embraces recursion. A recursive function is a function that calls itself in its own definition. And similarly, a recursive type is a type that is defined in terms of itself.
Let's see an example for both.
type MyList<a> {
Empty
Prepend(a, MyList<a>)
}
fn length(xs: MyList<a>) -> Int {
when xs is {
Empty -> 0
Prepend(_head, tail) -> 1 + length(tail)
}
}
length(Prepend(1, Prepend(2, Prepend(3, Empty)))) // == 3
Here we artificially define a custom MyList
type which represents a linked
list. And we define a function over it that computes its length. In both cases,
you can see how it is crucial that there exist a terminal case.
Recursion is extremely powerful, in particular on an execution environment like Plutus on Cardano. Abuse it.
If-Else
Pattern matching on a Bool
value is discouraged and if *condition* 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 fibonacci(n: Int) -> Int {
if n == 0 {
0
} else if n == 1 {
1
} else {
fibonacci(n-2) + fibonacci(n-1)
}
}
Fail & 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: fail
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. fail
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 -> fail // 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 fail
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 fail
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 -> fail @"Option has no value"
}
}
Expect
expect
is a special assignment keyword which works like let
, but allows to
perform some potentially unsafe conversions. There are two main use cases for
it:
Non-exhaustive pattern-matching
In cases where you have a value of a type that has multiple constructor
variants, but are only truly interested in one of the possible variants as
outcomes (i.e. any other outcomes invalidate the program), then expect
is the perfect tool. Consider the Option
type in the following example:
let x = Some(42)
// As a pattern-match
let y = when x is {
None -> fail
Some(y) -> y
}
// Using expect
expect Some(y) = x
As you can see, expect
works like a non-exhaustive pattern-match. The
difference being that it is instructed to crash the entire program in case
where the right-hand side wouldn't have the expected shape. It signals to the
compiler that in this particular place, it is acceptable to not be exhaustive.
This may seem like a bad practice from the traditional world, but remember that Aiken is used in a smart contract context where there's often no room for error handling. Either the data has the expected form, or the entire contract fails.
Casting from Data
into a custom type
Another crucial use of expect
is to turn some opaque Data
into a
concrete representation. In the context of a Cardano smart contract, we can
encounter Data
in various places although in general it's most likely when
dealing with the datums attached to outputs.
Often, we do expect a specific structure for some given datum, and so being able to safely relay those assumptions in the validators comes in handy.
The syntax is identical to the other use case, but it requires an explicit type annotation.
type MyDatum {
foo: Int,
bar: ByteArray,
}
fn to_my_datum(data: Data) -> MyDatum {
expect my_datum: MyDatum = data
my_datum
}
Note that this conversion will fail if the given Data
isn't actually a valid
representation of the target type MyDatum
. The primary use-case here is for
instantiating script contexts, datums and redeemers provided to scripts in a
serialized form.
Soft casting with if/is
The expect
keyword works well in cases where failing is an option. But there are situations, like for example when recursively traversing elements of a list, where it isn't acceptable. The if *expr* is *pattern*
therefore comes into play when you need to fallback in case a right-hand side value doesn't have the right shape instead of failing loudly. It can also be particularly handy when you simply don't know the expected type of an opaque Data
and need to try several alternatives.
if *expr* is *pattern*
There is an extension to if expressions to be able to attempt casting but without returning an error by using the provided else instead.
type Foo {
foo: Int
}
type Bar {
Buzz(Int)
Bazz(Int)
}
fn soft_casting(d: Data, x: Data) -> Bool {
// with no pattern provided
if d is Foo {
d.foo == 1
// with a pattern
} else if d is Bazz(y): Bar {
y == 1
// with a pattern
} else if x is Buzz(y): Bar {
y == 2
} else {
False
}
}
Like expect
, patterns can be used for either type-casting or pattern-matching on a single pattern. And like expect
, it can also introduce new identifiers in scope. As shown, in the second and third branch, we introduce a y
variable that is only available within those branches. Note that the use of if/is
is discourage in situation where no type-casting is necessary: just use when/is
.
A final else
is always required and acts like a wildcard / catch-all case to handle any remaining pattern or casting not explicitly covered by any of the previous branch.