Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generics and Traits

Type parameters

A function can take type parameters in square brackets. Uppercase names are types; lowercase ones are effect rows, as in fn map_all[A, B, e](xs: List[A], f: A -> B ! e) -> List[B] ! e, which performs whatever f performs. When several arguments share a row, it is the union of what they perform, and fail[E] next to a row (next: Int -> Int ! fail[E], e) takes E from the argument’s error type, so a wrapper that catches it is left with just e.

fn last_or[T](xs: List[T], fallback: T) -> T
= xs.last.or(fallback)

test generic = last_or([1, 2], 0) == 2 and last_or([], "none") == "none"

Without a bound, a type parameter can be stored, passed on, compared with == and printed with .str, since every value supports those structurally. To call trait methods on it, or use < and +, give it a bound.

Traits and impls

A trait names a set of methods. The first parameter of each one has type Self. A method with = body has a default that an impl may override. An impl gives the methods for one type:

trait Shape
  fn area(s: Self) -> F64
  fn name(s: Self) -> Str = "shape"
  fn describe(s: Self) -> Str = "{s.name} with area {s.area}"

type Circle = {r: F64}
type Square = {side: F64}

impl Shape for Circle
  fn area(c: Circle) -> F64 = 3.0 * c.r * c.r
  fn name(c: Circle) -> Str = "circle"

impl Shape for Square
  fn area(s: Square) -> F64 = s.side * s.side

test traits = Circle{r: 1.0}.describe == "circle with area 3.0" and Square{side: 2.0}.describe == "shape with area 4.0"

Methods are called like any other function: c.area, c.area() or area(c). An impl must give every method that has no default, with the trait’s signature, and nothing else. The checker reports E_IMPL_MISSING, E_IMPL_SIG or E_IMPL_EXTRA with the line to write.

There is one impl per trait and type in a program, and it lives in the package of the trait or of the type. To implement someone else’s trait for someone else’s type, wrap the type in a newtype.

Bounds

[T: Shape] says T must implement Shape, and lets the body use its methods. + combines bounds:

fn total_area[T: Shape](xs: List[T]) -> F64
= xs.map(area).sum

fn largest[T: Ord + Show](xs: List[T]) -> Str
= xs.sort.last.map(_.show).or("empty")

test bounds = total_area([Square{side: 1.0}, Square{side: 2.0}]) == 5.0 and largest([3, 9, 4]) == "9"

xs.map(area) passes a trait method as a value; it resolves to the impl for the element type. Bounds are checked in two places: in the body, which may only use what the bounds allow, and at each call, which must pass types that have the impls. The second error names the type and the fix:

type Point = {x: Int, y: Int}

fn largest[T: Ord + Show](xs: List[T]) -> Str
= xs.sort.last.map(_.show).or("empty")

fn main() -> Unit ! log
= log(largest([Point{x: 1, y: 2}]))
$ sspur check unbounded.ssp
unbounded.ssp:7:7 E_TRAIT_MISSING largest needs T: Ord, but Point does not implement Ord
  hint: add 'derive Ord' to type Point, or write 'impl Ord for Point'
unbounded.ssp:7:7 E_TRAIT_MISSING largest needs T: Show, but Point does not implement Show
  hint: add 'derive Show' to type Point, or write 'impl Show for Point'

Resolution is static. Each bounded function is compiled once per type it is called with, so a trait call costs the same as a direct call, in the interpreter and in native code.

Operators and derive

Operators are trait methods. impl Add for Money gives Money a +; Sub, Mul, Div, Neg and Index[K, V] work the same way. derive writes structural impls for you: Eq and Ord compare fields in declaration order (variants by name), and a field whose type has its own impl Eq is compared with it, as it is inside an Opt, a list or a tuple, Show prints the value as source, Hash hashes that text and Json encodes it like json.encode.

type Money = {cents: Int} derive Eq, Ord, Show

impl Add for Money
  fn add(a: Money, b: Money) -> Money = Money{cents: a.cents + b.cents}

type Version = {major: Int, minor: Int} derive Eq, Ord, Show

test ops = Money{cents: 150} + Money{cents: 75} == Money{cents: 225} and Money{cents: 1} < Money{cents: 2}
test derived = [Version{major: 1, minor: 9}, Version{major: 1, minor: 10}].sort.last == some(Version{major: 1, minor: 10})
test shown = largest([Version{major: 2, minor: 0}, Version{major: 1, minor: 5}]) == "Version{major: 2, minor: 0}"
TraitMethodGives you
Eqeq(a, b) -> Bool==, !=
Ordcmp(a, b) -> Int<, <=, >, >=, sort, min, max
Showshow(x) -> Str.show, used by largest above
Hashhash(x) -> Inthashing
Jsonto_json(x) -> Strjson.encode
Add, Sub, Mul, Divadd(a, b) -> Self, …+, -, *, /
Negneg(a) -> Selfunary -
Index[K, V]index(x, k) -> Vx[k]

Built-in types already implement the traits that make sense for them; Int arithmetic keeps trapping on overflow even when it is reached through a bound. The language reference has every rule. Next: Collections and Pipelines.