Velo by Example: Closures

Functions in Velo capture the variables of their defining scope. The captured environment lives as long as the function value itself, even after the enclosing function has returned.

makeAdder returns a lambda that adds a captured number. Each call to makeAdder produces a fresh closure with its own n.

func makeAdder(int n) func[int] {
    func(int x) int {
        x + n;
    };
};

func[int] add5 = makeAdder(5);
func[int] add10 = makeAdder(10);

add5(3);   # 8
add10(3);  # 13

Closures can capture mutable state. Two counters made from the same factory keep their counts independent.

func makeCounter() func[int] {
    int count = 0;
    func() int {
        count = count + 1;
        count;
    };
};

func[int] c1 = makeCounter();
func[int] c2 = makeCounter();

c1();  # 1
c1();  # 2
c2();  # 1 — independent state
c1();  # 3

A factory can return multiple closures sharing the same captured variable — a classic recipe for encapsulated state.

class Pair(func[void] add, func[int] get) {};

func makeAccumulator() Pair {
    int total = 0;
    new Pair(
        func(int v) void { total = total + v; },
        func() int { total; }
    );
};

Pair acc = makeAccumulator();
acc.add(7);
acc.add(35);
acc.get();   # 42

Currying: a function that returns another function lets you bind arguments one at a time.

func add(int a) func[int] {
    func(int b) int {
        a + b;
    };
};

func[int] addTen = add(10);
addTen(5);   # 15
add(3)(4);   # 7

Closures are ordinary values: store them in fields, arrays, or pass them across function boundaries. Their captured environment is reachable as long as the closure itself is reachable.

class Button(str label, func[void] onClick) {};

int clicks = 0;
Button b = new Button("OK", func() void {
    clicks = clicks + 1;
});

b.onClick();
b.onClick();
# clicks is now 2