Automatic Derivation

Flix supports automatic derivation of several traits, including:

  • Eq — to derive structural equality on the values of a type.
  • Order — to derive a total ordering on the values of a type.
  • ToString — to derive a human-readable string representation on the values of a type.
  • Sendable — to enable the values of an (immutable) type to be sent over a channel.
  • Coerce - to convert simple data types to their underlying representation.

Derivation of Eq and Order

We can automatically derive instances of the Eq and Order traits using the with clause in the enum declaration. For example:

enum Shape with Eq, Order {
    case Circle(Int32)
    case Square(Int32)
    case Rectangle(Int32, Int32)
}

The derived implementations are structural and rely on the order of the case declarations:

def main(): Unit \ IO = 
    println(Circle(123) == Circle(123)); // prints `true`.
    println(Circle(123) != Square(123)); // prints `true`.
    println(Circle(123) <= Circle(123)); // prints `true`.
    println(Circle(456) <= Square(123))  // prints `true`.

Note: Automatic derivation of Eq and Order requires that the inner types of the enum implement Eq and Order themselves.

Derivation of ToString

We can also automatically derive ToString instances:

enum Shape with ToString {
    case Circle(Int32)
    case Square(Int32)
    case Rectangle(Int32, Int32)
}

Then we can take advantage of string interpolation and write:

def main(): Unit \ IO = 
    let c = Circle(123);
    let s = Square(123);
    let r = Rectangle(123, 456);
    println("A ${c}, ${s}, and ${r} walk into a bar.")

which prints:

A Circle(123), Square(123), and Rectangle(123, 456) walk into a bar.

Derivation of Sendable

We can automatically derive implementations of the Sendable trait (which allow values of a specific type to be sent over a channel). For example:

enum Shape with Sendable, ToString {
    case Circle(Int32)
}

def main(): Unit \ IO = 
    region rc {
        let (tx, rx) = Channel.buffered(rc, 10);
        Channel.send(Circle(123), tx); // OK, since Shape is Sendable.
        println(Channel.recv(rx))
    }

We cannot derive Sendable for types that rely on scoped mutable memory. For example, if we try:

enum Shape[r: Region] with Sendable {
    case Circle(Array[Int32, r])
}

The Flix compiler emits a compiler error:

❌ -- Safety Error --------------------------------------

>> Cannot derive 'Sendable' for type Shape[b27587945]

Because it takes a type parameter of kind 'Region'.

1 | enum Shape[r: Region] with Sendable {
                               ^^^^^^^^
                               unable to derive Sendable.

This is because mutable data is not safe to share between threads.

Derivation of Coerce

We can automatically derive implementations of the Coerce type class. The Coerce class converts a simple (one-case) data type to its underlying implementation.

enum Shape with Coerce {
    case Circle(Int32)
}

def main(): Unit \ IO =
    let c = Circle(123);
    println("The radius is ${coerce(c)}")

We cannot derive Coerce for an enum with more than one case. For example, if we try:

enum Shape with Coerce {
    case Circle(Int32)
    case Square(Int32)
}

The Flix compiler emits a compiler error:

❌ -- Derivation Error --------------------------------------------------

>> Cannot derive 'Coerce' for the non-singleton enum 'Shape'.

1 | enum Shape with Coerce {
                    ^^^^^^
                    illegal derivation

'Coerce' can only be derived for enums with exactly one case.