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

Currying

All functions are curried, which means that not all arguments need to be supplied up front. If not all arguments are given, then instead of evaluating the function, a new function is created that accepts the remaining arguments.

function multiply (n: Int) (m: Int) -> Int = n * m

constant example: Int =
let double: Int -> Int = multiply 2; // Partially apply `multiply`.
double 10 // 20

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 1 n

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

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 (\x -> x ** 2) [1, 2, 3] // [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, 6]
| 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 argument to a function on the right side. This works because functions are curried.

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.

Unit argument shorthand

When () is passed as an argument to a function, it does not need to be written out in the full annotation with (name: Type). It is sufficient to simply write ().

// Full annotation.
function main (_: ()) -> () :: IO = io.println "Fully annotated"

// Shorthand.
function main () -> () :: IO = io.println "Shorthand"

Monomorphization restriction

TODO

Write this.