Generics
E# generics are reified: every instantiation is a real closed type or method at runtime, never an
erased object. Option<int> is a distinct closed generic struct; List<Point> holds Points by their
actual representation. This page specifies parameterization, the open/closed distinction, how type
arguments are inferred, and how a generic type conforms to interfaces.
In this specification
Section titled “In this specification”- Declarations, instantiation, and conformance covers reification, arity identity, generic methods, and interfaces.
- Inference and default values covers generic call inference,
unconstrained type parameters, and
default/default(T).
Type parameters
Section titled “Type parameters”A generic entity introduces type parameters in angle brackets after its name:
Generics = "<" identifier { "," identifier } ">" .They appear on struct, class, union / ref union, interface, and func:
struct Pair<A, B> { first: A, second: B }class Box<T> { v: T init(x: T) { self.v = x } func get() -> T = self.v }union Tree<T> { leaf(value: T), node(left: *Tree<T>, right: *Tree<T>) }interface IMap<K, V> { func get(k: K) -> V }func identity<T>(value: T) -> T = valueA type-parameter name follows the type-name casing convention (PascalCase: T, TValue) and is in scope
throughout the declaration it heads. enum is never generic — it is an integral type.
Reification
Section titled “Reification”Each instantiation is its own CLR closed type; there is no type erasure. A generic argument that is a
user type stays that type through the metadata, never boxed to object: a struct field of type T
substituted with int is laid out as an int, and match / equality / field access on a closed
instantiation see the concrete type. Generics nest — Box<Box<int>> is a closed type whose payload is
itself a closed Box<int>, and a field may be a nested construction like Dictionary<string, List<T>>,
closed when the enclosing type is. The cost model is the CLR’s: reference instantiations share code, value
instantiations are specialized.
Open vs. closed
Section titled “Open vs. closed”A bare generic name (Pair, no arguments) denotes the open definition; it appears only where an open
type is meaningful — the type’s own body (its field and method signatures reference the open parameters)
and as the target of an instantiation. A name with arguments (Pair<int, string>) is a closed type,
and is what values, fields, parameters, returns, and locals are typed by. Resolving a bare generic name in
value or type position yields the open definition; supplying arguments closes it.
Instantiation and type identity
Section titled “Instantiation and type identity”A generic name is closed by supplying one type argument per declared parameter; the count of arguments
is the arity the use site demands (below). Each argument is itself a type — a primitive,
a user type, another closed generic (Box<Box<int>>), a tuple, a pointer, or a delegate type.
Two closed types are the same type exactly when they close the same generic definition with the same
type arguments, compared by ordinal type identity: Pair<int, string> denotes one type wherever it
appears, and Pair<int, int> is a different type from Pair<string, int>. Because instantiations are
reified, each closed type carries its own metadata and — for value instantiations — its own layout,
independent of every other instantiation of the same definition.
Arity keying
Section titled “Arity keying”Types are keyed by name and arity, mirroring the CLR’s Name`N convention. The consequences:
- a generic
struct Foo<A, B>(arity 2) and a non-genericstatic Foo(arity 0) coexist under the one nameFoo. The standard library uses exactly this to pair theResult<TValue, TError>value type with aResultstatic factory class (Result.Ok/Result.Error) —struct Tag<T>alongsidestatic Tagresolvest.vandTag.id()with no collision; - two generic types of the same name and different arity coexist —
Cell<A>andCell<A, B>are distinct types (Cell`1andCell`2); - only a genuine same-name and same-arity redeclaration is an error (ES2152).
Every use site resolves by the arity it demands: a bare name → arity 0, an instantiation Foo<…> → the
count of arguments.
Recursion through generics
Section titled “Recursion through generics”A generic type that refers to itself does so through a pointer, the same rule the value/reference
split imposes on non-generic types (Type system → value types). A
recursive union or struct carries its self-reference as *T:
union Tree<T> { leaf(value: T), node(left: *Tree<T>, right: *Tree<T>) }A value struct that contains itself by value — directly (next: Node) or through a generic wrapper
that holds it by value (children: List<Node>) — is ill-formed
(ES2002); break the cycle with *T (next: *Node, children: List<*Node>).
A class is heap-native and may hold itself by reference without a pointer.
Generic methods
Section titled “Generic methods”A method or function may carry its own type parameters, independent of any on its enclosing type. On a generic type, the receiver’s parameters ride on the declaring type and the method’s remaining parameters become the method’s own:
class Box<T> { v: T init(x: T) { self.v = x } func count<U>(other: U) -> int = 3 // U is the method's own parameter}// b.count<string>("x") → Box`1<int>::count<string>(string) -> intA generic method shows the same split: in
func (w: Wrap<T>) mapped<T, U>(f: Func<T, U>) -> Wrap<U>, T is pinned by the Wrap<T> receiver and U
is the method’s own (Functions → methods). It emits as
Wrap`1<T>::mapped<U>(Func<T, U>) -> Wrap<U> — a generic method on a generic type.
Generic interface conformance
Section titled “Generic interface conformance”A generic type conforms to an interface by naming it after :, exactly as a non-generic type does
(Type system → interfaces); conformance is nominal.
The interface may be parameterized by the type’s own parameters, by a closed instantiation, or be
non-generic:
interface IBox<T> { func get() -> T }class Box<T> : IBox<T> { v: T init(x: T) { self.v = x } func get() -> T = self.v }
interface IMap<K, V> { func get(k: K) -> V }class One<K, V> : IMap<K, V> { val: V init(v: V) { self.val = v } func get(k: K) -> V = self.val }This holds for a struct, a class, a BCL generic interface (Ver<T> : IComparable<Ver<T>>), and a
non-generic interface whose members are independent of the type parameter (Box<T> : ITagged). A value
struct conforming to an interface boxes at the interface boundary — passing Bag<int> where
ICount<int> is expected boxes the value — which is the same value-through-interface rule as a non-generic
struct (Type system → boxing).
Generic base classes
Section titled “Generic base classes”A class may extend a generic base, closing it over type arguments written after :. The arguments
are the base’s, in the order the base declares them — they need not match the derived type’s own
parameters:
open class Animal<T> { tag: T init(t: T) { self.tag = t } }class Dog<T> : Animal<T> { init(t: T) : base(t) { } } // base closed over the derived's own T
open class Box<T> { value: T init(v: T) { self.value = v } }class IntBox : Box<int> { init(v: int) : base(v) { } } // base closed over a concrete argument
open class Pair<X, Y> { fst: X snd: Y init(f: X, s: Y) { self.fst = f self.snd = s } }class Rev<A, B> : Pair<B, A> { init(f: B, s: A) : base(f, s) { } } // arguments reorderedThe emitted extends clause and the : base(...) constructor call are both hosted on the closed base
instance — Dog<int> extends Animal<int>, IntBox extends Box<int>, and Rev<int, string> extends
Pair<string, int> — so a derived value is assignable to the closed base (Dog<int> is an Animal<int>),
exactly as reification requires; an open generic base would be invalid metadata. : base(args) resolves
against the base’s constructor with matching arity, its parameters substituted through the same closure, so
base(t) above calls Animal`1<int>::.ctor(T). A generic base carries its arguments; a non-generic
base (class TagAttribute : Attribute) names no arguments and closes nothing. As with any inheritance the
base must be the first entry after : and declared open or abstract
(Declarations → class).
This is the same closed-base wiring the reified ref union subclass relies on: Expr_add : Expr and a
generic Box_full<T> : Box<T> extend their reified base over its arguments through this rule
(Declarations → reference union).
Variance
Section titled “Variance”E# provides no syntax to declare variance on a type parameter — there is no in / out annotation —
so an E# generic interface is invariant in its parameters: IBox<Derived> is not an IBox<Base>.
Variance is a property of a generic interface’s own metadata, so when E# conforms to or consumes a
variant BCL interface — the covariant IEnumerable<out T>, the contravariant IComparer<in T> — that
interface’s declared variance applies, because it travels with the interface, not with E#‘s use of it.
Type-argument inference
Section titled “Type-argument inference”A generic call supplies its type arguments explicitly or has them inferred. Explicit arguments are
written in angle brackets at the call (identity<int>(x), b.count<string>("x"), w.mapped<int, string>(f))
and always work. Inference applies when they are omitted, by three steps:
- Receiver inference. For a method call, the receiver’s closed type pins every type parameter that
appears in the receiver position —
w : Wrap<int>pinsT = intforw.mapped(...); for a BCL extension method, the receiver pins the source element type (xs : List<int>pinsTSource = intforxs.Select(...)). - Argument inference. Each remaining parameter is pinned by matching a value argument’s type against the corresponding parameter’s declared type, descending through constructed generics.
- Lambda-body inference. A lambda argument participates in both directions: its parameter types bind
from the already-pinned parameters (so
(x) => …typesxasintabove), and its body-inferred return type then pins any type parameter that appears only in the lambda’s result position.
let w = Wrap<int> { v: 3 }let a = w.mapped((x) => x + 5) // T=int (receiver), x:int, body→int, U=int → Wrap<int>let b = w.mapped((x) => x.ToString()) // U=string → Wrap<string>let c = w.mapped((x) => x + 5).mapped((y) => y * 2) // inference threads through the chain
let xs = List<int>() // … populatedlet n = xs.Select((x) => x + 1).Sum() // Select<int,int> → IEnumerable<int> → Sum() → intlet m = xs.Where((x) => x > 3).Count() // Where<int>(Func<int,bool>) → intThe same algorithm serves a user generic method and a BCL generic extension (Select / Where / Any
/ All) uniformly. If a type parameter is pinned by neither the receiver, an argument, nor a lambda body,
inference fails for that call and the type arguments must be given explicitly. Inference does not flow
from the expected result type back into the call.
derive on generic types
Section titled “derive on generic types”derive equality and derive debug (Declarations → derive) apply
to generic struct. The generated members target the self-instantiation: derive equality on
Pair<A, B> implements IEquatable<Pair<A, B>> and compares field-by-field, and distinct closed
instantiations are independent — Pair<int, int> equality and Pair<string, string> equality are
separate closed methods. derive debug renders the closed value (Pair { first = 3, second = 4 }).
Type parameters are unconstrained (except unmanaged)
Section titled “Type parameters are unconstrained (except unmanaged)”A type parameter is opaque at the source level: there is no general constraint clause (no where, no
<T : I>). Within a declaration a T is used by storing it (a field, a collection, a tuple), passing
it, returning it, taking default(T), or comparing it via a derived equality — operations that hold
for any type. There is no way to call a T-specific member on a bare T, because nothing has been
asserted about T; reach the concrete operations through a closed instantiation, an interface-typed
parameter, or a delegate parameter (Func<T, U>). Reification makes this pay off: at the closed
instantiation the argument’s real representation and behavior are present.
The one capability bound the current surface accepts is unmanaged:
Generics = "<" TypeParam { "," TypeParam } ">" .TypeParam = identifier [ ":" Bound ] .Bound = "unmanaged" .func writeArray<T: unmanaged>(…) emits the CLR unmanaged constraint (the NotNullableValueType flag plus
a System.ValueType modreq(UnmanagedType) constraint, byte-identical to C#‘s where T : unmanaged), so
MemoryMarshal.AsBytes<T>, span reinterpretation, and Unsafe.SizeOf<T> type-check against T. It is
specified in Low-level & unmanaged. Other type-parameter
bounds — the type-set form like func max<T: int | long | double>(…) — remain part of the union-types
direction on the roadmap.
default(T)
Section titled “default(T)”default(T) yields the zero value of T: 0 / false / '\0' for the numeric and char primitives,
nil for a reference type or *T, and the all-zero value (initobj) for a value struct, tuple, or other
value type — including an unconstrained type parameter, where it lowers to initobj on the reified
argument rather than a null reference.