Chapter 5
Traits
A trait declares an interface; an impl provides it for one type. Both builtin types and user types can implement traits.
trait Describe(a) describe(a) -> Str priority(a) -> Int impl Describe(Shape) fn describe(s) = "shape " + show(s) fn priority(s) = 2 impl Describe(Int) fn describe(n) = "int " + int_to_str(n) fn priority(n) = 1 fn announce(x) = println(describe(x)) # works for any implementing type
A method may come with a default, written after its signature with its parameters named, and an impl that leaves the method out gets it. The default is checked once per such impl, at that impl's type, so a default that calls another method of the trait calls that impl's:
trait Describe(a) describe(a) -> Str priority(x: a) -> Int = 1 banner(x: a) -> Str = describe(x) + " (" + int_to_str(priority(x)) + ")"
A trait may require others: trait Pretty(a): Show, Describe says a type implements Pretty only if it implements those too, and an impl of Pretty for a type that does not is refused, naming what is missing.
A method's name belongs to its trait, and two traits may use the same one: describe(x) is settled by the type of x, whichever trait is implemented for it, and where that cannot tell β both are, or x is a parameter whose type is not known yet β the call says which, Describe.describe(x), which is always allowed. Constructor names stay global. Dispatch is resolved statically during monomorphization: no dictionaries, no vtables, and a compile error if an implementation is missing.
An impl may target a generic type by naming its parameters, in which case it covers every instantiation:
impl Show(List(a)) fn show(l) = "{" + join(map(l, \x -> show(x)), " ") + "}"
The show(x) on elements resolves when the element type is known, so List(Box(Int)) composes both impls without any where clause. An impl that quietly works for only one instantiation is rejected.
Objects
Dispatch is static, so a List(a) holds one type. Where a list has to hold a dog and a robot because both can speak, a value is made an object of the trait: Speak(rex) is a record of the trait's methods, each a closure over rex, and its type is the trait's name.
trait Speak(a) speak(a) -> Str greet(a, Str) -> Str fn main() = all = Cons(Speak(Dog("rex")), Cons(Speak(Robot(7)), Nil)) # a List(Speak) say(all) fn say(xs) = match xs Nil -> 0 Cons(s, rest) -> println(s.speak() + ", " + s.greet("ada")) say(rest)
Written out, Speak(rex) is Speak.obj(\ -> speak(rex), \o -> greet(rex, o)): an ordinary record whose fields hold functions, made by a constructor the trait declares and a program cannot name. A field that holds a function is called as s.speak(). Each call inside the closures dispatches on the type of rex, statically, as every method call does; what is paid is the closures β one allocation per method when the object is made β and the indirect call through each. Nothing else is boxed, and no dictionary travels with a value.
A trait is the type of objects when every method takes the trait's type once, and first: same(a, a) cannot be, since an object does not know what the other one is, nor can a method that answers with the type. Speak(x) on such a trait says which method is in the way. A record of functions written by hand β Shape(area: () -> Float, name: Str) β is the same thing with no trait at all, and a function's type is written the way a lambda reads: (Int, Str) -> Bool, or () -> Str for one that takes nothing, which is \ -> e.
Built-in traits
Two traits are built in and change how the language's own operators behave:
impl Show(Money) # replaces derived printing fn show(m) = ... impl Ord(Money) # enables <, <=, >, >= fn compare(a, b) = ... # negative, zero or positive
Without a Show impl, every data value already prints structurally (Cons(1, Nil)), and == already compares structurally.