Using Traits

A trait defines functionality that can be shared between types. For example, one could define an Equal trait to represent equality.

trait Equal[T] {
  fn .equal(x: T, y: T) -> Bool;
}

The Equal trait is generic on a type T, and has a single method, equal, which tests for equality between two values of type T.

Traits only define the signature of their methods, so to actually call these methods, the trait has to be implemented.

struct Color({ r: N32, g: N32, b: N32 });
const red: Color = Color({ r: 255, g: 0, b: 0 });
const green: Color = Color({ r: 0, g: 255, b: 0 });
const blue: Color = Color({ r: 0, g: 0, b: 255 });

impl equal_color: Equal[Color] {
  fn equal(x: Color, y: Color) -> Bool {
    x.r == y.r && x.g == y.g && x.b == y.b
  }
}

Now, we've defined an implementation named equal_color of the trait Equal for the type Color, so we can now call the equal method on Colors:

red.equal(blue) // false
red.equal(red) // true

If we wanted to use this method on other types, we could create more implementations.

impl equal_bool: Equal[Bool] {
  fn equal(x: Bool, y: Bool) -> Bool {
    x && y || !x && !y
  }
}

(Most of the time, the names of implementations don't matter, but they are used to disambiguate if multiple implementations could apply.)

Implementation Parameters

The advantage of using traits instead of just defining individual functions for types is that, with traits, one can abstract over any type that has an implementation of a certain trait. For example, we could write a not_equal function:

fn not_equal[T; Equal[T]](a: T, b: T) -> Bool {
  !a.equal(b)
}

The not_equal function takes one type parameter, T, and one implementation parameter, of the trait Equal[T]. (The semicolon inside the generic parameters separates the type parameters from the implementation parameters.) Since a and b are of type T, and there is an implementation of Equal[T], equal can be called on a and b.

We can then call this function on any types that we've implemented Equal for.

not_equal(red, blue) // true
not_equal(true, true) // false

Without traits, we would have to manually implement this function for every type we created an equal method for.

Implementations themselves can also be generic and take implementation parameters. This allows us to implement Equal[List[T]] for any T where Equal[T] is implemented:

impl equal_list[T; Equal[T]]: Equal[List[T]] {
  fn equal(x: List[T], y: List[T]) -> Bool {
    if x.len() != y.len() {
      return false;
    }
    while x.pop_front() is Some(a) && y.pop_front() is Some(b) {
      if not_equal(a, b) {
        return false;
      }
    }
    true
  }
}

The signature of equal_list says that for any type T, if there is an implementation of Equal[T], then equal_list implements Equal[List[T]]. We can thus now check equality for lists of colors or lists of booleans:

[red, green, blue].equal([red, blue, green]) // false
[true, false].equal([true, false]) // true

We can even check equality for lists of lists of colors!

[[red, blue], [green, red]].equal([[red, blue], [green, red]]) // true
[[], [red]].equal([[red], []]) // false