Skip to Content

Generics

Generics decouple “the shape of a type” from “the concrete type”: types and functions alike take type parameters, and the instantiation is fixed only at the call site. Constraints are expressed with where clauses; the constraint surface is the capability surface.

Generic Types

struct Box<T> { public var value: T; public func Get() -> T => value; public func Replaced(nv: T) -> Box<T> => new Box<T>() { value: nv }; } val bi = new Box<int>(); bi.value = 42; print(bi.Get(), bi.Replaced(7).Get()); val bs = new Box<string>(); bs.value = "word"; print(bs.Get());
42 7 word

Box<int> and Box<string> are different instantiations, each with its own member surface. Nested instantiations are legal: Box<list<int>> holds a list.

Generic Functions

func First<T>(xs: list<T>) -> T? { if xs.Count == 0 { return null; } return xs[0]; } val empty = new list<string>(); print(First(empty) == null); val names = new list<string>(); names.Add("a"); print(First(names));
True a

where Constraints and Admitted Operators

Constraints are written before the return type (clause order: uses → fail → where → ->). A constraint declares “what T must be able to do”, and operators in the generic body are admitted according to the constraint:

import Math.*; func Sum2<T>(a: T, b: T) where T: Numeric -> T { return a + b; } print(Sum2(2, 3), Sum2(1.5, 2.5));
5 4

Numeric is a protocol from std Math (the four arithmetic operations plus equality); built-in scalars satisfy it implicitly, and a + b is admitted by the constraint protocol’s operator signatures. Multiple constraints are allowed: where T: Ordered, Clone puts several constraints in one clause, or use several where clauses.

Constructor Signature Constraints

When a constraint protocol declares init(), the generic body gains the ability to write new T(), constructed via the concrete type’s constructor at instantiation:

import Collections.*; protocol Makeable { init(); } struct Token { public var stamp: string = "made"; } func Make<T>() where T: Makeable -> T { return new T(); } val t = Make<Token>(); print(t.stamp);
made

Without an init constraint, new T() is a compile-time error outright, because there is no way to know whether the T on the other side permits parameterless construction.

Unary Operator Constraints

Constraint protocols can also declare unary operators. operator -(self) -> Self; admits -x inside the generic body:

protocol Neg { operator -(self) -> Self; } func Negate<T>(x: T) where T: Neg -> T { return -x; } print(Negate(5));
-5

On the argument side, satisfaction is judged by built-in operator capability: uint / byte have no unary negation.

Notes

  • Constraints go before the return type (uses → fail → where → ->), never to the right of ->.
  • Multiple constraints can coexist: where T: Ordered, Clone puts several in one clause, or use several where clauses, one per type parameter.
  • Using an operator the constraint surface lacks reports MS3144; the error message names the protocol to add.
  • list<T> / map<K, V> are ready-made examples of generic containers; for the method surface see Collections and Text.
Last updated on October 11, 2026