Generics

Full generic type system with constraints and type inference.

Generic Structs

// Single type parameter
pub struct Box<T> {
  value: T
}

// Multiple type parameters
pub struct Pair<A, B> {
  first: A,
  second: B
}

// With constraints
pub trait Layout {
  width: I32
}

pub struct Container<T: Layout> {
  items: [T],
  gap: I32
}

// Multiple constraints
pub trait Renderable { fn render(self) -> Boolean }
pub trait Clickable { fn click(self) -> Boolean }

pub struct Widget<T: Renderable + Clickable> {
  component: T
}

Generic Traits

pub trait Collection<T> {
  items: [T]
}

pub trait Comparable<T> {
  fn compare(self, other: T) -> I32
}

Generic Enums

pub enum Result<T, E> {
  ok(value: T),
  error(err: E)
}

pub enum Option<T> {
  some(value: T),
  none
}

Generic Instantiation

pub struct Box<T> { value: T }
pub struct Pair<A, B> { first: A, second: B }
pub enum Result<T, E> { ok(value: T), error(err: E) }
pub enum Option<T> { some(value: T), none }

// With explicit type arguments
pub let string_box = Box<String>(value: "hello")
pub let number_box = Box<I32>(value: 42)
pub let pair = Pair<I32, Boolean>(first: 42, second: true)

// Type inference (when inferrable)
pub let inferred_box = Box(value: "inferred as String")
pub let inferred_pair = Pair(first: 10, second: true)

// Generic enums
pub let success: Result<String, I32> = .ok(value: "success")
pub let failure: Result<String, I32> = .error(err: 404)
pub let maybe: Option<I32> = .some(value: 42)
pub let nothing: Option<I32> = .none

// A variant path takes the type arguments too
pub let also_nothing = Option<I32>.none

An instantiation with no <...> takes its type arguments from the type that its position expects: a let annotation, a parameter, a return type, a field, an element, or the branches of an if or a match. Only when the position expects nothing do the arguments come from the fields or the payload.

pub struct Box<T> { value: T }

pub fn run_checks() {
  let b: Box<I32?> = Box(value: 1)       // a Box<I32?>, not a Box<I32>
  let xs: [Box<I32?>] = [Box(value: 2), Box(value: nil)]
  assert(condition: b.value != nil)
  assert(condition: xs.len() == 2)
}

Two instances of one generic type are two types. A value of type Box<I32> does not fit where a Box<I32?> goes, even though an I32 fits where an I32? goes:

pub struct Box<T> { value: T }

pub fn run_checks() {
  let a = Box(value: 1)                  // a Box<I32>
  let b: Box<I32?> = a                   // error: TypeMismatch
}

A type parameter that only the return type mentions takes its type from the position of the call: a let annotation, a parameter, a return type, a field, a payload, an assignment, an element, or the branches of an if or a match. The call is then the same as a call with <...>:

pub struct Meters { v: I64 }

impl From<Literal<I64>> for Meters {
  fn from(value: Literal<I64>) -> Meters { Meters(v: value.value) }
}

fn zero<T: From<Literal<I64>>>() -> T { 0 }

pub let d: Decimal = zero()      // zero<Decimal>()
pub let m: Meters = zero()       // zero<Meters>()
pub let o: Meters? = zero()      // zero<Meters>(): a value fits its optional

A return type of T in a position that expects X? gives T the type X when X meets the bounds of T, and else X?. With no expected type, let d = zero() is the error UninferableMethodTypeParameter (E147).

Generic Methods

A method can declare its own type parameters, after its name. They sit beside the type parameters of the impl block:

pub struct Box<T> {
  value: T
}

impl Box<T> {
  fn map<U>(self, f: (T) -> U) -> Box<U> {
    Box(value: f(self.value))
  }
}

pub let flag = Box(value: 3).map(f: (v) -> v > 2)   // Box<Boolean>

A type parameter that only the return type mentions takes its type from the position of the call, on a value and on a type:

pub struct Meters { v: I64 }

impl From<Literal<I64>> for Meters {
  fn from(value: Literal<I64>) -> Meters { Meters(v: value.value) }
}

pub struct Scale { n: I64 }

impl Scale {
  fn unit<T: From<Literal<I64>>>() -> T { 1 }
  fn size<T: From<Literal<I64>>>(self) -> T { 2 }
}

pub let u: Meters = Scale.unit()          // unit with T = Meters
pub let d: Decimal = Scale(n: 1).size()   // size with T = Decimal

