Skip to content

Advanced Generics

This chapter covers generic library design, method-specific requirements, and runtime type matching. Read Generics, Traits and Implementations, and Classes first. The examples build on the basic type parameters and where clauses from Part 7.

You can write extensions that work with generic types. As with other extensions, give the extension a name and import it before using its methods:

package example
import example.ArrayHelpers
extension ArrayHelpers<T> for Array<T> =
function isEmpty(self): Bool =
self.length == 0
function first(self): Option<T> =
if self.isEmpty() then
None
else
Some(self[0])
function main(): Unit =
let numbers = [|1, 2, 3|]
assert numbers.isEmpty() == false
assert numbers.first() == Some(1)

Use where T: class when an operation needs class identity:

function sameInstance<T>(left: T, right: T): Bool where T: class =
ClassIdentity.equals(left, right)
function instanceHash<T>(value: T): Int64 where T: class =
ClassIdentity.hash(value)

The bound permits concrete and generic classes, aliases of classes, and generic parameters already constrained by class. A superclass bound such as where T: Animal also satisfies it. A Box<Int32> is a class even though its element type is not.

Records, enums, arrays, newtypes, primitives, Any, and interface objects do not satisfy class. The bound grants neither constructors nor arbitrary members, and it does not automatically implement Equatable or Hashable.

Combine it with trait bounds using where T: class + Display. An unconstrained generic function cannot call the identity functions just because its current callers happen to pass classes. See Class Identity for the equality and hash contracts.

A primitive subtype bound can restrict a method to a particular element type. For example, where T: Uint8 allows ReadonlySlice<T> to widen to ReadonlySlice<Uint8>. It accepts Uint8 and the uninhabited subtype Never; it does not convert other integer types or imply the class constraint. Stream.writeToOutputStream uses this bound to expose byte output directly on its generic module.


A method’s where clause can constrain type parameters declared by its enclosing module, class, implementation block, or extension block. The method does not need to declare its own type parameters:

class Holder<T>(public value: T) =
public function render(self): String where T: Display = self.value.format()
public function unchanged(self): T = self.value

render requires T: Display; constructing a Holder<T> or calling unchanged does not. The bound applies throughout the method’s signature, body, and nested closures, and does not become available to sibling methods.

Callers must satisfy these requirements when calling the method or taking a method reference. Generic callers prove them using their own bounds:

function renderHolder<T>(holder: Holder<T>): String where T: Display = holder.render()

A method can use the class’s type parameter together with a type parameter of its own. For example, this box can search the value it holds:

trait Contains<Item> =
function contains(self, item: Item): Bool
class SearchBox<T>(public value: T) =
public function contains<U>(self, item: U): Bool where T: Contains<U> =
self.value.contains(item)

Here, T is the type stored in the box. U is the type of item passed to contains. Read where T: Contains<U> as: the stored type must support searching for this kind of item.

For a concrete example, a range can contain integers:

record Range = start: Int32, end: Int32
implement Contains<Int32> for Range =
function contains(self: Range, item: Int32): Bool =
item >= self.start && item <= self.end
let box = SearchBox(Range { start = 1, end = 10 })
box.contains(5) // T is Range, U is Int32: allowed; returns true
box.contains(true) // U is Bool: rejected; Range does not implement Contains<Bool>

The same box keeps its stored type, Range, across calls. Each call chooses the method’s U from the item being searched for and checks the corresponding Contains<U> requirement.

Use different names for the two parameters: declaring contains<T> here would reuse the class’s T, which the compiler rejects.


An implementation must accept the calls that its trait promises to callers. It cannot add an extra requirement to one of those methods. For example, the compiler rejects this implementation:

trait Inspect =
function inspect(self): Unit
record Box<T> = value: T
implement <T> Inspect for Box<T> =
function inspect(self: Box<T>): Unit where T: Display = ()

The implementation says every Box<T> implements Inspect, but its method would only work for some choices of T. A caller that knows only Inspect has no way to satisfy that extra requirement.

Putting where T: Display on the implementation block instead restricts which boxes implement Inspect. Every box that does implement the trait then has the promised method.

The same rule applies to class overrides: a child method must accept the calls allowed by its parent method. It cannot add an extra bound. A requirement already guaranteed by the enclosing class or implementation block can be repeated on the method.

An implementation also inherits the bounds declared on a generic trait method. Renaming the method’s type parameter, such as from T to U, does not remove those requirements.


