Skip to content

Generic function

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 generic function is a function definition that carries one or more type variables in angle brackets. A single body then serves every value type (and every domain) that the variables can stand for, instead of being written out once per value type.

function DoubleG<V: numerics>(attribute<V> x) -> attribute<V> := x + x;

Here V is a type variable, numerics is its constraint, and the parameter, result and body are all written in terms of V. Applied to a float64 attribute DoubleG returns a float64; applied to a uint32 attribute it returns a uint32 — one body, with the actual value type chosen at each call. (A body that mixes in a literal of a specific value type, such as x + 1.0, is correspondingly restricted to that literal's value types.)

type variables and constraints

A type variable is declared in the <...> clause between the function name and its parameter list, written name: constraint. The constraint restricts which value types the variable may take, and the check is enforced whenever the function is applied. Type variables appear in attribute<V> and parameter<V> positions, and in the result argument after ->.

The constraint keywords name classes over the closed universe of GeoDMS value types:

constraint admits
any any value type
numerics all numeric value types (integer and floating point)
integers all integer value types
floats floating-point value types
uints / unsigned_ints unsigned integer value types
sints / signed_ints signed integer value types
domains value types usable as a [[domain unit
points point value types
domain_points point types eligible as a grid domain

The testcase battery exercises numerics, floats, signed_ints, uints, any on value variables and domains on domain variables.

multiple type variables

Several variables may be declared in one clause; each is instantiated independently at the call:

function Mix<V: numerics, W: numerics>(unit<uint32> D; attribute<V> a (D); attribute<W> b (D))
-> attribute<float64> (D) := float64(a) + float64(b);

attribute<float64> mixed (Road) := Mix(Road, Road/flow, Road/lanes);

V binds to the value type of a and W to that of b, so the two arguments need not share a value type. When the same variable appears on more than one parameter, all those arguments must agree — this is how a signature expresses "same type here and there".

domain variables versus unit parameters

Two different mechanisms both give a function reach over many types, and it is worth keeping them apart.

A plain unit parameter is already a form of polymorphism: unit<uint32> D is a unit variable that is instantiated by whatever unit is passed, with no <...> clause needed. The types of the other parameters then follow from it.

A <D: domains> variable is the genuine generic layer applied to a domain. It lets a function be written against an unknown domain and require several arguments to live on the same domain:

function ratio<V: numerics, D: domains>(attribute<V> a (D); attribute<V> b (D))
-> attribute<V> (D) := a / b;

attribute<float64> ok    (Road) := ratio(Road/flow, Road/cap);
attribute<float64> mixed (Road) := ratio(Road/flow, 2.0);      // void broadcasts into D

Both a and b are declared over the same variable D, so their domains must match; a void argument such as the literal 2.0 broadcasts into D. A domain variable can also constrain a unit argument directly, as in function CountOf<D: domains>(unit<D> E) -> parameter<uint32> := uint32(#E);.

instantiation per call

A generic function is never expanded into per-type copies. At each application the value class of every generic argument is derived from that argument, checked against the variable's constraint, and checked for consistency across the parameters that share a variable. The one definition is then reduced with those types filled in. Because the reducer is type-agnostic, generic locals inside the body come for free, and the result type is derived from the body: TwiceG<V: numerics>(...) := x + x produces a float64 result for a float64 argument and a uint32 result for a uint32 argument, from the one body.

function TwiceG<V: numerics>(unit<uint32> D; attribute<V> x (D)) -> attribute<V> (D) := x + x;

attribute<float64> f2 (Road) := TwiceG(Road, Road/flow);
attribute<uint32>  l2 (Road) := TwiceG(Road, Road/lanes);

constraint violations are errors

A constraint that cannot be satisfied is reported at application time, not silently ignored.

Passing an argument outside the constraint fails. Applying TwiceG<V: numerics> to a string attribute reports "does not satisfy 'V: numerics'".

Binding the same variable to two different value types fails. function Same<V: numerics>(...; attribute<V> a (D); attribute<V> b (D)) applied to a float64 and a uint32 argument reports an inconsistent instantiation of type variable 'V'.

The same holds for domain variables: with ratio<V: numerics, D: domains>, calling ratio(Road/flow, Rail/flow) — two attributes on different domains — cannot instantiate D consistently and is rejected. (These cases are the _neg entries in the testcase suite.)

overloaded variants

Uniform polymorphism — one body, one derivation scheme — is the job of type variables above. When behaviour must genuinely differ per type, a function item may instead hold named variant blocks, each a full function, and the call dispatches on the argument value types:

function describe
{
    variant asFloat(attribute<float64> x (D)) -> attribute<float64> (D) := x * 100.0;
    variant asInt  (attribute<int32>   x (D)) -> attribute<int32>   (D) := x + x;
}
attribute<float64> df (D) := describe(D/f);   // picks asFloat
attribute<int32>   di (D) := describe(D/i);   // picks asInt

The most specific matching variant is selected; overlapping variants that are not strictly ordered by specificity are rejected at definition, and an ambiguous or unmatched call is an error listing the variants. Variant parameters may themselves be generic, so the two mechanisms combine.

see also

since version

20.9.0

Clone this wiki locally