hanki

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 three cases:

  1. A user writes f<T>(args) or f::<T>(args) in expression position. The error says type arguments cannot be supplied inline and points at the typed-binding fix.
  2. 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.
  3. A call leaves a bounded parameter unpinned that the callee dispatches through statically: def fresh<C: Blanker>() -> i32 whose body calls C.blank(), called as fresh() with nothing pinning C. There is no impl for that call to reach, and the error says cannot tell which type this call's C: Blanker bound 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 == y on a C, needs a value of C, which a parameter nothing pinned never had. Ok(1) == Ok(1) leaves the Err payload's type open and is accepted.

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.