Skip to content

4. Functions

A function is a value like any other. It can be bound to a name, passed to a step, returned from another function, and written once for every type it works on.

Lambdas and their types

n => n * 2 is a lambda. Written as an argument, it takes its parameter's type from the step it is passed to. Written on its own, it states the type:

double = (n: Int) => n * 2
[412, 280] | map(double)
[824, 560]

A function's type is written with an arrow: Int -> Int, (Int, Int) -> Int.

double = (n: Int) => n * 2
double
Int -> Int

Piping into a function calls it: x | f is f(x).

double = (n: Int) => n * 2
280 | double
560

Declared functions

fn declares a function with typed parameters and a result type. Its name is bound before its body is checked, so it may call itself:

fn factorial(n: Int) : Int = 1 given n <= 1 otherwise n * factorial(n - 1)
[1, 2, 3, 4, 5] | map(factorial)
[1, 2, 6, 24, 120]

A function may take a function, and return one. The returned function keeps the values it captured:

fn adder(k: Int) : Int -> Int = n => n + k
fn twice(f: Int -> Int, x: Int) : Int = f(f(x))
twice(adder(3), 30)
36

A last parameter written ...T collects any number of trailing arguments into a list:

fn total(xs: ...Int) : Int = xs | sum
[total(), total(1, 2), total(1, 2, 3, 4)]
[0, 3, 10]

Generic functions

A function written once for every type names a type parameter, one capital letter in angle brackets. Each call fills it from its arguments:

fn first<T>(xs: List<T>) : Option<T> = xs | reduce((a, b) => a)
first(["Dune", "Emma"])
"Dune"

The same function over a list of numbers gives an Option<Int>:

fn first<T>(xs: List<T>) : Option<T> = xs | reduce((a, b) => a)
first([412, 280])
Option<Int>

Inside the body T is a type of its own: the body may rely only on what the parameter promises. T : Orderable promises an order, so the body may sort or find a minimum, and each call checks that the promise holds:

fn shortest<T : Orderable>(xs: List<T>) : Option<T> = xs | min
shortest([412, 280, 310])
280
fn shortest<T : Orderable>(xs: List<T>) : Option<T> = xs | min
shortest([{1}, {2}])
type error: `shortest` needs T : Orderable, and T is Set<Int> here
 --> <expression>:2:1
  |
2 | shortest([{1}, {2}])
  | ^^^^^^^^^^^^^^^^^^^^

Aliases

Most small functions are a pipeline fragment with a name. := defines one, and the checker writes its type: the input is the most general one the fragment accepts, and each parameter takes the type of the step argument it fills.

top(n) := sort | take(n)
[412, 280, 310, 145] | top(2)
[145, 280]

hql check prints the signature it wrote:

top(n) := sort | take(n)
1
top : (Collection<T>, Int) -> List<T> where T : Orderable
Int

Holes

A call with _ for an argument is a function of the arguments left open — a partial application:

fn add(a: Int, b: Int) : Int = a + b
[1, 2, 3] | map(add(10, _))
[11, 12, 13]

Absence, once more

Option<T> is how a function says a value may be missing. some(x) and none build one, match takes one apart, and somes keeps the present values of a collection:

fn rating_of(r: Int) : Option<Int> = some(r) given r > 0 otherwise none
[5, 0, 4] | map(rating_of) | somes
[5, 4]

What is next

With functions and collections in hand, chapter 5 asks the questions SQL asks: where, order by, group by, and the aggregates.