Traits

Traits, also known as type classes, support abstraction and modularity. The Flix trait system is similar to that of Haskell and Rust, but not identical. Traits in Flix support associated types, associated effects, and higher-kinded types.

We illustrate traits with an example.

We can define equality on Option[Int32] as follows:

def equals(x: Option[Int32], y: Option[Int32]): Bool = 
    match (x, y) {
        case (None, None)         => true
        case (Some(v1), Some(v2)) => v1 == v2
        case _                    => false
    }

We can also define equality on List[Int32] as follows:

def equals(x: List[Int32], y: List[Int32]): Bool = 
    match (x, y) {
        case (Nil, Nil)           => true
        case (v1 :: xs, v2 :: ys) => v1 == v2 and equals(xs, ys)
        case _                    => false
    }

But what if we wanted a common abstraction for data types which support equality?

Here we can use traits. We can define an Equatable trait:

trait Equatable[t] {
    pub def equals(x: t, y: t): Bool
}

which has a single equals trait signature. The trait is polymorphic over the type parameter t which means that we can implement Equatable for both Option[t] and List[t]:

instance Equatable[Option[t]] with Equatable[t] {
    pub def equals(x: Option[t], y: Option[t]): Bool = 
        match (x, y) {
            case (None, None)         => true
            case (Some(v1), Some(v2)) => Equatable.equals(v1, v2)
            case _                    => false
        }
}

Notice that we did not implement Equatable for Option[Int32], but instead for any Option[t] as long as t itself is equatable. Moreover, instead of comparing v1 and v2 directly using ==, we call Equatable.equals on them.

We can also implement Equatable for List[t]:

instance Equatable[List[t]] with Equatable[t] {
    pub def equals(x: List[t], y: List[t]): Bool = 
        use Equatable.equals;
        match (x, y) {
            case (Nil, Nil)           => true
            case (v1 :: xs, v2 :: ys) => equals(v1, v2) and equals(xs, ys)
            case _                    => false
        }
}

Assuming we also implement Equatable for Int32, we can use Equatable to compute whether two Option[Int32] values are equal. But we can also compute if two Option[List[Int32]] values are equal! This demonstrates the power of abstraction: We have implemented instances for Option[t] and List[t] and we can now reuse these instances everywhere.

We can use our newly defined Equatable trait to write polymorphic functions.

For example, we can define a function to compute if an element occurs in a list:

def memberOf(x: t, l: List[t]): Bool with Equatable[t] = 
    match l {
        case Nil     => false
        case y :: ys => Equatable.equals(x, y) or memberOf(x, ys)
    }

We can use memberOf for a list of any type, as the element type implements Equatable.

Note: In the Flix Standard Library the Equatable trait is called Eq. Moreover, the == operator is syntactic sugar for the trait signature Eq.eq.

Sealed Traits

We can declare a trait as sealed to restrict who can implement the trait.

For example:

mod Zoo {
    sealed trait Animal[a] {
        pub def isMammal(x: a): Bool
    }

    instance Animal[Giraffe] {
        pub def isMammal(_: Giraffe): Bool = true
    }

    instance Animal[Penguin] {
        pub def isMammal(_: Penguin): Bool = true
    }

    pub enum Giraffe
    pub enum Penguin
}

Here we can implement instances for Animal and Giraffe because they occur in the same module as the Animal trait. But we cannot implement Animal from outside the Zoo module. If we try:

mod Lake {
    pub enum Swan

    instance Zoo.Animal[Swan] {
        pub def isMammal(_: Swan): Bool = false
    }
}

then Flix reports:

❌ -- Resolution Error -------------------------------------------------- 

>> Class 'Zoo.Animal' is sealed from the module 'Lake'.

21 |     instance Zoo.Animal[Swan] {
                  ^^^^^^^^^^
                  sealed class.

Malformed Traits

A trait is not a C# or Java-style interface. Specifically:

  • every trait must have exactly one type parameter, and
  • every signature must mention that type parameter.

For example, the following trait is incorrect:

trait Animal[a] {
    pub def isMammal(x: a): Bool      // OK     -- mentions a.
    pub def numberOfGiraffes(): Int32 // NOT OK -- does not mention a.
}

If we compile the above trait, Flix reports:

❌ -- Resolution Error -------------------------------------------------- 

>> Unexpected signature 'numberOfGiraffes' which does not mention the type 
>> variable of the trait.

7 |     pub def numberOfGiraffes(): Int32 
                ^^^^^^^^^^^
                unexpected signature.

The problem is that the signature for numberOfGiraffes does not mention the type parameter a.

Complex Instances

A trait instance must be defined on:

  • exactly one type constructor —
  • that is applied to zero or more distinct type variables.

For example, given the Equatable trait from before:

trait Equatable[t] {
    pub def equals(x: t, y: t): Bool
}

We can implement instances for e.g.:

  • Option[a]
  • List[a]
  • (a, b)

but we cannot implement instances for e.g.:

  • Option[Int32]
  • List[String]
  • (a, Bool)
  • Map[Int32, v]

If we try to implement an instance for e.g. List[Int32] Flix reports:

❌ -- Instance Error -------------------------------------------------- 

>> Complex instance type 'List[Int32]' in 'Equatable'.

6 | instance Equatable[List[Int32]] {
             ^^^^^^^^^
             complex instance type

An instance type must be a type constructor applied to zero or more 
distinct type variables.

Overlapping Instances

We cannot implement two instances of of the same trait for overlapping types.

For example, if we try to implement two instances of Equatable for List[t]:

instance Equatable[List[t]] {
    pub def equals(x: List[t], y: List[t]): Bool = ???
}

instance Equatable[List[t]] {
    pub def equals(x: List[t], y: List[t]): Bool = ???
}

then Flix reports:

❌ -- Instance Error -------------------------------------------------- 

>> Overlapping instances for 'Equatable'.

1 | instance Equatable[List[t]] {
              ^^^^^^^^^
              the first instance was declared here.

4 | instance Equatable[List[t]] {
             ^^^^^^^^^
             the second instance was declared here.