FunctionK
API Documentation: FunctionK
A FunctionK
transforms values from one first-order-kinded type (a type that takes a single type
parameter, such as List
or Option
) into another first-order-kinded type. This transformation is
universal, meaning that a FunctionK[List, Option]
will translate all List[A]
values into an
Option[A]
value for all possible types of A
. This explanation may be easier to understand if we
first step back and talk about ordinary functions.
Ordinary Functions
Consider the following scala method:
def first(l: List[Int]): Option[Int] = l.headOption
This isn't a particularly helpful method, but it will work as an example. Instead of writing this as a method, we could have written this as a function value:
val first: List[Int] => Option[Int] = l => l.headOption
And here, =>
is really just some syntactic sugar for Function1
, so we could also write that as:
val first: Function1[List[Int], Option[Int]] = l => l.headOption
Let's cut through the syntactic sugar even a little bit further. Function1
isn't really a special type.
It's just a trait that looks something like this:
// we are calling this `MyFunction1` so we don't collide with the actual `Function1`
trait MyFunction1[A, B] {
def apply(a: A): B
}
So if we didn't mind being a bit verbose, we could have written our function as:
val first: Function1[List[Int], Option[Int]] = new Function1[List[Int], Option[Int]] {
def apply(l: List[Int]): Option[Int] = l.headOption
}
Abstracting via Generics
Recall our first
method:
def first(l: List[Int]): Option[Int] = l.headOption
The astute reader may have noticed that there's really no reason that this method needs to be tied directly to Int
. We could use generics to make this a bit more general:
def first[A](l: List[A]): Option[A] = l.headOption
But how would we represent this new first
method as a =>
/Function1
value? We are looking for something like a type of List[A] => Option[A] forAll A
, but this isn't valid scala syntax. Function1
isn't quite the right fit, because its apply
method doesn't take a generic type parameter.
Higher Kinds to the Rescue
It turns out that we can represent our universal List
to Option
transformation with something that looks a bit like Function1
but
that adds a type parameter to the apply
method and utilizes higher kinds:
trait MyFunctionK[F[_], G[_]] {
def apply[A](fa: F[A]): G[A]
}
Cats provides this type as FunctionK
(we used MyFunctionK
for our example type to avoid confusion). So now we can write first
as a FunctionK[List, Option]
value:
import cats.arrow.FunctionK
val first: FunctionK[List, Option] = new FunctionK[List, Option] {
def apply[A](l: List[A]): Option[A] = l.headOption
}
Syntactic Sugar
If the example above looks a bit too verbose for you, the kind-projector
compiler plugin provides a more concise syntax.
After adding the plugin to your project, you could write the first
example as:
val first: FunctionK[List, Option] = λ[FunctionK[List, Option]](_.headOption)
Cats also provides a ~>
type alias for FunctionK
, so an even more concise version would be:
import cats.~>
val first: List ~> Option = λ[List ~> Option](_.headOption)
Being able to use ~>
as an alias for FunctionK
parallels being able to use =>
as an alias for Function1
.
Use-cases
FunctionK
tends to show up when there is abstraction over higher-kinds. For example, interpreters for free monads and free applicatives are represented as FunctionK
instances.
Types with more than one type parameter
Earlier it was mentioned that FunctionK
operates on first-order-kinded types (types that take a single type parameter such as List
or Option
). It's still possible to use FunctionK
with types that would normally take more than one type parameter (such as Either
) if we fix all of the type parameters except for one. For example:
type ErrorOr[A] = Either[String, A]
val errorOrFirst: FunctionK[List, ErrorOr] =
λ[FunctionK[List, ErrorOr]](_.headOption.toRight("ERROR: the list was empty!"))
Natural Transformation
In category theory, a natural transformation provides a morphism between Functors while preserving the internal structure. It's one of the most fundamental notions of category theory.
If we have two Functors F
and G
, FunctionK[F, G]
is a natural transformation via parametricity. That is, given fk: FunctionK[F, G]
, for all functions A => B
and all fa: F[A]
the following are equivalent:
fk(F.map(fa)(f)) <-> G.map(fk(fa))(f)
We don't need to write a law to test the implementation of the fk
for the above to be true. It's automatically given by parametricity.
Thus natural transformation can be implemented in terms of FunctionK
. This is why a parametric polymorphic function FunctionK[F, G]
is sometimes referred as a natural transformation. However, they are two different concepts that are not isomorphic.
For more details, Bartosz Milewski has written a great blog post titled "Parametricity: Money for Nothing and Theorems for Free".