The Tessera Book
Chapter 10

Generics and Interfaces

Many functions don't care exactly what type they work on. Finding the largest item works for numbers and for strings; computing a total area works for anything that has an area. This chapter shows how to write such code once: with type parameters for "any type", interfaces for "any type that has these methods", and dyn for "a value of any such type, decided at run time".

Type parameters

struct Pair[A, B] {
    first: A,
    second: B,
}

fn swap[A, B](p: Pair[A, B]) -> Pair[B, A] {
    Pair { first: p.second, second: p.first }
}

fn largest[T: Ordered](xs: List[T]) -> Option[T] {
    let mut best = xs.get(0)?
    for x in xs {
        if x > best {
            best = x
        }
    }
    Some(best)
}

fn main(root: Root) {
    let p = Pair { first: 1, second: "one" }
    root.stdout.print("{p} -> {swap(p)}")
    root.stdout.print("{largest([3, 41, 7])} {largest(["pear", "apple"])}")
    let none: List[Float] = []
    root.stdout.print("{largest(none)}")
}
Listing 10-1: A generic struct and generic functions examples/ch10-01-generics/main.tsr
$ tessera run main.tsr
Pair { first: 1, second: "one" } -> Pair { first: "one", second: 1 }
Some(41) Some("pear")
None

Type parameters go in square brackets after the name. Pair[A, B] is a struct that works for any two types, and swap[A, B] a function that works for any pair. You never write the types at a call: swap(p) works them out from p.

largest[T: Ordered] needs a little more than "any type": it compares items with >, and not every type can be compared. Ordered is a bound: T must be a type that has an order. Int, Float, Str and Bool do, and so do lists, tuples, structs and enums of ordered types. Maps and functions don't. Another built-in bound, Key, means "can be a map key".

Each use of a generic function is compiled for its own types, so generic code runs exactly as fast as code written for one type.

Interfaces

An interface names a set of methods. Any type that has them satisfies it; there's nothing to declare.

interface Shape {
    fn area(self) -> Float
    fn name(self) -> Str
}

struct Circle {
    r: Float,
}

fn area(self: Circle) -> Float {
    3.14159 * self.r * self.r
}

fn name(self: Circle) -> Str {
    "circle"
}

struct Square {
    side: Float,
}

fn area(self: Square) -> Float {
    self.side * self.side
}

fn name(self: Square) -> Str {
    "square"
}

fn total_area[T: Shape](xs: List[T]) -> Float {
    xs.map(|s| s.area()).fold(0.0, |a, b| a + b)
}

fn describe[T: Shape](s: T) -> Str {
    "a {s.name()} of area {s.area().to_fixed(2)}"
}

fn main(root: Root) {
    root.stdout.print(describe(Circle { r: 1.0 }))
    root.stdout.print(describe(Square { side: 2.0 }))
    let circles = [Circle { r: 1.0 }, Circle { r: 2.0 }]
    root.stdout.print("circles: {total_area(circles).to_fixed(2)}")
}
Listing 10-2: An interface used as a bound examples/ch10-02-interfaces/main.tsr
$ tessera run main.tsr
a circle of area 3.14
a square of area 4.00
circles: 15.71

Circle and Square each have area and name methods with the right types, so both satisfy Shape, without mentioning it. describe[T: Shape](s: T) takes any such type, and inside it, s has exactly the methods Shape lists.

When a type doesn't fit, the error says which method is missing:

This program does not compile
interface Shape {
    fn area(self) -> Float
    fn name(self) -> Str
}

struct Triangle {
    base: Float,
    height: Float,
}

fn area(self: Triangle) -> Float {
    self.base * self.height / 2.0
}

fn describe[T: Shape](s: T) -> Str {
    "a {s.name()} of area {s.area()}"
}

fn main(root: Root) {
    root.stdout.print(describe(Triangle { base: 3.0, height: 4.0 }))
}
Listing 10-3: A type that is missing a method examples/ch10-03-unsatisfied/main.tsr
$ tessera check main.tsr
error[E0111]: `Triangle` does not satisfy interface `Shape`, required of `T`
  --> main.tsr:20:23
    |
 20 |     root.stdout.print(describe(Triangle { base: 3.0, height: 4.0 }))
    |                       ^^^^^^^^
  = note: `Triangle` has no method `name`

1 error

