Skip to content

Generics

Generics allow you to write code that works with multiple types while maintaining type safety. Instead of writing separate functions for each type, you write one generic version.


Use angle brackets to declare type parameters:

function identity<T>(x: T): T = x
// Use with different types
let a = identity(42) // T is Int32
let b = identity("hello") // T is String
let c = identity(true) // T is Bool

The compiler infers the type argument from the value you pass.

Functions can have multiple type parameters:

function first<A, B>(a: A, b: B): A = a
function second<A, B>(a: A, b: B): B = b
let x = first(10, "hello") // A is Int32, B is String, returns 10
let y = second(10, "hello") // returns "hello"

Sometimes you need to specify type arguments explicitly:

function create<T>(): Array<T> = [||]
// Must specify T since it can't be inferred
let numbers = create<Int32>()
let names = create<String>()

Type parameters are in scope for the entire function:

function swap<T>(pair: (T, T)): (T, T) =
let (a, b) = pair
(b, a)
let swapped = swap((1, 2)) // (2, 1)

Records can have type parameters:

record Box<T> =
value: T
let intBox = Box { value = 42 }
let stringBox = Box { value = "hello" }
record Pair<A, B> =
first: A
second: B
let pair = Pair { first = 1; second = "one" }
let x = pair.first // Int32
let y = pair.second // String

Enums can also be generic:

enum Option<T> =
Some(T)
None
function divide(a: Int32, b: Int32): Option<Int32> =
if b == 0 then
None
else
Some(a / b)
let result = divide(10, 2)
match result with
case Some(value) => debug("Result: $value")
case None => debug("Cannot divide by zero")

The Result type uses two type parameters:

enum Result<T, E> =
Ok(T)
Error(E)
function divideOrError(a: Int32, b: Int32): Result<Int32, String> =
if b == 0 then
Error("Cannot divide by zero")
else
Ok(a / b)
let result = divideOrError(10, 2) // Ok(5)

Classes can have type parameters:

class Container<T>(value: T) =
function get(self): T = self.value
function set(self, newValue: T): Container<T> =
Container(newValue)
let container = Container(42)
let value = container.get() // 42

A constraint says what a type must support before it can be used with your generic code. For example, a function that compares two values needs a type that supports comparison.

Use the where clause to add constraints:

trait Printable =
function format(self): String
function printIt<T>(x: T): String
where T: Printable
= x.format()

Now printIt can only be called with types that implement Printable:

implement Printable for Int32 =
function format(self): String = "$self"
printIt(42) // OK - Int32 implements Printable
// printIt([1, 2]) // Error - List doesn't implement Printable

You can require multiple traits using +:

trait Showable =
function show(self): String
trait Countable =
function count(self): Int32
function describe<T>(x: T): String
where T: Showable + Countable
= "${x.show()} (count: ${x.count()})"

Or list them separately:

function describe<T>(x: T): String
where T: Showable, T: Countable
= "${x.show()} (count: ${x.count()})"

For simple cases, you can put the constraint inline:

function printIt<T: Printable>(x: T): String = x.format()

This is equivalent to using a where clause.

function combine<A, B>(a: A, b: B): String
where A: Showable, B: Showable
= "${a.show()} and ${b.show()}"

Records, enums, and classes can also have constraints:

record SortedPair<T>
where T: Comparable
=
first: T
second: T
// Only types that implement Comparable can be used
let pair = SortedPair { first = 1; second = 2 } // OK

Some commonly used trait constraints:

  • Equatable - allows == and != comparisons
  • Comparable - allows <, >, <=, >= comparisons
  • Printable - can be formatted as a string
function findMax<T>(a: T, b: T): T
where T: Comparable =
if a > b then a else b
let bigger = findMax(10, 20) // 20

  • Use <T> to write a function or type that works with different types.
  • The compiler usually infers type arguments from the values you pass.
  • Use explicit type arguments, such as <Int32>, when needed.
  • Use where T: Trait to require operations your generic code needs.

For generic extensions, class constraints, method-specific bounds, variance, and runtime type matching, continue with Part 26: Advanced Generics.