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 T accepts a type argument.
  • Integer N accepts a statically evaluable integer value argument.
  • where T isa Real requires the type argument to be assignable to Real.
  • where N ∈ 1..4 requires the integer value to be in the inclusive range 1..4.
  • Generic where predicates may mention more than one parameter, such as N + M < 10.
  • Static value parameters may use Boolean, String, the integer-family built-ins, floating-point built-ins such as Float32 and Float64, 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.