Rill v0.13 Reference

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.