Variance controls whether one use of a generic type can be assigned to another. For example, every Int32 value can be used as Any, but that does not by itself mean a Box<Int32> can be used as Box<Any>.

With a plain parameter, such as Box<T>, different type arguments stay separate: Box<Int32> cannot be assigned to Box<Any>. This is called invariance.

Use out T when the type provides values of T. A reader that produces integers can be used where a reader of any values is expected:

class Reader<out T>(public value: T)
let integers = Reader(42)
let anyValues: Reader<Any> = integers

This is called covariance. The compiler checks that an out parameter is not used where a caller could supply a value of that type, such as a method argument or a mutable field.

Use in T when the type accepts values of T. Something that accepts any value can also accept integers:

class Consumer<in T>() =
public function accept(self, value: T): Unit = ()
let anyValues = Consumer<Any>()
let integers: Consumer<Int32> = anyValues

This is called contravariance. An in parameter cannot be used where the caller receives a value of that type, such as a method return type. Start with plain type parameters; add in or out when your API needs these conversions.

There is one restriction when combining these method bounds with in or out on a class parameter. Consider this hypothetical declaration, which the compiler rejects:

class Formatter<in T>() =
public function render(self, value: T): String where T: Display = value.format()

Normally, in T lets you use a Formatter<Any> as a Formatter<Int32>: something that accepts any value can also accept an integer. But that would cause a problem here:

let original = Formatter<Any>()
let integers: Formatter<Int32> = original
integers.render(42)

The last call appears valid because Int32 implements Display. However, the object is still the original Formatter<Any>. Its render method is unavailable because Any does not implement Display. Assigning the object to a differently typed variable does not create a new object or a new implementation of its method. A virtual call uses the method belonging to that original object.

To prevent this, use plain T, without in or out, when a virtual method adds a requirement on T:

class Formatter<T>() =
public function render(self, value: T): String where T: Display = value.format()

Plain T is called invariant. It prevents the conversion from Formatter<Any> to Formatter<Int32> shown above. The same restriction applies to parameters mentioned inside a method’s bound: for where T: Contains<U>, both class parameters T and U must be invariant if the class does not already guarantee that requirement.

Alternatively, require Display on the whole class:

class Formatter<in T>() where T: Display =
public function render(self, value: T): String = value.format()

Now every allowed Formatter<T> has a working render method. You cannot construct Formatter<Any> in the first place, so the problematic conversion cannot arise. The class-level bound does not impose the extra invariance rule; the usual rules for in and out still apply.


You can use pattern matching to check types at runtime. This is particularly useful in generic functions where you need to handle different types differently.

The most common use case is determining the actual type in a generic function:

record Box<T> =
value: T
function getBoxType<T>(box: T): Int32 =
match box with
case _: Box<Int32> => 1
case _: Box<String> => 2
case _ => 0
let intBox = Box { value = 42 }
let strBox = Box { value = "hello" }
getBoxType(intBox) // returns 1
getBoxType(strBox) // returns 2

The _: Type syntax checks if the value has the specified type without binding it to a variable.

Type patterns work with non-generic types too, letting you distinguish between different user-defined types:

record Cat = name: String
record Dog = name: String
function getAnimalType<T>(animal: T): Int32 =
match animal with
case _: Cat => 1
case _: Dog => 2
case _ => 0
let cat = Cat { name = "Whiskers" }
let dog = Dog { name = "Rex" }
getAnimalType(cat) // returns 1
getAnimalType(dog) // returns 2

Use the type name with explicit type arguments in a pattern:

record Box<T> =
value: T
function describeBox(box: Box<String>): Int32 =
match box with
case _: Box<String> => 1
case _ => 0
let b = Box { value = "hello" }
describeBox(b) // returns 1

You can match on generic types instantiated with user-defined types:

record Person = name: String
record Container<T> =
item: T
function checkContainer<T>(c: T): Int32 =
match c with
case _: Container<Person> => 1
case _ => 0

For types with multiple type parameters, specify all type arguments. This allows distinguishing between different instantiations:

record Pair<A, B> =
first: A
second: B
function getPairType<T>(p: T): Int32 =
match p with
case _: Pair<Int32, String> => 1
case _: Pair<String, Int32> => 2
case _: Pair<String, String> => 3
case _ => 0
let p1 = Pair { first = 42; second = "hello" }
let p2 = Pair { first = "hello"; second = 42 }
let p3 = Pair { first = "a"; second = "b" }
getPairType(p1) // returns 1
getPairType(p2) // returns 2
getPairType(p3) // returns 3

