16. trait: constrained generics and operator overloading

Up to here a generic function has known nothing about T — it cannot compare it, print it or call a method on it. A trait attaches a capability constraint to a type parameter. Declare a trait, write an impl for a concrete type, then constrain the generic with [T: Trait]:

trait Area[T] {
  fn area(s: T) -> Float
  fn bigger_than(s: T, limit: Float) -> Bool = area(s) > limit
}

type Rect = { w: Float, h: Float }

impl Area[Rect] {
  fn area(s: Rect) -> Float = s.w * s.h
}

fn total_area[T: Area](xs: List[T]) -> Float =
  fold(xs, 0.0, (acc, x) => acc + area(x))

pub fn main() -> Unit !io = {
  let rooms = [Rect { w: 3.0, h: 4.0 }, Rect { w: 2.0, h: 2.0 }]
  println(to_string(total_area(rooms)))
  # a trait method is an ordinary function name, so a UFCS dot call works too
  println(to_string(rooms[0].bigger_than(10.0)))
}
Open in playground
16.0
true

The rules are few: a trait has exactly one type parameter; each "trait × type" pair admits exactly one impl in the whole program; and an impl has to be written in the module of either the trait or the subject type (the orphan rule). A method with a default body (bigger_than above) may be left out of an impl, and writing it is an override.

Sorting: Ord and the comparison operators

The built-in trait Ord[T] — one method, cmp(a: T, b: T) -> Int, negative/zero/ positive for less/equal/greater — is what bridges < <= > >=. Int/Float/String are ordered from the start; give a type of your own an Ord impl (or just derive Ord) and it can use the comparison operators, be passed where [T: Ord] is asked for, and be fed to the sorting functions:

type Card = { rank: Int, name: String } derive Show, Ord

fn max2[T: Ord](a: T, b: T) -> T = if a < b { b } else { a }

pub fn main() -> Unit !io = {
  let hand = [Card { rank: 3, name: "queen" }, Card { rank: 1, name: "pawn" }]
  # derive Ord compares field by field in declaration order (a sum type compares constructors first)
  println(to_string(hand[1] < hand[0]))
  println(max2("pear", "apple"))
  println(to_string(sort([3, 1, 2])))
  println(to_string(map(sort(hand), c => c.name)))
  println(to_string(max_by(hand, c => c.rank)))
}
Open in playground
true
pear
[1, 2, 3]
["pawn", "queen"]
Some(Card { rank: 3, name: "queen" })

The list functions that go with it are stable sorts that keep the first of a tie: sort/max/min want Ord on the element, sort_by(xs, cmp) takes a comparison function of your own, and max_by/min_by(xs, key) take the extreme by a key (whose type needs Ord).

A trait method, or any function with a bound, can be passed around as a bare function value. What it needs is an expected function type, because that is what says which type the bound is discharged at; the wrapper is then written for you, dictionary and all:

fn shout[T: Show](xs: List[T]) -> List[String] = map(xs, to_string)

pub fn main() -> Unit !io = {
  println(join(shout([1, 2, 3]), " "))
  # the bound here is `shout`'s own, so the wrapper closes over the dictionary
  # `shout` was handed, which is what `x => to_string(x)` would have done
  println(join(shout(["a", "b"]), " "))
}
Open in playground
1 2 3
"a" "b"

Without one, as in let f = to_string, there is nothing to discharge the bound at and the compiler says so; write the type, or write the lambda with an annotated parameter.

One list, many types: a record of functions

A List[T] holds one T. When you want a list of different types that all support the same operation, Dawn has no dyn Trait to reach for. Capture the operation in a record of functions, hide the record behind an opaque type, and give that type an impl of its own. The bound is discharged where the value is packed, which is the last place the concrete type is still known:

type ShownRepr = { render: fn() -> String }

pub opaque type Shown = ShownRepr

pub fn shown[T: Show](x: T) -> Shown = {
  let r: ShownRepr = ShownRepr { render: () => show(x) }
  r
}

impl Show[Shown] {
  fn show(s: Shown) -> String = {
    let r: ShownRepr = s
    r.render()
  }
}

pub fn main() -> Unit !io = {
  let xs: List[Shown] = [shown(1), shown("two"), shown(true)]
  for x in xs {
    println("${x}")
  }
}
Open in playground
1
"two"
true

The second line keeps its quotes. That is what Show[String] renders, here and everywhere else, and not a slip in the example.

The trick is complete for a trait that takes its subject in one position: Show, Hash, anything shaped fn(T) -> .... It does not reach Eq or Ord, whose eq(a: T, b: T) and cmp(a: T, b: T) need two values of the same type, and packing is exactly what throws that fact away. So a heterogeneous List is available and a heterogeneous Map key is not. Why the line falls there, and why Dawn does not add trait objects, is in trait.md §10, in Chinese.

The v1 boundary: an impl's subject can only be a non-generic named type or Int/Float/Bool/String (there are no conditional impls, and List[T] cannot be a subject); and a call under a trait constraint is not available in comptime. The full design is in trait.md, in Chinese.