Chapter 11 — Functions as Values, and Closures

Functions in Ingle are values. You can put one in a binding, pass it to another function, and call it later. A function type is written fn(ArgTypes) -> ReturnType.

fn double(x: int) -> int { return x * 2 }

fn apply(f: fn(int) -> int, x: int) -> int {
    return f(x)
}

fn main() -> int {
    let g = double                 // a named function, held as a value
    return apply(double, 5) + g(7) // 10 + 14 = 24
}
=> 24

apply takes a function as its first argument — any fn(int) -> int will do — and calls it. That’s the foundation of every “do this to each element” operation.

Lambdas

A lambda is a function written inline, with |params| expr (or |params| { ... } for a block). Its parameter types are inferred from where it’s used:

fn main() -> int {
    return apply(|x| x + 100, 5)   // the lambda is an fn(int) -> int, inferred from apply
}
=> 105

A lambda can capture variables from around it — and it does so by value, copying them in when the lambda is created. That means it can never dangle, and there are no lifetime puzzles:

fn main() -> int {
    let n = 100
    let add_n: fn(int) -> int = |x| x + n   // captures n (by value)
    return add_n(5)                         // 105
}
=> 105

The one rule that will trip you up. A lambda needs to know its types, and it learns them from context — either by being passed straight into a function that expects a function type, or from an explicit annotation on the binding. So apply(|x| x + 1, 5) is fine (the context is apply’s parameter), and let f: fn(int) -> int = |x| x + 1 is fine (the annotation). But a bare, unannotated let f = |x| x + 1 is an error — Ingle can’t guess what x is. The compiler says so plainly: “a lambda needs a function-typed context.” When in doubt, annotate the binding or pass the lambda directly.

Two more honest limits: capturing is read-only (a closure can’t reassign a variable it captured), and you can capture scalars, strings, and enums, but not a struct or an array (that would alias a unique owner — pass it as a parameter instead).

Higher-order functions, the standard-library way

Put functions-as-values together with generics and you get the classic trio, which live in std/list (more on imports in Chapter 15):

import "std/list" as list

fn main() -> int {
    let xs = [1, 2, 3, 4, 5]
    let evens   = list.filter(xs, |n| n % 2 == 0)            // [2, 4]
    let doubled = list.map(evens, |n| n * 2)                 // [4, 8]
    let sum     = list.reduce(doubled, 0, |acc, n| acc + n)  // 12
    return sum
}
=> 12

There’s a list.sort too, which takes a “is a before b?” lambda:

import "std/list" as list

fn main() -> int {
    let words = ["ccc", "a", "bb"]
    let sorted = list.sort(words, |a, b| a.len() < b.len())  // ["a", "bb", "ccc"]
    for w in sorted { println(w) }
    return sorted.len()
}
a
bb
ccc
=> 3