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
andOrder
requires that the inner types of theenum
implementEq
andOrder
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.