Pattern matching works with generic enums too:

enum Maybe<T> =
Just(T)
Nothing
function checkMaybe<T>(m: T): Int32 =
match m with
case _: Maybe<String> => 1
case _: Maybe<Int32> => 2
case _ => 0

And with generic classes:

class Wrapper<T>(value: T) =
function get(self): T = self.value
function checkWrapper<T>(w: T): Int32 =
match w with
case _: Wrapper<String> => 1
case _: Wrapper<Int32> => 2
case _ => 0

You can distinguish between different generic types that have the same type argument:

record Box<T> =
value: T
record Wrapper<T> =
inner: T
function getType<T>(x: T): Int32 =
match x with
case _: Box<Int32> => 1
case _: Wrapper<Int32> => 2
case _ => 0
let b = Box { value = 42 }
let w = Wrapper { inner = 42 }
getType(b) // returns 1
getType(w) // returns 2

You can bind the matched value to a variable:

record Box<T> =
value: T
function processBox<T>(x: T): Int32 =
match x with
case box: Box<Int32> => box.value
case box: Box<String> => box.value.length
case _ => 0
let intBox = Box { value = 42 }
let strBox = Box { value = "hello" }
processBox(intBox) // returns 42
processBox(strBox) // returns 5

The wildcard pattern _ catches any type that doesn’t match the specific patterns:

record Box<T> =
value: T
function boxCode<T>(x: T): Int32 =
match x with
case _: Box<Int32> => 1
case _: Box<String> => 2
case _ => 99 // Fallback for other types
let b = Box { value = true }
boxCode(b) // returns 99 (Box<Bool> falls through to wildcard)

A default is a method body supplied by the trait. The implementing type can use it by leaving that method out of its implementation block.

In this example, pick<T> is a generic method: each call chooses T. The first call chooses Int32; the second chooses String. Reader itself has no type parameters.

package example
trait Picker =
function pick<T>(self, value: T): T = value
record Reader = id: Int32
implement Picker for Reader
function main(): Unit =
let reader = Reader { id = 0 }
assert reader.pick(42) == 42
assert reader.pick("text") == "text"

A generic class can use the same default. Here Reader<T> has a class parameter, and pick<T> has a separate method parameter. Creating Reader(1) fixes the class parameter to Int32; it does not prevent reader.pick("text") from choosing String for the method parameter.

package example
trait Picker =
function pick<T>(self, value: T): T = value
class Reader<T>(id: T) implements Picker =
public function get(self: Reader<T>): T = self.id
function choose<P, T>(picker: P, value: T): T where P: Picker = picker.pick(value)
function main(): Unit =
assert choose(Reader(1), "bound") == "bound"
let reader = Reader(1)
assert reader.pick("text") == "text"
assert reader.pick(42) == 42

An explicit matching method takes precedence over the default. Defaults may also be properties. Calls from a default to another instance member observe subclass overrides, just like calls from other class methods.

A generic default body can use only the operations justified by its declared bounds. For example, a method comparing two values with == needs where T: Equatable. Implementations cannot add stricter requirements than the trait declares. Generic methods still cannot be dispatched through interface values; use a generic function with a trait bound instead.

An associated type lets each implementation choose a type. Producer below says that produce() returns its implementation’s Output. NumberProducer chooses Int32 in its implementation block.

The generic helper does not need to choose that output type. P.Output means “the output chosen by the Producer implementation for P.”

package example
trait Producer =
type Output
function produce(self): Output
record NumberProducer = value: Int32
implement Producer for NumberProducer =
type Output = Int32
function produce(self): Int32 = self.value
function read<P>(producer: P): P.Output where P: Producer =
producer.produce()
function forward<P>(producer: P): P.Output where P: Producer =
read(producer)
function main(): Unit =
assert forward(NumberProducer { value = 42 }) == 42

For NumberProducer, both read and forward return Int32. Another producer could choose String, and the same helpers would return String for it.

Inside read, the output is still unknown. It can be returned or passed to another function expecting P.Output. It cannot be treated as Int32 or used in arithmetic merely because one implementation returns a number.

When a helper specifically needs a numeric output, state that requirement:

function readNumber<P>(producer: P): Int32
where P: Producer<Output = Int32> =
producer.produce()

