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 pairSyntax#
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#
A type parameter is declared in angle brackets directly after the name, before the parameter list of a
fn, the body of atypeortrait, or the target of anextend.fn identity<Value>,type Pair<Left, Right>.A type parameter's name is written out and capitalized like a type:
Item,Key,Value,Output,Failure- never a single letter.List<Item>andMap<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.extenddeclares its own type parameters, separate from the type it targets.extend<Item> List<Item> with Show where Item: ShowintroducesItemfor thisextendalone; a secondextendofListintroduces 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())A
typeortraitparameter can have a default; afnparameter cannot declare one at all.trait Add<Other = Self, Output = Self>meanswith AddisAdd<Self, Self>, and nobody writes it out. Afn'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 nichtfn empty<Item = String>(): List<Item> { [] } const y: List<Bool> = empty() // error: A type parameter of a `fn` has no defaultSelfis never declared as a type parameter. Inside atraitor atype,Selfalready names the implementing type; a trait can use it as an ordinary type, including as an argument to another trait (Accumulator<Item, Self>).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, aList<Int>or a value of a trait type where anItemis expected is the ordinary mismatch a named type gets.Kompiliert nichtfn textOf<Item>(): Item { "x" } // error: Expected `Item`, found `String`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: Numericis the literal itself, so0and1become the parameter. An integer literal needs a bound that impliesNumeric, a decimal one needsReal- without such a bound the literal is anInt64or aFloat64and then does not fit. The two neutral elements need no literal at all:Numericrequiresstatic zero: Selfandstatic one: Selfof every number type, soScalar.zeroandScalar.oneare 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 nichtfn 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>
}type Buffer<Size> {
data: Array<Int, Size>
}
// error: `Size` is a type, and a value belongs hereRelated#
- 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.