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}"
| Trait | Method | Gives you |
|---|---|---|
Eq | eq(a, b) -> Bool | ==, != |
Ord | cmp(a, b) -> Int | <, <=, >, >=, sort, min, max |
Show | show(x) -> Str | .show, used by largest above |
Hash | hash(x) -> Int | hashing |
Json | to_json(x) -> Str | json.encode |
Add, Sub, Mul, Div | add(a, b) -> Self, … | +, -, *, / |
Neg | neg(a) -> Self | unary - |
Index[K, V] | index(x, k) -> V | x[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.