Functions
Declaring and calling functions
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
Write this.