Language Tour
Functions

Functions

Named functions

Named functions in Aiken are defined using the fn keyword. Functions can have (typed) arguments, and always have a return type. Because in Aiken, pretty much everything is an expression, functions do not have an explicit return keyword. Instead, they implicitly return whatever they evaluate to.

fn add(x: Int, y: Int) -> Int {
  x + y
}
 
fn multiply(x: Int, y: Int) -> Int {
  x * y
}

Functions are first class values and so can be assigned to variables, passed to other functions, or anything else you might do with any other data type.

/// This function takes a function as an argument
fn twice(f: fn(t) -> t, x: t) -> t {
  f(f(x))
}
 
fn add_one(x: Int) -> Int {
  x + 1
}
 
fn add_two(x: Int) -> Int {
  twice(add_one, x)
}

Anonymous functions

Anonymous functions can be defined with a similar syntax.

fn run() {
  let add = fn(x, y) { x + y }
 
  add(1, 2)
}

Labeled arguments

When functions take several arguments it can be difficult for the user to remember what the arguments are, and what order they are expected in.

To help with this Aiken supports labeled arguments, where function arguments are given by labels instead of position.

Take this function that replaces sections of a string:

fn replace(self: String, pattern: String, replacement: String) {
  // ...
}

When calling the function, it is possible to use the defined labels to pass the arguments:

replace(self: @"A,B,C", pattern: @",", replacement: @" ")
 
// Labeled arguments can be given in any order
replace(pattern: @",", replacement: @" ", self: @"A,B,C")
 
// Positional arguments and labels can be mixed
replace(@"A,B,C", pattern: @",", replacement: @" ")

The use of argument labels can allow a function to be called in an expressive, sentence-like manner, while still providing a function body that is readable and clear in intent.

Overriding default labels

Note that, when defining a function, it is possible to override the default label to use different names (e.g. a shorter name) in the function body. For example:

fn insert(self: List<(String, Int)>, key k: String, value v: Int) {
  // ... do something with `k` and `v`
}

Externally, the function can still be called using key and value as labelled, but in the function body, they are named k and v for conciseness.

Validators

In Aiken, you can promote some functions to validators using the keyword validator.

validator {
  fn foo(redeemer: Data, script_context: Data) {
    ..
  }
 
  fn bar(datum: Data, redeemer: Data, script_context: Data) {
    ..
  }
}

Functions present in a validator block must abide by the following rules:

  • They must have exactly 2 or 3 arguments.
  • They must be named
  • There may be one or two functions, in a single block, but no more.

Parameters

Validators themselves can take parameters, which represent configuration elements that must be provided to create an instance of the validator. Once provided, parameters are embedded within the compiled validator and part of the generated code. Hence they must be provided before any address can be calculated for the corresponding validator.

validator(utxo_ref: ByteArray) {
  fn foo(redeemer: Data, script_context: Data) {
    ..
  }
}

We see more examples of validators in the examples presented later in this user manual.

Pipe Operator

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

Function capturing

There is a shorthand syntax for creating anonymous functions that take one argument and call another function. The _ is used to indicate where the argument should be passed.

fn add(x, y) {
  x + y
}
 
fn run() {
  let add_one = add(1, _)
 
  add_one(2)
}

The function capture syntax is often used with the pipe operator to create a series of transformations on some data.

fn add(x: Int , y: Int ) -> Int {
  x + y
}
 
fn run() {
  // This is the same as add(add(add(1, 3), 6), 9)
  1
  |> add(_, 3)
  |> add(_, 6)
  |> add(_, 9)
}

In fact, this usage is so common that there is a special shorthand for it.

fn run() {
  // This is the same as the example above
  1
  |> add(3)
  |> add(6)
  |> add(9)
}

The pipe operator will first check to see if the left hand value could be used as the first argument to the call, e.g. a |> b(1, 2) would become b(a, 1, 2).

If not it falls back to calling the result of the right hand side as a function , e.g. b(1, 2)(a).

Backpassing

Functions that take a continuation (i.e. callback with a single argument) can be invoked using the backpassing syntax:

let x <- foo()
 
// which is equivalent to writing:
 
foo(fn(x) {
 
})

This is particularly well suited for functions that operate over abstractions such as Option or Fuzzer and that provide an API of primitives to manipulate them. For example, let's consider the following small API over a Fuzzer:

fn constant(a) -> Fuzzer<a>
fn bool() -> Fuzzer<Bool>
fn map(Fuzzer<a>, fn(a) -> b) -> Fuzzer<b>
fn and_then(Fuzzer<a>, fn(a) -> Fuzzer<b>) -> Fuzzer<b>

And let's look at an excerpt constructing a pipeline using those primitives without backpassing. Something along the lines of:

pub fn option(fuzz_a: Fuzzer<a>) -> Fuzzer<Option<a>> {
  bool()
    |> and_then(
         fn(predicate) {
           if predicate {
             fuzz_a |> map(Some)
           } else {
             constant(None)
           }
         },
       )
}

A bit involved. Here, we can see how and_then is used to step through the bool's Fuzzer. This can be re-written more simply with backpassing as:

pub fn option(fuzz_a: Fuzzer<a>) -> Fuzzer<Option<a>> {
  let predicate <- and_then(bool())
  if predicate {
   fuzz_a |> map(Some)
  } else {
    constant(None)
  }
}

Which reads a lot better! In fact, this technique becomes even more helpful when there are a lot of nested callbacks as it flattens them into a sequence of backpassed let-bindings.

Generic functions

At times you may wish to write functions that are generic over multiple types. For example, consider a function that consumes any value and returns a list containing two of the value that was passed in. This can be expressed in Aiken like this:

fn list_of_two(my_value: a) -> List<a> {
  [my_value, my_value]
}

Here the type variable a is used to represent any possible type.

You can use any number of different type variables in the same function. This function declares type variables a and b.

fn multi_result(x: a, y: b, condition: Bool) -> Result<a, b> {
  when condition is {
    True -> Ok(x)
    False -> Error(y)
  }
}

Type variables can be named anything and may contain underscores (_), but the names must be lowercase. Like other type annotations, they are completely optional, but using them may make it easier to understand the code.

Type annotations

Function arguments are normally annotated with their type, and the compiler will check these annotations and ensure they are correct.

fn identity(x: some_type) -> some_type {
  x
}
 
fn inferred_identity(x) {
  x
}

The Aiken compiler can infer all the types of Aiken code without annotations and both annotated and unannotated code is equally safe. It's considered a best practice to always write type annotations for your functions as they provide useful documentation, and they encourage thinking about types as code is being written.

Documentation

You may add user facing documentation in front of function definitions with a documentation comment /// per line. Markdown is supported and this text will be included with the module's entry in generated HTML documentation.

/// Always true.
///
fn always_true(_a) -> Bool {
  True
}