65 Generics
Predica declarations may be parameterised by type parameters and by static value parameters. The same angle-bracket syntax is used at declaration sites and application sites:
value Point<Type T, Integer N>
{
T coordinate[N];
}
value Holder
{
Point<Real, 3> position;
}
A parameterised declaration introduces a generic declaration shell. A concrete declaration is
created when an application is needed, such as Point<Real, 3>. Repeated uses of the same
canonical application denote the same concrete declaration. For type declarations this means the
same concrete type; for constants and functions the concrete declaration may be an internal ordinary
constant or function used to encode the application.
Parameter declarations
The declaration spelling follows ordinary Predica declarations: Type T for a type parameter and
Integer N for a static value parameter. The parameter kind or value type is written first, and
the parameter name is written second. Untyped parameters, such as Point<T, N>, and name-first
parameters, such as T: Type, are invalid in declarations.
Parameters may carry metadata and trailing defaults. Constraints are written in a generic
where clause over the already-declared parameters:
value Vector<@role("coordinate") Type T = Float64,
@count Integer N = 3>
where T isa Real, N ∈ 1..4
{
T coordinate[N];
}
value Array2d<Type T, Integer N, Integer M>
where T isa Real, N > 0, M > 1, N + M < 10, isOdd(N)
{
}
Type Taccepts a type argument.Integer Naccepts a statically evaluable integer value argument.where T isa Realrequires the type argument to be assignable toReal.where N ∈ 1..4requires the integer value to be in the inclusive range1..4.- Generic
wherepredicates may mention more than one parameter, such asN + M < 10. - Static value parameters may use
Boolean,String, the integer-family built-ins, floating-point built-ins such asFloat32andFloat64, and enum types. - An enum-valued static parameter uses the enum type as the parameter kind, such as
Axis A. - Defaults must form a trailing suffix of the parameter list.
- Parameter metadata belongs to the generic declaration parameter, not to each concrete instantiation separately.
An enum type may be used as the value type of a static value parameter. The selected enum attribute is part of the generic application's canonical identity:
enum Axis
{
x,
y,
z
}
value AxisTag<Axis A>
{
Integer id;
}
Applications and canonical form
A generic application supplies type and value arguments in source order:
Point<Real, 3>
Vector<Integer>
Vector<Float64, 2+1>
AxisTag<Axis.x>
AxisTag<x>
Defaults are expanded before canonical identity is computed. Typed static value arguments must
produce a canonical static value compatible with the declared parameter type. Boolean, string,
integer, and floating-point arguments are statically evaluated to literal values. Integer evaluation
is exact and conservative: literals and previously-bound integer parameters may be combined with
unary +, unary -, +, -, *, exact /, and non-negative ^; non-integral division,
division by zero, and overflow are invalid. Therefore Vector<Float64, 2+1> and
Vector<Float64, 3> denote the same concrete type and may be rendered in canonical form as
Vector<Float64, 3>.
Enum-valued arguments must name an attribute of the declared enum type. The qualified spelling,
such as Axis.x, is always valid. In the expected-type context of a generic application, an
unqualified attribute such as x in AxisTag<x> is also valid when it names an attribute of
the expected enum type. It canonicalises to the qualified enum attribute, so AxisTag<x> and
AxisTag<Axis.x> denote the same concrete type.
Bare use of a generic type is invalid. If Box is declared as value Box<Type T>, write
Box<Integer> rather than Box.
Supported declarations
Value, variant, entity, and enum declarations may be parameterised:
value Box<Type T>
{
T value;
}
variant Maybe<Type T>
{
T some;
absent;
}
entity Node<Type T>
{
Integer id;
T value;
key { id };
}
enum Offset<Integer N>
{
first = N,
second,
last = N + 10
}
An enum's representation-value expressions may depend on its generic parameters. They are evaluated separately for each instantiation, and the resulting values must be distinct and representable by the enum's storage type. Generic constraints may restrict the valid instantiations.
Generic constants and functions
Constants and functions may also be parameterised:
const plus<Integer N = 2> = N + 1;
func T identity<Type T>(T value) = value;
extern "C++:math"
{
T clamp<Type T, Integer N = 3>(T value)
where T isa Real, N ∈ 1..4;
}
Generic constants require an explicit angle application, even when all parameters have defaults:
write plus<> rather than bare plus. Generic functions may be called with explicit generic
arguments:
identity<Integer>(1)
plus<2+1>
Function generic arguments may also be inferred when they are unambiguous from literal ordinary
arguments or trailing defaults. For example, identity(1) may infer identity<Int64>(1).
Inference is intentionally conservative: when a required parameter cannot be inferred, the call must
write explicit generic arguments.
Ordinary and generic functions may share a source name and participate in the same overload set. Each generic candidate is instantiated provisionally using explicit arguments, inference, and defaults before its viability and specificity are compared with the other candidates. The Function Overload Resolution chapter defines this process.
Instantiation model
Generic instantiation substitutes canonical argument bindings into the declaration body and then uses the ordinary schema rules for attributes, keys, constraints, derived attributes, embeds, function bodies, and schema evolution. This keeps generic behaviour aligned with non-generic declarations and gives the same diagnostics after substitution wherever possible.
An embed declaration whose type mentions a generic parameter must instantiate to a named type. For
example embed T as item; is valid when T is supplied by a named value or enum type, but not when
the application supplies an anonymous structural type.
Recursive generic type and function references may cross an explicit schema reference boundary, such as an attribute type or function call. Generic constants do not have such a boundary and recursive generic constant instantiations are invalid.
Annotation behaviour
Annotations for generic declarations should preserve the declaration parameters rather than choosing
an incidental concrete instantiation. Debug tooling may additionally show concrete instantiated
denotations, but the product-facing view should keep formulas such as T value parameterised.
Cross-component use
Generic applications may refer to declarations imported from another component, either by selected import or by component alias:
using acme.Library { Box, identity };
using acme.Library as lib;
value Holder
{
Box<Integer> localName;
lib.Box<Real> qualifiedName;
Integer copied = identity<Integer>(1);
}
Concrete instantiations belong to the exported generic declaration, while renderers may qualify the application name as needed to keep the reference stable outside the exporting component.