Skip to content

Closure

MaartenHilferink edited this page Jul 25, 2026 · 1 revision

Note: the syntax described on this page is available from GeoDMS version 20.9.0 and later.

A function may not only return data or a unit; it may return another function. When it does, the returned function carries the values that were supplied to its producing call. Such a captured function value is a closure.

A closure is a function value produced by another function, carrying the enclosing function's arguments, captured by value at the point of the producing call.

Closures let a configuration build small, specialised functions on the fly and apply, compose or pass them around — all before any data item is calculated.

returning a function

A function returns a function by declaring its result type as function and designating a nested function named result:

function adder(parameter<float64> a) -> function
{
	function result(attribute<float64> x) -> attribute<float64> := x + a;
}

adder(10.0) does not compute anything — it yields a function value in which the parameter a has been fixed to 10.0. That value is then applied to data by chaining a second argument list:

attribute<float64> plus10 (Road) := adder(10.0)(Road/flow); // [11,12,13]

The nested result references a, which is visible at its definition point (the enclosing parameter). Capture is by value: adder(10.0) and adder(20.0) are distinct closures with distinct fixed environments.

composition — a user-defined higher-order pattern

Because a function value can be both returned and taken as an argument, higher-order combinators such as function composition are ordinary user code rather than built-ins. Given two unary functions:

function double_<V: numerics>(attribute<V> x) -> attribute<V> := x + x;
function inc<V: numerics>(attribute<V> x) -> attribute<V> := x + 1.0;

a compose function that returns "f after g" is written:

function compose(f: function; g: function) -> function
{
	function result<V: numerics>(attribute<V> x) -> attribute<V> := f(g(x));
}

attribute<float64> di  (Road) := compose(double_, inc)(Road/flow);  // double(inc(x)) = 2x+2 -> [4,6,8]
attribute<float64> id2 (Road) := compose(inc, double_)(Road/flow);  // inc(double(x)) = 2x+1 -> [3,5,7]

Composition nests, because a closure may itself be composed with a closure-producing call:

attribute<float64> quad (Road) := compose(compose(double_, inc), inc)(Road/flow); // 2x+4 -> [6,8,10]

passing a function to a function

A function value can be handed to another function through a parameter of type function, which then applies it:

function apply1(h: function; attribute<float64> y) -> attribute<float64> := h(y);

attribute<float64> viaArg (Road) := apply1(compose(double_, inc), Road/flow); // [4,6,8]

Here the composed closure compose(double_, inc) is the value bound to h, and apply1 applies it to Road/flow.

typed closures

The function result and parameter types above accept any function of matching arity. To constrain what may be passed or produced, declare a Function-signature and use it in place of the bare function keyword:

nuf = function<V: numerics, D: domains>(attribute<V> (D)) -> attribute<V> (D);

function twice(h: nuf) -> function
{
	function result<V: numerics>(attribute<V> x) -> attribute<V> := h(h(x));
}

attribute<float64> t (Road) := twice(inc)(Road/flow); // inc(inc(x)) = x+2 -> [3,4,5]

twice(inc) is a closure that applies its captured function h twice; twice(inc)(Road/flow) then applies that closure to the flow attribute.

resolved at the meta stage

All of this — producing a function value, capturing its environment, composing, passing and applying — happens before calculation. By the time the calculation engine runs, every application has been reduced away: adder(10.0)(Road/flow) is indistinguishable from Road/flow + 10.0, and compose(double_, inc)(Road/flow) from (Road/flow + 1.0) + (Road/flow + 1.0). Closures leave no trace at run time; they are purely a way of organising the model logic.

what a function value cannot be

A function value may be applied via a trailing (...), passed as an argument, or returned as a result. It may not be bound to a data item, and a data result may not be applied. Applying the result of a call whose result is data is rejected:

// double_(Road/flow) yields data, so applying it with (Road/flow) is an error
attribute<float64> bad (Road) := double_(Road/flow)(Road/flow);

Application of a result is always written explicitly, and over-application (supplying more arguments than a function's arity) remains an arity error.

see also

since version

20.9.0

Clone this wiki locally