-
Notifications
You must be signed in to change notification settings - Fork 1
Closure
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.
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.
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]
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.
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.
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.
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.
20.9.0
GeoDMS ©Object Vision BV. Source code distributed under GNU GPL-3. Documentation distributed under CC BY-SA 4.0.