11. Generics
Type application uses angle brackets in type position:
list: List<i32>
m: Map<string, User>
The grammar is unambiguous: < opens a type-argument list only in type-parsing context, after :, ->, type, impl, struct, trait, def's generic-params slot, actor, and inside an existing <…> list. Elsewhere < and > are comparison operators, and the expression parser never switches modes.
Inference rule
Type arguments are inferred from value arguments at the call site, or from the surrounding type context: a typed let, a function-parameter type, or the return-type position of the enclosing function. There is no syntax for supplying type arguments inline to an expression. There is no parse<i32>("42") and no equivalent.
A required function type also constrains generic arguments inside throws payloads. Rows are unordered sets, and substitution removes duplicate effects. The checker accepts one most general assignment; incompatible assignments produce H0524 after argument and result constraints have settled. Ordinary function values require equal rows, while a closure may perform a subset of its slot's row.
Where inference cannot pin a type parameter, annotate the binding:
xs: List<i32> = List.empty() # the element type is pinned by the annotation
count = xs.length
The compiler emits a teaching diagnostic in five cases:
- A user writes
f<T>(args)orf::<T>(args)in expression position. The error says type arguments cannot be supplied inline and points at the typed-binding fix. - A let-binding without a type annotation leaves a type parameter unconstrained. The error says cannot infer type for
<name>and points at the same fix. - A call leaves a bounded parameter unpinned that the callee dispatches through statically:
def fresh<C: Blanker>() -> i32whose body callsC.blank(), called asfresh()with nothing pinningC. There is no impl for that call to reach, and the error says cannot tell which type this call'sC: Blankerbound applies to and names the callee (H0555). A bounded parameter nothing pins is otherwise fine:Set.empty()never calls through its bound, and a value-receiver dispatch,x == yon aC, needs a value ofC, which a parameter nothing pinned never had.Ok(1) == Ok(1)leaves theErrpayload's type open and is accepted. - A call leaves a type parameter unpinned whose runtime type the callee matches errors against. A
catcharm and athrowspayload match on the full declared error type (§7), and a generic callee matches against the type its caller instantiated it at.def guard!<T>() -> string [io]whose body catchesFailure<T>, called asguard!(), supplies no such type, and the error names the callable and the parameter (H0653). Where the parameter appears in no argument and no result, no annotation pins it: give the callable a parameter that names the type, or remove the type parameter. A bounded call is stricter. The implementation behind a forwarded dictionary is not knowable at the call, and every witness a trait method declares for its own generics is required at each call site, whichever implementation answers. A type parameter no such consumer needs remains legal unpinned, as case 3 describes. - A
catcharm or athrowin a body that receives no hidden identity arguments names a type or effect-row parameter, written directly or reached through a projection such asS.Error. An actor handler ormigratebody, an actor handler's or a trait method's parameter default, an actor state-field default orwherepredicate, aprovideop body, and atestblock take no witness window from any demand plan. An actorwherepredicate cannot hold athrow(H0605), nor can an actor handler's parameter default (H0658), and atestblock declares no generics, which leaves nothing in one to name a parameter. The error names the construct and the parameter (H0655). Match on a concrete type, or, outside a trait method's or an actor handler's parameter default, move the error matching into a callable whose signature names the parameter, where the call site supplies the runtime type; in those two defaults, remove the default and pass the value at each call or send.
The cost is that an API whose type parameter appears only in return position with no value parameter (empty<T>() -> List<T>, size_of<T>) needs either a value parameter or a typed binding at every call site. In return, the syntax is uniform, with no second-class escape hatch and no inline-type-argument commitment locking <> out of a future alternative.
Single-uppercase-letter names are reserved for type parameters
A bare single uppercase letter in type position (T, K, V) is always an implicit type parameter and never a concrete type. A struct, type or actor declared with a single-uppercase-letter name is therefore rejected at the declaration: such a name could never be referenced as itself. Give user-defined types a name of two or more characters (Point, Cmd, Tree). The rule runs in both directions, and an explicit <...> list is no exception to it. A type parameter is a single uppercase letter wherever it is declared, and def pick<Item>(…), struct Box<Item>, impl<Key, Val> … and a trait member's own def choose<Item>(…) are all rejected at the declaration (H0634). It is a rule and no convention. A multi-letter name in a <...> list parses, binds nothing, and resolves to an opaque nominal type: the declaration looks fine and every call fails instead with type mismatch: expected Item, found i32, naming a type the reader never declared. One position is exempt. A trait's own parameter list (trait Add<Rhs = Self>, trait Scale<Factor>) has its parameters bound by the trait machinery and not by the name's form, a longer name there binds correctly, and it remains legal.
The lowercase counterpart applies to effect rows. A bare single lowercase letter inside […] (e, r) is an effect variable (§6) and never an effect name. Built-in and user effects are multi-letter (io, fs, Database) or capitalised (Crash), and the two never collide.