Sisu

Reference / Generics

Generics

A generic declaration has type parameters, so that one definition works for many types. A use of it gives a type argument for each parameter.

class Box<T> {
    value: T
}

fn first<T>(xs: [T]) -> T? {
    if xs.len() > 0 { xs[0] } else { None }
}

fn main() {
    let b = Box(value: "sauna")
    print(b.value)                     // sauna
    print(first([10, 20]) ?? 0)        // 10
    print(first(["Aino"]) ?? "none")   // Aino
}

first works for an array whose elements have a type that a type argument can be (Type arguments). A function, a class and an enum can be generic. This page also tells what a generic body can do with a value of a type parameter, and which features are in development.

Type parameters

The type parameters of a declaration are names between < and > after the name of the declaration. A function, a class or an enum can have them.

class Pair<A, B> {
    first: A
    second: B

    fn swap(self) -> Pair<B, A> {
        Pair(first: self.second, second: self.first)
    }

    fn with_second<C>(self, second: C) -> Pair<A, C> {
        Pair(first: self.first, second: second)
    }
}

enum Tree<T> {
    Leaf
    Node(Tree<T>, T, Tree<T>)
}

fn size<T>(t: Tree<T>) -> i64 {
    match t {
        Leaf => 0
        Node(l, _, r) => size(l) + 1 + size(r)
    }
}

fn main() {
    let p = Pair(first: 1, second: "one")
    print(p.swap().first)              // one
    print(p.with_second(2.5).second)   // 2.5
    let t = Tree.Node(Tree.Leaf, 5, Tree.Node(Tree.Leaf, 6, Tree.Leaf))
    print(size(t))                     // 2
}

The rules:

  • A type parameter is a type in the whole declaration. It can be the type of a parameter, a field or a payload, a return type, and a binding in the body.
  • A method of a generic class or enum uses the type parameters of its type. A method can also add type parameters of its own, such as C above.
  • The type parameters of one declaration have different names. A method cannot reuse a name from its type (E0034).
  • A type parameter cannot take a name that E0026 lists, such as i64 or Option.
  • main has no type parameters (E0029).
class Box<T> {
    value: T

    fn map<T>(self, x: T) -> T {         // error[E0034]
        x
    }
}

fn main() {}

E0034: what this error means

Type arguments

A type argument is a type between < and > after the name of a generic class or enum. It can be any type that a value can have, such as i64, [string] or another generic type.

class Box<T> {
    value: T
}

fn main() {
    let a: Box<i64> = Box(value: 1)
    let b: Box<Box<string>> = Box(value: Box(value: "sauna"))
    let c: [Box<bool>] = [Box(value: true)]
    let d: Box<f64>? = None
    print(a.value)                     // 1
    print(b.value.value)               // sauna
    print(c.len())                     // 1
    print(d == None)                   // true
}

The rules:

  • A type that names a generic class or enum gives one type argument for each type parameter (E0137). As a type, Box alone and Box<i64, i64> are errors.
  • A class, an enum or a function that is not generic takes no type arguments (E0137).
  • The type arguments of a variant go on its enum: Tree<i64>.Node(...), not Tree.Node<i64>(...) (E0137).
  • A type argument cannot be unit or never (E0138). The type of a call that gives no value, such as print(1), is unit. A type argument can be an optional, such as i64? in Box<i64?>. The section unit is in development, and it lifts the rule for unit.
class Box<T> {
    value: T
}

fn main() {
    let c: Box = Box(value: 1)         // error[E0137]
}

E0137: what this error means

Inference

A call usually needs no type arguments. sisu finds each one from the arguments of the call, or from the type that the context expects. The context can be a let with a type, a parameter or a return type.

class Stack<T> {
    mut items: [T]
}

fn empty<T>() -> Stack<T> {
    Stack(items: [])
}

fn first<T>(xs: [T]) -> T? {
    if xs.len() > 0 { xs[0] } else { None }
}

fn main() {
    // T is i64, from the argument.
    let a = Stack(items: [1, 2])
    // T is string, from the type of b.
    let b: Stack<string> = empty()
    // T is written.
    let c = empty<bool>()
    print(a.items.len())                       // 2
    print(b.items.len())                       // 0
    print(c.items.len())                       // 0
    print(first<string>([]) ?? "empty")        // empty
}