An interface is not a type by itself: fn f(s: Shape) is an error. Write fn f[T: Shape](s: T), which says "some type that is a Shape", or use dyn Shape, below. Several bounds combine with +: [T: Shape + Named].

Built-in types count too, with their built-in methods. In chapter 11 you'll see an interface with one method, now_ms, that both the real Clock capability and a fake clock satisfy. That is how tests pass a fake in place of a real capability.

Generic interfaces

An interface can take type parameters of its own:

interface Container[T] {
    fn get(self, i: Int) -> Option[T]
    fn len(self) -> Int
}

// A ring of values that never ends: index 5 of a ring of 3 is index 2.
struct Ring[T] {
    items: List[T],
}

fn get[T](self: Ring[T], i: Int) -> Option[T] {
    if self.items.is_empty() {
        None
    } else {
        self.items.get(i % self.items.len())
    }
}

fn len[T](self: Ring[T]) -> Int {
    self.items.len()
}

fn first_and_last[C: Container[T], T](c: C) -> Option[(T, T)] {
    let first = c.get(0)?
    let last = c.get(c.len() - 1)?
    Some((first, last))
}

fn main(root: Root) {
    root.stdout.print("{first_and_last(["a", "b", "c"])}")
    root.stdout.print("{first_and_last(Ring { items: [1, 2, 3] })}")
    root.stdout.print("{Ring { items: ["x", "y", "z"] }.get(5)}")
}
Listing 10-4: An interface with a type parameter examples/ch10-04-generic-interfaces/main.tsr
$ tessera run main.tsr
Some(("a", "c"))
Some((1, 3))
Some("z")

Container[T] describes anything you can index into and measure. List[Str] already has get and len with the right types, so it's a Container[Str]; our own Ring[T] is one too.

first_and_last[C: Container[T], T] takes any container and returns its item type. At each call, T is worked out from the container's methods: for ["a", "b", "c"] it is Str, for Ring { items: [1, 2, 3] } it is Int.

dyn: a value of any type

A bound like [T: Shape] means one particular type per call. A list holds one type, so a List[T] can't mix circles and squares. When you need that, use dyn Shape, a value of any type that satisfies Shape:

interface Shape {
    fn area(self) -> Float
    fn name(self) -> Str
}

struct Circle {
    r: Float,
}

fn area(self: Circle) -> Float {
    3.14159 * self.r * self.r
}

fn name(self: Circle) -> Str {
    "circle"
}

struct Square {
    side: Float,
}

fn area(self: Square) -> Float {
    self.side * self.side
}

fn name(self: Square) -> Str {
    "square"
}

fn pick(big: Bool) -> dyn Shape {
    if big {
        Square { side: 10.0 }
    } else {
        Circle { r: 0.5 }
    }
}

fn main(root: Root) {
    let shapes: List[dyn Shape] = [Circle { r: 1.0 }, Square { side: 2.0 }, pick(true)]
    for s in shapes {
        root.stdout.print("{s.name()}: {s.area()}")
    }
    let total = shapes.map(|s| s.area()).fold(0.0, |a, b| a + b)
    root.stdout.print("total: {total}")
    root.stdout.print("{pick(false)}")
}
Listing 10-5: A list of shapes of different types examples/ch10-05-dyn/main.tsr
$ tessera run main.tsr
circle: 3.14159
square: 4.0
square: 100.0
total: 107.14159
Circle { r: 0.5 }

A struct or enum value becomes a dyn Shape wherever one is expected: in an annotated let, a list of that type, an argument, or a function result, as in pick, whose two branches return different types. Only the interface's methods can be called on it.

dyn has a small cost: the value is boxed and calls go through a table of methods. Bounds have none. Use bounds by default, and dyn when a collection really mixes types.

One rule ties dyn to capabilities: only values that hold no capability, function or channel can become dyn, and that includes capabilities held in a field of a field (chapter 12). A dyn value can never hide authority inside it. To work with capabilities through an interface, use a bound, where the type stays visible in the signature.

Summary

Type parameters make functions and structs work for many types, with no cost at run time. Interfaces describe a set of methods, and any type that has them fits, built-in ones included. Generic interfaces take parameters that are worked out at each call. dyn holds values of different types behind one interface, at the price of an indirect call.

Interfaces are also the key to the next chapter: testing code that uses capabilities.