Skip to content

Generics

Generics let one declaration work with many concrete types while keeping those types visible in the signature. The names in <...> are type placeholders that the declaration can reuse in parameters, fields, return types, and bodies. Each call or concrete type fills those placeholders with real types. Bounds go in a where clause.

A generic function introduces type parameters before its value parameters. The call site usually pins those types from the arguments and expected return type:

fn pair<'T, 'U>(first: 'T, second: 'U): ('T, 'U) {
  (first, second)
}

fn main(): (String, Int) {
  dbg pair("age", 30)
}

Here 'T is String and 'U is Int for this call. Another call can choose a different pair of concrete types.

Structs, enums, opaque types, extern types, interfaces, and type aliases can all introduce type parameters. Those names are available anywhere the declaration needs to talk about its parts:

struct Box<'T> {
  field value: 'T
}

fn main(): Int {
  box = Box{value: 42}
  dbg box.value
}

Box<'T> says every Box carries one value, and the type of that value is the same 'T wherever it appears. Box<Int> and Box<String> are different concrete types made from the same generic definition.

An unconstrained 'T can be stored, returned, and passed around, but it does not promise any extra capabilities. A where clause adds those promises.

For now, read a bound like where 'T: Comparable as “'T must be a type that can be compared.” Bounds name interfaces; the next chapter explains how interfaces are defined and implemented. Here, the important part is the generic shape:

fn first_two_sorted<'T>(xs: List<'T>): List<'T> where 'T: Comparable {
  xs
  |> Iter.sort()
  |> Iter.take(2)
  |> Iter.to_list()
}

fn main(): List<String> {
  first_two_sorted([3, 1, 4, 1, 5, 9, 2, 6])
  |> dbg

  first_two_sorted(["banana", "apple", "cherry"])
  |> dbg
}

Both Int and String implement Comparable, so both calls satisfy where 'T: Comparable.

Multiple bounds use and:

fn describe<'T>(value: 'T): String where 'T: Display and Debug

The type parameter has to satisfy every listed interface.

A bound can mention another type parameter. That ties the choices together without requiring them to be the same type:

interface StepBy<'S> {
  fn step_by(value: self, step: 'S): self
}

type Counter Int

impl StepBy for Counter {
  fn step_by(value: Counter, step: Int): Counter {
    Counter(Int(value) + step)
  }
}

fn advance<'T, 'S>(value: 'T, step: 'S): 'T where 'T: StepBy<'S> {
  'T.step_by(value, step)
}

fn main(): Counter {
  dbg advance(Counter(10), 5)
}

advance<'T, 'S> introduces two independent type parameters. The bound where 'T: StepBy<'S> says the chosen 'T must know how to step by the chosen 'S. In this example, 'T is Counter and 'S is Int.

That shape matters for APIs like ranges and dates: a value type might step by an Int, a duration, or some other unit type instead of stepping by another value of its own type.

The same idea lets operator interfaces infer a result that is not necessarily the left-hand type. Add<'Rhs, 'Out> says what the right-hand operand may be and what the expression returns; Subtract, Multiply, and Divide use the same rhs/result shape:

type Day Int {
  fn number(day: Day): Int {
    Day(n) = day
    n
  }
}

type Days Int {
  fn number(days: Days): Int {
    Days(n) = days
    n
  }
}

impl Add<Days, Day> for Day {
  fn add(lhs: Day, rhs: Days): Day {
    Day(Day.number(lhs) + Days.number(rhs))
  }
}

fn plus<'L, 'R, 'Out>(lhs: 'L, rhs: 'R): 'Out where 'L: Add<'R, 'Out> {
  lhs + rhs
}

fn main(): Day {
  dbg plus(Day(10), Days(4))
}

Here 'L is Day, 'R is Days, and 'Out is Day.

where can appear on the generic declaration that needs the constraint:

struct Range<'T> where 'T: Comparable {
  field start: 'T
  field end: 'T

  fn contains?(r: Range<'T>, value: 'T): Bool {
    r.start <= value and value <= r.end
  }
}

derive Display for Box<'T> where 'T: Display

Constraints on a type declaration apply to that type everywhere. Constraints on an impl, derive, or function are narrower: they apply only to that implementation, derive, or function.

Generic interface functions can introduce their own type parameters too:

interface Ranked<'T> {
  fn item(value: self): 'T
  fn prefer<'K>(value: self, lhs: 'K, rhs: 'K): Bool where 'K: Comparable
}

'T comes from Ranked<'T>. 'K comes from prefer<'K>. A type parameter must be introduced before a where clause can constrain it.

Next: Interfaces & Dispatch.