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, bitsInference: 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.500000Three 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 typesConstraints 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.875000Four 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 to | The compiler said |
|---|---|
| name a capability that does not exist | 5212: 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 string | 5210: type argument 'string' … does not satisfy its constraint (requires numeric) |
compare with < under is numeric | 5116: data type 'U' cannot be used in comparison operation 'less' (requires ordered) |
pass an int64 and a float64 to one U | 5208: … one type parameter cannot bind two different types |
What this does not do
- No implicit conversions across a type parameter. The
5208refusal 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 Ucomposes 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 runChange 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.