Fixpoints over Functors
In the previous two blog posts we've looked at a combinator language for categories, as well as a data type for simply typed lambda terms. We've also had a look at translating from one to the other. Both blog posts assumed quite a bit of background knowledge, so our goal today will be to start at the very beginning and introduce recursion schemes step by step.
Semirings
To start, let's look at how we'd normally implement a datatype for semirings, along with an evaluator for it, and then see how we can generalise it using recursion schemes.
First, we'll add a constructor for each operation.
namespace Simple
data Semiring : Type -> Type where
Val : value -> Semiring value
One : Semiring value
Zero : Semiring value
Mult : Semiring value -> Semiring value -> Semiring value
Add : Semiring value -> Semiring value -> Semiring value
Now we would like to implement an evaluator for each of the constructors of the semiring.
Following the Haskell tradition of writing folds, it would look something like this:
eval : (add : a -> a -> a) -> (mult : a -> a -> a)
-> (zero : a) -> (one : a) -> Semiring a -> a
_ _ _ _ (Val v) = v
eval _ _ _ one One = one
eval _ _ zero _ Zero = zero
eval Mult x y) =
eval add mult zero one (
mult (eval add mult zero one x) (eval add mult zero one y)Add x y) =
eval add mult zero one ( add (eval add mult zero one x) (eval add mult zero one y)
This function, known as a fold, is capable of expressing any interpreter that consumes this data type. However, while folds are interesting theoretically, the function itself is fairly boilerplate - all it's doing is matching constructors of the datatype to individual functions that consume them.
Since we're going to be writing a lot of datatypes, we want to avoid writing an individual evaluator for each of them - we'd rather get them for free. If we had macros in our language, we could use them for this. But we don't need macros where we're going, as we've got category theory.
The starting idea is that the same way that we bundle up our constructors into a single datatype, we can bundle up our consumers together as well.
record SemiringAlgebra (a : Type) where
add : a -> a -> a
mult : a -> a -> a
zero : a
one : a
-- if we uncurry we can see that these are all just morphisms of a category
record SemiringAlgebra' (a : Type) where
add : (a, a) -> a
mult : (a, a) -> a
zero : () -> a
one : () -> a
Using SemiringAlgebra we can get a slightly cleaner looking evaluator:
eval' : SemiringAlgebra a -> Semiring a -> a
But it would involve the same amount of boilerplate as before.
The problem is that we are essentially declaring each concept twice - once as a constructor, and once as a function consuming it. What we would like is to do declare it once, and derive the rest from that single declaration.
Now, Semiring has the type Type -> Type
, and so does
SemiringAlgebra. So we want something of the type
(Type -> Type) -> (Type -> Type)
. This turns out
to be fairly straightforward:
Algebra : (Type -> Type) -> (Type -> Type)
Algebra f a = f a -> a
SemiringAlgebra' : Type -> Type
SemiringAlgebra' a = Algebra Semiring a
But how would we use such a thing? Naively, it seems that our evaluator has now become trivial:
evalNope : Algebra Semiring a -> Semiring a -> a
= alg s evalNope alg s
This type checks, however it does not do anything interesting, it merely applies the function that we've supplied to it, and we still need to define that function manually. What we really want is for this function to be derived for us from smaller pieces.
To resolve this, we notice that our original SemiringAlgebra record does not actually tell us how to tear down a full semiring. All it does is tell us how to tear down each individual layer. So let's start by describing these layers as a data type.
data SemiringLayer : Type -> Type -> Type where
Val : value -> SemiringLayer value expression
One : SemiringLayer value expression
Zero : SemiringLayer value expression
Mult : expression -> expression -> SemiringLayer value expression
Add : expression -> expression -> SemiringLayer value expression
The structure of this datatype is very similar to our original
Semiring. But whereas Semiring was inductive - we defined complex
expressions in terms of simpler ones - our new SemiringLayer only
defines the contents of a single layer. And rather than being
parametrised by a single type of values Type -> Type
, we
now take two parameters Type -> Type -> Type
: one for
values, and one for complex expressions.
The result is that our ideal evaluator would now look like this:
evalAlg : Algebra (SemiringLayer val) val -> Algebra Semiring val
In other words, we lift an algebra over a layer of a semiring to an
algebra over the entire semiring. The final piece of the puzzle is that
we would like to derive Semiring
from
SemiringLayer
, rather than defining them separately.
We do this by using a type-level fixpoint that operates on datatypes.
data Fix : (layer : Type -> Type) -> Type where
In : {layer : Type -> Type} -> layer (Fix layer) -> Fix layer
It is not unlike a standard fixpoint operator that works on functions:
partial
fix : (a -> a) -> a
= f (fix f)
fix f
-- Don't try this at home:
-- SemiringBoom : Type -> Type
-- SemiringBoom value = fix (SemiringLayer value)
Using Fix
, we can now define Semiring
inductively in terms of SemiringLayer
- each time that
SemiringLayer
expects an expression, Fix
will
fill that in with Fix SemiringLayer
as a subexpression:
Semiring : Type -> Type
Semiring value = Fix (SemiringLayer value)
-- example expression
ex1 : Semiring Nat
= In (Mult (In One) (In $ Val 2)) ex1
The small downside is that we now have In
around our
code, however this can be avoided using smart constructors.
Now we can finally define our evaluator uniformly without mentioning any individual constructor:
-- We'll need a functor constraint for what's to come:
Functor (SemiringLayer a) where
Val x) = Val x
map f (One = One
map f Zero = Zero
map f Mult x y) = Mult (f x) (f y)
map f (Add x y) = Add (f x) (f y)
map f (
-- Lift an algebra of a semiring layer to a semiring
eval : Algebra (SemiringLayer val) a -> Fix (SemiringLayer val) -> a
In op) = alg (map (eval alg) op) eval alg (
And since our eval
makes no reference to the
constructors of Semiring
, this means that we can generalise
it to an arbitrary functor.
-- Mission accomplished.
cata : Functor f => Algebra f a -> Fix f -> a
In op) = alg (map (cata alg) op) cata alg (
We can now apply our cata
to arbitrary datatypes (with
some restrictions, that we will talk about later in the blog), and
provided that we give them a functor constraint.
data ListLayer : Type -> Type -> Type where
Nil : ListLayer val expr
Cons : val -> expr -> ListLayer val expr
Functor (ListLayer a) where
= []
map f [] Cons x y) = Cons x (f y)
map f (
List : Type -> Type
List a = Fix (ListLayer a)
-- we get the evaluator for free!
foldr : Algebra (ListLayer a) a -> Fix (ListLayer a) -> a
= cata foldr
It's worth tracing out what exactly happens when we use
eval
- the gist of it is that there are two things that we
need to do: use map
to go under an f
to turn
f (Fix f)
into f a
, and then use
alg
to turn an f a
into a
.
We can split cata
into two mutually-inductive functions,
each of which is responsible for one of these steps. As a bonus, this
makes it structurally recursive over Fix f.
mutual
-- take a layer of `f a` to an `a`
fold : Functor f => (f a -> a) -> Fix f -> a
In x) = alg (mapFold alg x)
fold alg (
-- go underneath an f to turn Fix f to a
mapFold : Functor f => (f a -> a) -> f (Fix f) -> f a
= map (fold alg) op mapFold alg op
It can get tedious writing all these functor constraints after a while. If we had macros, we could automate this, but once again we can get away without macros with the help of category theory - using what is known as the Coyoneda trick.
The Coyoneda Trick
A lot has been written about the Yoneda lemma, and its dual cousin, Coyoneda. Personally, I find that it's one of those concepts that you don't so much "understand" as "get used to". And the best way to get used to something is to use it, which - luckily for us - we'll have plenty of opportunity to do.
The datatype representing Coyoneda is simple, it is a pair of a value inside of a functor, and a function that will map this value. Essentially, it is nothing more than a suspended map operation:
data Coyoneda : (Type -> Type) -> Type -> Type where
Map : {0 a : _} -> (a -> b) -> f a -> Coyoneda f b
-- map : (a -> b) -> f a -> f b
In other words, Coyoneda is to functors as SemiringLayer is to semirings - it's a data type that holds the contents of a functorial map, the same way that SemiringLayer holds values of some semiring. Like SemiringLayer, Coyoneda comes with its own notion of algebra, and we will make this precise in a futre blog post.
-- When `f` is a functor, we can tear down a layer of Coyoneda
mapCoyo : Functor f => Coyoneda f b -> f b
Map f fa) = map f fa mapCoyo (
Using this trick, we can modify our generic evaluator to no longer need a functor constraint:
mcata : Algebra (Coyoneda f) c -> Fix f -> c
In op) = alg (Map (mcata alg) op) mcata alg (
The m
in mcata
stands for
Mendler
, and Algebra (Coyoneda f) a
is
commonly known as a Mendler Algebra
.
MAlgebra : (Type -> Type) -> (Type -> Type)
MAlgebra f a = Algebra (Coyoneda f) a
Kmett wrote a great
post about catamorphisms and Mendler catamorphisms, and it includes
how to go from cata
to mcata
and vice
versa.
Algebra Transformers
The downside of using Mendler-style recursion schemes is that our algebras become a tad more involved. Compare the following:
-- monoid of natural numbers, standard algebra style
algPlus : Algebra (ListLayer Nat) Nat
= 0
algPlus [] Cons x y) = x + y
algPlus (
-- monoid of natural numbers, Mendler style
malgPlus : MAlgebra (ListLayer Nat) Nat
Map f []) = 0
malgPlus (Map f (Cons x y)) = x + f y malgPlus (
Whereas a normal algebra merely accesses the values of each constructor, a Mendler algebra carries around an extra function that we need to apply before accessing sub-expressions.
One way around this is to use algebra transformers, ie. a way to turn a simple algebra into a more complex one. In this case, we can derive a Mendler algebra from a standard one:
malgToAlg : Algebra (ListLayer v) a -> MAlgebra (ListLayer v) a
Map f []) = alg []
malgToAlg alg (Map f (Cons x y)) = alg (Cons x (f y)) malgToAlg alg (
We can now use standard algebras with mcata
, as if they
were Mendler algebras:
listEval' : Algebra (ListLayer v) a -> Fix (ListLayer v) -> a
= mcata (malgToAlg alg) listEval' alg
What's interesting is that if we look at malgToAlg
closely, we'll see that it's nothing other than an inside out functor
interface for ListLayer. So in the end we did about the same amount of
work as before, but at least with Mendler algebras we could choose when
to do it, and we were not forced to write all of our interfaces up
front.
If we had macros at our disposal, we could actually derive malgToAlg automatically. And... well, now I actually wish that I had macros at my disposal here. (Or at least that I had a working knowledge of elaborator reflection, which could accomplish the same thing).
At this point, we might wonder if going through Mendler algebras is worth it if they are equally expressive to standard algebras. As we will soon see, the answer is yes, as a small generalisation to Mendler algebras - leading to what I call "Kan algebras" - will give us a vast amount of expressive power that can capture many datatypes of interest besides those representable as functors on sets.
But before we get to that, let us talk about monads.