Skip to main content

Functions

Declaring and calling functions

info

Function declarations are only allowed at the module level.

A function is declared by listing its type followed by its definition. Functions return the last evaluated expression of their body.

function add_many(x: Int, y: Int, z: Int) -> Int = x + y + z

// Can use multiple expressions in the body.
function add_many(x: Int, y: Int, z: Int) -> Int =
let s = x + y;
s + z

A function is called by passing all of its arguments.

add_many(1, 2, 3) // 6

Named arguments

Function arguments can be passed by name. Arguments passed by position are first filled left to right, and those passed by name are matched on remaining arguments.

function message(prefix: String, body: String, suffix: String) -> String =
prefix + body + suffix

// Order does not matter for named arguments.
function example() -> String =
message(">>> ", suffix: ".", body: "Hello") // ">>> Hello."

Named arguments can have a different interface name than the value used in the function by placing it before the function name.

// `x` and `y` are used in the function.
function difference(left x: Int, right y: Int) -> Int = x - y

// `left` and `right` are available for named arguments.
// Below is every possible way that the function could be
// called and get the same result.
difference(10, 3) // 7
difference(10, right: 3) // 7
difference(left: 10, right: 3) // 7
difference(right: 3, left: 10) // 7

If the name already exists in the local scope, it can be passed as a named argument using the shorthand where : appears at the end of the name.

let left = 10
let right = 3
difference(left:, right:) // 7

Recursion

Functions can call themselves recursively to implement loops. Consider the factorial function, which accepts an integer n and computes the product of all positive integers up to and including n.

function factorial(n: Int) -> Int = match n
case 0 => 1
case m => m * factorial(m - 1)

This works but is not tail recursive because the last evaluated operation is *, not the recursive call to factorial. This means that the function can grow to use a lot of stack space. The tail recursive version is better because it will be optimized to constant stack space regardless of the number of recursive calls.

// Public interface.
@[public]
function factorial(n: Int) -> Int = factorial_tail(n, 1)

// This is usually not public. Instead, the `factorial` function
// with the nicer interface is exposed.
function factorial_tail(n: Int, acc: Int) -> Int = match n
case 0 => acc // Base case, ends recursion.
case m => factorial_tail(m - 1, m * acc) // Recursive call.

Anonymous functions

Anonymous functions are created as needed and do not have names. Use \ to create an anonymous function. Anonymous functions are closures and can use any value in scope at the time they are declared. Types are not specified.

//       Body
// -------
(x, y) -> x + y
//----
// Arguments

When creating an anonymous function with one argument, the parentheses around the arguments are optional. Note that the argument list is not a tuple.

Higher-order functions

It is often useful to pass a function another function, such as map, which calls a function for every item in a continer to produce a new container full of the new items.

map([1, 2, 3], x -> x ** 2) // [1, 4, 9]

When a function with named arguments is used as a higher order function, the argument names are lost and can only be passed by position.

The type of a function

Functions are first-class values and have a type, which is simply the tuple of the argument types, followed by the return type, and the effects.

Consider the following function. It has the type (Int, Int) -> Int :: IO.

import std/io

function add_and_print(x: Int, y: Int) -> Int :: IO =
let sum = x + y;
io.println(sum);
sum

Pipes

Pipes offer an easy way to write chained computations.

[1, 2, 3, 4, 5]
|> stream.filter(int.odd)
|> stream.map(x -> x ** 2)
|> stream.collect()
// [1, 9, 25]

The pipe, |>, takes a value on the left side and inserts it as the first argument to a function call on the right side. If the value should go in a positon other that the first argument, then it can be placed in the argument list using _. Named arguments can still be used.

Parameterized functions

Concrete types, such as Int and String, begin with uppercase letters, while type parameters, such as a and b, begin with lowercase letters. Type parameters indicate that any type can be used in that spot.

function apply(f: a -> b, x: a) -> b = f(x)

When apply is used, such as apply(s -> int.parse(s), "123"), the function type will unify to the concrete types involved in the computation.