Associated names are resolved through the parameter’s trait bounds. If two bounds give P different members named Output, P.Output is ambiguous. Associated-type declarations cannot provide defaults: each implementation must supply its associated definitions.

Sometimes the implementation chooses a container, while each method call chooses what goes inside it. That is the purpose of type Wrapped<T>.

Read the following example in three steps:

  1. Wrapper requires a generic method wrap<T> and an associated type Wrapped<T>.
  2. OptionalWrapper chooses Option<T>; ArrayWrapper chooses Array<T>.
  3. wrapValue returns the container chosen by its wrapper argument, containing the type of its value argument.
package example
trait Wrapper =
type Wrapped<T>
function wrap<T>(self, value: T): Wrapped<T>
record OptionalWrapper = name: String
record ArrayWrapper = name: String
implement Wrapper for OptionalWrapper =
type Wrapped<T> = Option<T>
function wrap<T>(self, value: T): Option<T> = Some(value)
implement Wrapper for ArrayWrapper =
type Wrapped<T> = Array<T>
function wrap<T>(self, value: T): Array<T> = [|value|]
function wrapValue<W, T>(wrapper: W, value: T): W.Wrapped<T>
where W: Wrapper =
wrapper.wrap(value)
function main(): Unit =
let optional = OptionalWrapper { name = "optional" }
let array = ArrayWrapper { name = "array" }
let numberOption: Option<Int32> = wrapValue(optional, 42)
let numberArray: Array<Int32> = wrapValue(array, 42)
let textOption: Option<String> = wrapValue(optional, "hello")
let textArray: Array<String> = wrapValue(array, "hello")
assert numberOption.require == 42
assert numberArray == [|42|]
assert textOption.require == "hello"
assert textArray == [|"hello"|]

W.Wrapped<T> names the result without forcing every implementation to use the same container. In the first call, W is OptionalWrapper and T is Int32, so the result is Option<Int32>. In the second, W is ArrayWrapper, so the result is Array<Int32>.

The associated definition does not have to use its parameter. An implementation could choose type Wrapped<T> = Int32 for every T. Therefore, knowing the result type does not necessarily tell the compiler the input type. Supply type arguments when the arguments to the call do not provide enough information.

Usable<T, E> uses an associated type Wrapped<U, E2> for the result of its continuation. A helper accepting an unknown resource must preserve that result contract. Its caller can provide a finish function returning the resource’s wrapper:

package example
newtype Holder = Int32
implement Usable<Int32, Never> for Holder =
type Wrapped<U, E2> = U
function use<U, E2>(self, f: (Int32) => U, errorF: (Never) => E2): U = f(self.value)
function scoped<R>(resource: R, finish: (Int32) => R.Wrapped<Int32, Never>): R.Wrapped<Int32, Never>
where R: Usable<Int32, Never> =
let value = use resource
finish(value)
function main(): Unit = assert scoped(Holder(40), value => value + 2) == 42

Holder chooses Wrapped<U, E2> = U, so its continuation returns a plain Int32. The generic scoped function cannot assume that choice for every resource. Its finish parameter and return type both say R.Wrapped<Int32, Never> to preserve the contract.

A child trait can add a method whose name is already used by a parent when the parameter lists differ. The implementation supplies both methods. The argument type determines which one a call selects:

package example
trait TextValue =
function value(self, input: String): Int32
trait Value extends TextValue =
function value(self, input: Int32): Int32
record Reader = id: Int32
implement Value for Reader =
function value(self, input: String): Int32 = 10
function value(self, input: Int32): Int32 = input + 1
function read<R>(reader: R): Int32 where R: Value =
reader.value("text") + reader.value(20)
function main(): Unit =
let reader = Reader { id = 0 }
assert reader.value("text") == 10
assert reader.value(20) == 21
assert read(reader) == 31

Here reader.value("text") selects the inherited String method, while reader.value(20) selects the Int32 method. This also works inside read, where only the R: Value bound is known.

The overloads can supply separate default bodies. Eligible interface methods retain separate dispatch slots, including after conversion to a parent interface. An implementation missing either required overload is incomplete. Calls matching more than one overload remain ambiguous; changing only the return type does not create a valid overload.


Part 25: Tuple Extension (Advanced) applies generic bounds to tuple shapes and recursive trait implementations. Its enclosing-parameter example shows how a method can require a tuple without restricting the whole type.