Type parameters

A type parameter is declared in angle brackets after the name of a fn, type, trait or extend, and its name is written out like a type, never a single letter.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

A type parameter stands for a type that is filled in at each use: at each call for a fn, at each construction for a type, at each implementation for a trait. It is declared once, in angle brackets, and used by name in the signature or the body that follows.

Example#

fn identity<Value>(value: Value): Value {
  value
}

fn empty<Item>(): List<Item> {
  []
}

const number = identity 5
const strings = empty<String>()
print "{number} {strings}"

type Pair<Left, Right> {
  left: Left
  right: Right
}

trait Convert<Target> {
  fn convert(): Target
}

const pair = Pair 1, "one"
print pair

Syntax#

fn <name><Params>(<parameters>): <Type> { ... }
type <Name><Params> { ... }
trait <Name><Params> { ... }
extend<Params> <Name><Args> [with <Trait>] [where <bounds>] { ... }

<Params> ::= <Param> ("," <Param>)*
<Param>  ::= <Name> [: <bound>] ["=" <default>]

A call or a construction that writes the arguments out uses the same angle brackets: empty<String>(), Pair<Int, String>(1, "one").

Rules#

  1. A type parameter is declared in angle brackets directly after the name, before the parameter list of a fn, the body of a type or trait, or the target of an extend. fn identity<Value>, type Pair<Left, Right>.

  2. A type parameter's name is written out and capitalized like a type: Item, Key, Value, Output, Failure - never a single letter. List<Item> and Map<Key, Value> read as what they hold; List<T> does not say anything a reader has to guess less about. The compiler accepts a single letter; the convention is enforced by the linter, not by the type checker.

  3. extend declares its own type parameters, separate from the type it targets. extend<Item> List<Item> with Show where Item: Show introduces Item for this extend alone; a second extend of List introduces its own.

    extend<Item> List<Item> where Item: Compare {
      fn largest(): Item? {
        get(0).map { first => fold first { a, b => a.max b } }
      }
    }
    
    print([3, 1, 2].largest())
  4. A type or trait parameter can have a default; a fn parameter cannot declare one at all. trait Add<Other = Self, Output = Self> means with Add is Add<Self, Self>, and nobody writes it out. A fn's type arguments always come from the call, so a default on one would never be consulted, and the checker rejects the declaration itself:

    fn empty<Item>(): List<Item> {
      []
    }
    
    const y: List<Bool> = empty()
    Kompiliert nicht
    fn empty<Item = String>(): List<Item> {
      []
    }
    
    const y: List<Bool> = empty()
    // error: A type parameter of a `fn` has no default
  5. Self is never declared as a type parameter. Inside a trait or a type, Self already names the implementing type; a trait can use it as an ordinary type, including as an argument to another trait (Accumulator<Item, Self>).

  6. Inside the body that declares it, a type parameter is opaque: it stands for the one type the call site chose, and nothing else is that type. A String, a List<Int> or a value of a trait type where an Item is expected is the ordinary mismatch a named type gets.

    Kompiliert nicht
    fn textOf<Item>(): Item {
      "x"
    }
    // error: Expected `Item`, found `String`
  7. The one exception is a numeric literal, and only where the bound says the parameter is a number. The neutral element of an algorithm over a Scalar: Numeric is the literal itself, so 0 and 1 become the parameter. An integer literal needs a bound that implies Numeric, a decimal one needs Real - without such a bound the literal is an Int64 or a Float64 and then does not fit. The two neutral elements need no literal at all: Numeric requires static zero: Self and static one: Self of every number type, so Scalar.zero and Scalar.one are the constants of whichever type the call site chose.

    fn doubled<Scalar: Numeric>(value: Scalar): Scalar {
      value * 2
    }
    
    fn total<Scalar: Numeric>(values: List<Scalar>): Scalar {
      values.fold Scalar.zero { sum, value => sum + value }
    }
    
    print doubled<Int>(4)
    print total([0.5, 0.25])
    Kompiliert nicht
    fn halfOf<Scalar: Numeric>(): Scalar {
      0.5
    }
    // error: Expected `Scalar`, found `Float64`

What this is not#

A type parameter is not a value, and a value is not a type parameter. A parameter declared without const fills in a type; filling in a number needs a const parameter instead, and the two cannot stand in for each other.

type Buffer<const Size: Int> {
  data: Array<Int, Size>
}
Kompiliert nicht
type Buffer<Size> {
  data: Array<Int, Size>
}
// error: `Size` is a type, and a value belongs here
  • Bounds - restricting what a type parameter can be filled in with.
  • Inference - when a type argument can be left out.
  • Traits - with Add<Self, Self> and where a trait's own parameters come from.
  • No higher-kinded types - what a type parameter cannot be.
Esc