8. Lambdas and the effect system

  • chapter 8 of 20
  • 2 min read

An anonymous function is written (params) => expr — a single un-annotated parameter may drop the parentheses and be written x => expr, and a parameter annotation may be left out wherever the type can be inferred. A function type is written fn(A) -> B !e, where !e is its effect. A pure function's signature is enough to know it has no side effects, and a test for one needs no mocks.

pub fn main() -> Unit !io = {
  let nums = [1, 2, 3, 4]
  let evens = filter(nums, n => n % 2 == 0)
  let doubled = map(evens, n => n * 2)
  println(to_string(doubled))
}
[4, 8]

A higher-order function forwards its arguments' effects through an effect variable: the effect of map(f) is the effect of f. The union of two function parameters' effects is written !(e1 | e2) — pure ∘ pure is still pure, and anything that touches io is io.

fn compose[A, B, C](f: fn(A) -> B !e1, g: fn(B) -> C !e2) -> fn(A) -> C !(e1 | e2) =
  a => g(f(a))

fn inc(x: Int) -> Int = x + 1
fn dbl(x: Int) -> Int = x * 2

pub fn main() -> Unit !io = {
  let f = compose(inc, dbl)
  println(to_string(f(10)))
}
22

A block as the last argument

When a call's last argument is a function, it can follow the call as a bare block on the same line: f(a) { e } is f(a, () => e). A block that takes parameters names them before a =>, as a lambda does, one bare name or a parenthesised list:

fn twice(body: fn() -> Unit !e) -> Unit !e = {
  body()
  body()
}

pub fn main() -> Unit !io = {
  twice {
    println("hi")
  }
  let total = fold([1, 2, 3], 0) { (acc, x) => acc + x * x }
  println("${total}")
  let loud = map(["a", "b"]) { s => s ++ "!" }
  println("${loud}")
}
hi
hi
14
["a!", "b!"]

The block fills the last parameter whatever came before it, which is why fold(xs, 0) { ... } reads as "fold from 0, with this". It has to open on the same line as the call; a { on the next line is a block statement of its own. In an if, while or for header, and after match, the braces are the body and not an argument, so parenthesise the call there: if (f(x) { ... }) { ... }. With named arguments and defaults, it is how a function with many options and a body reads at the call site: column(gap: 12) { ... }.

See also: spec.en.md §4.3, §4.5, §6