Rules:

  • A call may write the method's type arguments after its name, as for a function: s.size<Decimal>(). A written type must meet the bounds (GenericConstraintViolation), the call must write one for each type parameter (GenericArityMismatch), and an argument must agree with it (TypeMismatch)
  • With no <...>, the arguments give each type parameter of the method its type, as for a free function. A type parameter that no argument binds takes its type from the position of the call, through the return type. So each one must appear in the type of a parameter with no default, or in the return type (E147)
  • A call may leave out a parameter with a default, and then that parameter gives no type. A call whose arguments and position give a type parameter no type is the error E147 at the call: let x = s.convert() for fn convert<T>(self) -> T
  • The type parameters of the impl block come from the receiver, never from the position of the call
  • A closure argument can give a type parameter its type through its return type: U above takes the type that f answers. A closure parameter with no type cannot: in fn apply<U>(self, f: (U) -> I32), the call b.apply(f: (x) -> 1) gives x no type (E145). Write it: (x: String) -> 1
  • A method type parameter cannot repeat a name of its impl block, or of the type when the impl block names none (E084)
  • A trait method may declare type parameters of its own too. See Traits
  • A type parameter hides a type with the same name inside its definition

Type Constraints

pub trait Named { name: String }
pub trait Layout { width: I32 }
pub trait Renderable { fn render(self) -> Boolean }
pub trait Clickable { fn click(self) -> Boolean }

// Single constraint
pub struct Wrapper<T: Named> {
  item: T
}

// Multiple constraints
pub struct Interactive<T: Renderable + Clickable> {
  component: T
}

// Constraint on trait field
pub trait Container<T: Layout> {
  items: [T]
}

Rules:

  • Type parameters use <T>, <A, B>, etc.
  • Constraints use : syntax: <T: Constraint>
  • Multiple constraints use +: <T: A + B>
  • Type arguments must match parameter count (arity). A wrong count, or type arguments on a type that takes none, is the error GenericArityMismatch
  • A type parameter is visible only in its own definition. A use outside it is the error OutOfScopeTypeParameter
  • Type inference works when types can be determined. Two arguments that give one type parameter two types are a TypeMismatch
  • A free function gives each type parameter its type through <...> at the call, through an argument, or through the type that the position of the call expects. The arguments give their types first. The position gives a type only to a parameter that the arguments leave free, through the return type of the function
  • fn make<T>() -> I32 needs make<I32>(): a bare make() is the error UninferableMethodTypeParameter (E147). Neither an argument nor the return type gives T a type
  • The type arguments at the call give each type parameter its type in the function, and a copy of the function for those types. make<I32>() and make<String>() are two calls of two copies
  • Constraints must reference existing traits: an unknown name is the error UndefinedTrait, and a struct is the error NotATrait
  • A bound can name the type parameter that it bounds: <T: Comparable<T>>. With T = N, the bound is Comparable<N>
  • A body that compares values of a type parameter with == or != needs an equatable type argument. A closure, and a type that holds one, is not equatable: same(a: f, b: g) on two closures is the error NotEquatableTypeArgument (E177). This holds through a generic caller that gives its own type parameter to same
  • A constraint holds wherever the type argument comes from — written at the call site (total<Square>(item: s)) or inferred from the argument (total(item: s))
pub trait Shape { fn area(self) -> I32 }
pub struct Square { side: I32 }
pub struct Plain { n: I32 }

impl Shape for Square {
  fn area(self) -> I32 { self.side * self.side }
}

fn total<T: Shape>(item: T) -> I32 { item.area() }

let a: I32 = total(item: Square(side: 3))  // ok
let b: I32 = total(item: Plain(n: 1))      // error E081: Plain is not a Shape
fn same<T>(a: T, b: T) -> Boolean { a == b }

let a: Boolean = same(a: 1, b: 1)                        // ok
let b: Boolean = same(a: (x: I32) -> x, b: (x: I32) -> x) // error E177

Monomorphisation

The MonomorphisePass clones generic definitions per unique argument tuple after parsing: see Built-in Passes / MonomorphisePass.

The set of clones must be finite. A generic function that calls itself with a larger type argument than its own, such as Box<T> for T, would need a new clone at each level. The pass stops at 32 levels with the error InstantiationDepthExceeded (E148). A written type that nests its type arguments more than 32 levels deep gets the same error.

struct Box<T> { value: T }

fn grow<T>(x: T, n: I32) -> I32 {
  if n <= 0 { 0 } else { 1 + grow(x: Box(value: x), n: n - 1) }
}

pub fn probe() -> I32 { grow(x: 1, n: 3) }

A generic function that calls itself must call itself with the same type arguments. Wrap the growing value in a non-generic type, or give it a fixed type.