Every language with generics answers the same three questions: how does a type parameter get its value, what may the body do with it, and what exists at run time? Java answers erasure, one compiled body, casts underneath. Go answers interfaces, sometimes boxed. C++ answered templates — a real body per type, but constraints arrived forty years late, so a bad call exploded inside the template. Mica takes the C++/Rust answer to the third question — monomorphization, one real specialization per concrete type — and makes the second question strict from the start: the body may only do to T what its capabilities grant.

The example is Generics.

The gen clause

function Twice(item : U) : U;
gen
    U is numeric;
begin
    Twice := item + item;
end;

gen declares the type parameters, and each one names its capabilities — what a concrete type must be able to do to stand in. numeric admits every integer and floating type and nothing else. The vocabulary is closed and the compiler will list it for you if you guess wrong:

analyzer error 5212: unknown capability 'comparable' in the constraint of
type parameter 'T'; a constraint is one of: numeric, ordered, equality,
logical, integral, fractional, dereferenceable, addressable, plain,
convertible, negatable, ordinal, bits

Inference: each call teaches the compiler a type

small := 5;              { int32   }
large := 3000000000;     { int64   }
fraction := 1.25;        { float64 }
WriteLn("  Twice of int32 5: %d", Twice(small));
WriteLn("  Twice of int64 3000000000: %lld", Twice(large));
WriteLn("  Twice of float64 1.25: %lf", Twice(fraction));
Inference: each call teaches the compiler a type
  Twice of int32 5: 10
  Twice of int64 3000000000: 6000000000
  Twice of float64 1.25: 2.500000

Three calls, three inferred type arguments, three real specializations in the binary — and the middle line is the proof they are real: 6,000,000,000 does not fit an int32, so the int64 doubling demonstrably ran in int64. A second call with a type already seen reuses its specialization; instantiation is deduplicated by the concrete type-argument set.

Inference unifies, it never converts: both arguments of a Smallest(a : U, b : U) must agree on one U:

analyzer error 5208: type parameter 'U' is inferred as both 'int64' and
'float64' in this generic call; one type parameter cannot bind two
different types

Constraints cut both ways

This is the part C++ shipped without for forty years, and it is checked in both directions.

The call’s duty: a type argument must satisfy the constraint —

analyzer error 5210: type argument 'string' for type parameter 'U' does
not satisfy its constraint (requires numeric)

The body’s rights: the generic may only do to U what the constraint grants. Smallest compares with <, so numeric is not enough — write it with U is numeric and the body is refused, before any call exists:

analyzer error 5116: data type 'U' cannot be used in comparison operation
'less' (requires ordered)
function Smallest(a : U, b : U) : U;
gen
    U is ordered;
begin
    if a < b then
        Smallest := a
    else
        Smallest := b;
end;

Because the body was checked against the constraint alone, no legal instantiation can ever break it — the C++ template error novel, three screens deep inside someone else’s header, is structurally impossible.

Generic types

Type parameters for the type section are declared at the unit level, and a generic type is one declaration standing for a family:

gen
    T is numeric;

type
    Vector of T = array[0..2] of T;

var
    vi : Vector of int32;
    vf : Vector of float64;

A generic function then rides the family — the vector arrives by value like every fixed array, and the element type follows as the result type:

function Total(v : Vector of U) : U;
gen
    U is numeric;
Generic types: one declaration, a family
  Total of a Vector of int32: 42
  Total of a Vector of float64: 0.875000

Four declarations on the screen; four real types and two real functions in the binary; nothing shared, boxed, or dispatched at run time.

What the compiler refused

The program tried toThe compiler said
name a capability that does not exist5212: unknown capability 'comparable' … a constraint is one of: numeric, ordered, equality, logical, integral, fractional, dereferenceable, addressable, plain, convertible, negatable, ordinal, bits
call Twice with a string5210: type argument 'string' … does not satisfy its constraint (requires numeric)
compare with < under is numeric5116: data type 'U' cannot be used in comparison operation 'less' (requires ordered)
pass an int64 and a float64 to one U5208: … one type parameter cannot bind two different types

What this does not do

  • No implicit conversions across a type parameter. The 5208 refusal is the policy: unify or say what you mean at the call.
  • Monomorphization costs binary size, visibly. Each concrete type-argument set is real code. That is the trade for zero runtime dispatch — the same trade C++ and Rust chose, made with open eyes.
  • The capability vocabulary is closed. Thirteen words, compiler-owned. There is no user-defined capability (no traits, no concepts) in this release — a constraint states machine-level abilities, not protocols.
  • Generic generators exist — a stream of U composes with the generator machinery — and this tutorial leaves them as its reader’s experiment.

Try it

git clone https://gitlab.com/mica-lang/mica-container.git
make -C mica-container/examples/Generics run

Change Smallest’s constraint to is numeric and meet 5116 pointing at the < inside the body — the two-sided check in one edit. Then call Twice("ab") and watch 5210 refuse the call instead of the body.

Next

Units and libraries — how a program grows past one file: exp deciding what escapes a unit, the import doctrine that makes nothing ambient, and a library whose emitted contract another build imports.