The rules:

  • A type argument can be written in a call: first<string>([]), Stack<i64>(items: []). A method call can write them too.
  • Only the arguments and the expected type give a type. A later use of the result does not.
  • An argument such as Ok(1), whose own arguments give only some of its types, takes the others from the outer call. In let r: Result<i64, string> = id(Ok(1)), the type of r gives T for id, and T gives E for Ok.
  • When an argument does not agree with the expected type, the error is at the argument. let r: Result<i64, string> = Ok("x") reports "x": expected i64, found string (E0032). With written type arguments, as in id<i64>(n), the error stays at the call.
  • A None or a [] alone has no type. When nothing else fixes a type parameter, the call is an error (E0050). Annotate the result, or write the type arguments.
fn first<T>(xs: [T]) -> T? {
    if xs.len() > 0 { xs[0] } else { None }
}

fn main() {
    let x = first([])                  // error[E0050]
}

E0050: what this error means

The < rule

After a name in an expression, < can start a list of type arguments. It does so only when a well-formed list follows and its closing > is followed by ( or .. Otherwise < is a comparison.

fn both(x: bool, y: bool) -> bool {
    x && y
}

fn id<T>(x: T) -> T {
    x
}

fn main() {
    let a = 1
    let b = 2
    let c = 3
    let d = 4
    print(both(a < b, c > d))          // false
    print(id<i64>(a) < id<i64>(b))     // true
}

So both(a < b, c > d) has two comparisons. In both(a < b, c > (d)), the < and the > read as a list of type arguments for a, and a is a value. This is an error (E0137). Put the first comparison in parentheses.

fn both(x: bool, y: bool) -> bool {
    x && y
}

fn main() {
    let a = 1
    let b = 2
    let c = 3
    let d = 4
    print(both(a < b, c > (d)))        // error[E0137]
}

E0137: what this error means

Values of a type parameter

sisu checks a generic body once, where it is written, and not for each type argument. So it reports an error in a generic body also when no call uses the body.

In the body, the type parameter T can stand for any type. So the body can pass, return and store a value of type T, and it can use it in an optional or an array.

fn pick<T>(a: T, b: T, first: bool) -> T {
    if first { a } else { b }
}

fn or_else<T>(x: T?, fallback: T) -> T {
    x ?? fallback
}

fn present<T>(x: T?) -> bool {
    x != None
}

fn main() {
    print(pick("a", "b", false))       // b
    print(or_else(None, 7))            // 7
    print(or_else(3, 7))               // 3
    print(present("x"))                // true
}

The rules:

  • An operator that computes with a T or compares it, such as +, < or ==, is an error (E0139). contains on a [T] uses ==, so it is an error too. x != None and x == None on a T? are valid. x == y on two T? is an error.
  • !, && and || take only a bool, so a T there is a type mismatch (E0032).
  • A T has no method and no field (E0063).
  • is does not apply to a T, because T can stand for a type that is not a class (E0046).
  • print and interpolation do not take a T (E0059, E0098).
  • [x; n] with x: T is an error (E0067), because T can be a class (Filled arrays). [None; n] is valid.
  • match does not take a T (E0124). In a payload of the type T, a pattern is a name or _ (Variant patterns).
fn smaller<T>(a: T, b: T) -> bool {
    a < b                              // error[E0139]
}

fn main() {}

E0139: what this error means

The sections Bounds and Interfaces tell how a generic body can ask for more.

Growing type arguments

A generic function, class or enum cannot use itself with a type argument that grows, directly or through other declarations. nest([x], n - 1) in nest<T> calls nest with [T] for T, and then with [[T]], without end. This is an error (E0140).

fn nest<T>(x: T, n: i64) {
    if n > 0 {
        nest([x], n - 1)               // error[E0140]
    }
}

fn main() {
    nest(1, 3)
}

E0140: what this error means

A use with the same type arguments, such as nest(x, n - 1), is valid.

Bounds In development

A bound says which interface a type argument must conform to. It follows the type parameter after a :. & joins two bounds, as in T: Shape & Ord.

In development
fn largest<T: Ord>(xs: [T]) -> T? {
    let mut best: T? = None
    for x in xs {
        if let b = best {
            if b < x {
                best = x
            }
        } else {
            best = x
        }
    }
    best
}

fn main() {
    print(largest([3, 9, 2]) ?? 0)     // 9
    print(largest(["b", "a"]) ?? "")   // b
}

The rules:

  • In the body, a value of type T can do what its bounds give it. These are the methods of the interfaces and the operators that they map to.
  • Eq gives == and !=. Ord gives <, <=, > and >=.
  • Anything else is an error where the body is written. The help suggests the bound that is missing.
  • A type argument that does not conform to a bound is an error at the call.
  • Every type parameter can print, with no bound. print(x), "{x}" and x.to_string() work on a T.
  • Every class conforms to AnyObject. So does an any value of a class-only interface (next rule). Nothing else does. T: AnyObject lets the body use is on values of type T, and lets a class declare a weak field of type T?.
  • An interface that inherits AnyObject is class-only. Its any values support is too, and a class can hold one in a weak field.

Interfaces In development

An interface lists the methods that a type must have. A class or an enum names its interfaces after its name. A method in an interface has self and no body.

In development
interface Shape {
    fn area(self) -> f64
}

class Circle: Shape {
    r: f64

    fn area(self) -> f64 {
        f64.pi * self.r * self.r
    }
}

fn total_area<T: Shape>(shapes: [T]) -> f64 {
    let mut sum = 0.0
    for s in shapes {
        sum += s.area()
    }
    sum
}

fn main() {
    print(total_area([Circle(r: 1.0), Circle(r: 2.0)]))
}

The rules:

  • A type that names an interface must have each method of it, with the same signature. A missing method is an error.
  • A type conforms to an interface with the same type arguments at most once.
  • Self in an interface is the type that conforms to it, as in fn eq(self, other: Self) -> bool.
  • An interface can require other interfaces: interface Shape: Display. A type that conforms to Shape conforms to Display too.
  • Several interfaces follow a name, separated by ,: class Tree: Display, Eq.
  • A method in an interface can have type parameters of its own. Such an interface can bound a type parameter, but any cannot take it.

Conformance after the declaration In development

extend adds methods to a type that is declared elsewhere. extend Type: Interface makes the type conform to the interface, with the methods of that interface in its body. One block names one interface.

In development
class Celsius {
    degrees: f64
}

class Fahrenheit {
    degrees: f64
}

interface Transform<T> {
    fn transform(self) -> T
}

extend Celsius: Transform<Fahrenheit> {
    fn transform(self) -> Fahrenheit {
        Fahrenheit(degrees: self.degrees * 1.8 + 32.0)
    }
}

fn main() {
    let f: Fahrenheit = Celsius(degrees: 100.0).transform()
    print(f.degrees)                   // 212.0
}

The rules:

  • extend Type { ... } adds methods that follow no interface.
  • A program cannot extend a built-in type, such as string or [T].
  • A generic interface, such as Transform<T>, lets one type conform to it several times, with different type arguments.
  • When a call can use more than one conformance, the expected type picks one: let f: Fahrenheit = c.transform(). Without an expected type, the call is an error.

Generic extensions In development

An extend can have type parameters. extend<T> Stack<T> adds methods to every Stack. extend Stack<i64> adds methods to Stack<i64> only.

In development
class Stack<T> {
    mut items: [T]
}

extend<T> Stack<T> {
    fn size(self) -> i64 {
        self.items.len()
    }
}

extend<T: Eq> Stack<T>: Eq {
    fn eq(self, other: Stack<T>) -> bool {
        self.items == other.items
    }
}

extend Stack<i64> {
    fn total(self) -> i64 {
        self.items.sum()
    }
}

fn main() {
    let a = Stack(items: [1, 2])
    let b = Stack(items: [1, 2])
    print(a.size())                    // 2
    print(a.total())                   // 3
    print(a == b)                      // true
}

The rules:

  • extend<T: Eq> Stack<T>: Eq makes Stack<T> conform to Eq only when T does.
  • Methods with the same name on different instantiations never overlap.
  • One method name in two extensions that both cover an instantiation is an error. An example is extend<T> Stack<T> and extend Stack<i64>, which both define total.

Associated types In development

An associated type is a type that each conforming type chooses once. The interface declares it with type. A conforming type sets it with type Name = Type.

In development
interface Source {
    type Item: Display
    fn next(self) -> Item?
}

class Countdown: Source {
    mut left: i64

    type Item = i64

    fn next(self) -> i64? {
        if self.left == 0 {
            return None
        }
        self.left -= 1
        self.left + 1
    }
}

fn first_item<S: Source>(s: S) -> S.Item? {
    s.next()
}

fn show_next<S: Source>(s: S) {
    if let x = s.next() {
        print(x)
    }
}

fn main() {
    let c = Countdown(left: 2)
    print(first_item(c) ?? 0)          // 2
    show_next(c)                       // 1
}

Inside the interface, the associated type is written Item. Elsewhere it is written through the type: S.Item for a type parameter S: Source.

An associated type can carry a bound, such as type Item: Display. Generic code can then print an S.Item, as show_next does.

Use an associated type when each type has one answer, such as the item of a Source. Use a type parameter, as in Transform<T>, when one type can conform in several ways. An interface can have both.

Display, Eq and Ord In development

Display, Eq and Ord are interfaces of the language. print and interpolation use Display. == and != use Eq. <, <=, > and >= use Ord.

In development
class Point {
    x: i64
    y: i64

    fn to_string(self) -> string {
        "({self.x}, {self.y})"
    }
}

class Money: Eq, Ord {
    cents: i64

    fn eq(self, other: Money) -> bool {
        self.cents == other.cents
    }

    fn less(self, other: Money) -> bool {
        self.cents < other.cents
    }
}

enum Level: Eq {
    Low
    High
}

fn main() {
    print(Point(x: 1, y: 2))                           // (1, 2)
    print(Money(cents: 5) == Money(cents: 5))          // true
    print(Money(cents: 3) < Money(cents: 5))           // true
    print(Level.Low == Level.High)                     // false
}

The rules:

  • Every type that can be a type argument has a Display.
  • A class prints its name, with its type arguments, and never its fields. A class with a method to_string prints the text of that method.
  • A class has no == and no <. A class that conforms to Eq has ==, and one that conforms to Ord has <. a is b compares two objects by identity.
  • A value that holds a class, such as an array of objects, has == only when the class conforms to Eq.
  • f64 conforms to Eq and Ord with the rules of IEEE 754. In generic code, nan is not equal to nan, as in other code.
  • An enum has == when all its payloads have it, and it has < only when it conforms to Ord.
  • The methods that the interfaces require are fn to_string(self) -> string for Display, fn eq(self, other: Self) -> bool for Eq, and fn less(self, other: Self) -> bool for Ord. A method named to_string with another signature is an error.
  • Ord requires Eq. A type that conforms to Ord but not to Eq is an error.
  • For Eq, a != b is !a.eq(b).
  • A conformance that a program writes replaces the generated one. An enum that writes to_string prints its own text.
  • A type can name a generated interface and write no method for it: enum Level: Eq has no eq. Where nothing is generated, such as class Node: Eq with no eq, a missing method is an error.
  • Only to_string conforms by name. A type with its own eq or less method conforms to Eq or Ord only when its declaration names the interface.
  • i64, u8, char and string have Eq and Ord with no declaration. An enum has Eq with no declaration when all its payloads have it.

Type aliases In development

A type alias gives a new name to a type. It is the same type as the type that it names, so no conversion is necessary.

In development
type Grid = [[char?]]
type Pairs<T> = [[T]]

fn main() {
    let g: Grid = [['a', None], [None, 'b']]
    let p: Pairs<i64> = [[1, 2], [3, 4]]
    print(g.len())                     // 2
    print(p[1][0])                     // 3
}

The rules:

  • A type alias is a top-level declaration. Its name must differ from the name of any function, class, enum or constant.
  • An alias can have type parameters, such as Pairs<T>.
  • An alias that refers to itself, directly or through another alias, is an error.

Interface values In development

any Shape is a type for a value of any type that conforms to Shape. A value converts to it where an any type is expected. A call of a method on it uses the method of the type that the value has. An interface that inherits AnyObject is class-only, as Bounds says.

In development
interface Shape {
    fn area(self) -> f64
}

class Circle: Shape {
    r: f64

    fn area(self) -> f64 {
        f64.pi * self.r * self.r
    }
}

class Rect: Shape {
    w: f64
    h: f64

    fn area(self) -> f64 {
        self.w * self.h
    }
}

fn main() {
    let shapes: [any Shape] = [Circle(r: 1.0), Rect(w: 2.0, h: 3.0)]
    for s in shapes {
        print(s.area())
    }
}

The rules:

  • An interface name alone is not a type. Write T: Shape for a type parameter, or any Shape for a value.
  • An interface whose methods are generic, or that uses Self besides the receiver, cannot be any. So any Eq and any Ord are errors.
  • An any value prints through the Display of the type that it holds. It has no == and no <.
  • A generic function with the bound T: Shape accepts an [any Shape].