Higher-Kinded Types
Flix supports higher-kinded types, hence traits can abstract over type constructors.
For example, we can write a trait that capture iteration over any
collection of the shape t[a]
where t
is a type constructor of kind
Type -> Type
and a
is the element type of kind Type
:
trait ForEach[t: Type -> Type] {
pub def forEach(f: a -> Unit \ ef, x: t[a]): Unit \ ef
}
To use higher-kinded types Flix requires us to provide kind annotations, i.e.
we had to write t: Type -> Type
to inform Flix that ForEach
abstracts over
type constructors.
We can implement an instance of the ForEach
trait for Option[t]
:
instance ForEach[Option] {
pub def forEach(f: a -> Unit \ ef, o: Option[a]): Unit \ ef = match o {
case None => ()
case Some(x) => f(x)
}
}
and we can implement an instance for List[t]
:
instance ForEach[List] {
pub def forEach(f: a -> Unit \ ef, l: List[a]): Unit \ ef = List.forEach(f, l)
}
Note: Flix does not have a
ForEach
trait, but instead has the much more powerful and versatileFoldable
trait.
The Flix Kinds
Flix supports the following kinds:
Type
: The kind of Flix types.- e.g.
Int32
,String
, andList[Int32]
.
- e.g.
RecordRow
: The kind of rows used in records- e.g. in
{x = Int32, y = Int32 | r}
the type variabler
has kindRecordRow
.
- e.g. in
SchemaRow
: The kind of rows used in first-class Datalog constraints- e.g. in
#{P(Int32, Int32) | r}
the type variabler
has kindSchemaRow
.
- e.g. in
Flix can usually infer kinds. For example, we can write:
def sum(r: {x = t, y = t | r}): t with Add[t] = r#x + r#y
and have the kinds of t: Type
and r: RecordRow
automatically inferred.
We can also explicitly specify them as follows:
def sum[t: Type, r: RecordRow](r: {x = t, y = t | r}): t with Add[t] = r#x + r#y
but this style is not considered idiomatic.
Flix requires explicit kind annotations in four situations:
- For type parameters of non-Type kind on enums.
- For type parameters of non-Type kind on type aliases.
- For type parameters of non-Type kind on traits.
- For type members of a non-Type kind in a trait.
The most common scenario where you will need a kind annotation is when you want a type parameter or type member to range over an effect.
Higher-Kinded Types vs. Associated Types
In practice higher-kinded types and associated types can be used to define similar abstractions.
For example, as we have seen, we can define the ForEach
trait in two different
ways:
With a higher-kinded type:
trait ForEach[t: Type -> Type] {
pub def forEach(f: a -> Unit \ ef, x: t[a]): Unit \ ef
}
and with an associated type:
trait ForEach[t] {
type Elm
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef
}
In the case of ForEach
, the definition with an associated type is more
flexible, since we can implement an instance for String
with associated
element type Char
. However, higher-kinded types are still useful. For example,
the Flix Standard Library defines the Functor
trait as:
trait Functor[m : Type -> Type] {
pub def map(f: a -> b \ ef, x: m[a]): m[b] \ ef
}
Notably the kind of m
ensures that every Functor
implementation is structure
preserving. That is, we know that when we map
over e.g. an Option[a]
then we
get back an Option[b]
.