To begin, consider a functor . An object of and a morphism comprise an -algebra. A morphism bewtween algebras and is a map such that .
In Haskell we have
newtype Algebra f a
= Algebra { runAlg :: Functor f => f a -> a }
By carefully choosing , we can encode the structure of monoids. Define , then an algebra of is a map , which is a pair of maps and , identity and multiplication respectively.
data MonoidF a
= Mempty
| Mappend a a
deriving Functor
class Monoid' a where
monoidCarrier :: Algebra MonoidF a
instance Monoid a => Monoid' a where
monoidCarrier
= Algebra $ \case x of
Mappend x y -> mappend x y
Mempty -> mempty
instance Monoid' a => Monoid a where
mappend x y
= runAlg monoidStructure (Mappend x y)
mempty
= runAlg monoidSstructure Mempty
Lambek’s Lemma
Lemma. If has an initial algebra then witnesses an isomorphism .
Proof. Consider the -algebra . There exists a unique algebra morphism $f : A \to F(A) $. Then by the uniqueness provided by initiality of . Since is an -algebra map, we have $ f \circ \alpha = F(\alpha) \circ F(f) $ which by functoriality is equal to identity.
This theorem says that if has an initial algebra, then has a fixed point . As an example, consider the functor . Then we have
\mu F_A = F_A(\mu F_A) = \cdots = 1 + A \times (1 + A \times ( \dots )) = 1 + A + A^2 + A^3 + \cdotsThis is simply the free monoid on . We have some degree of choice when it comes to encoding the fixed point. Later we will find another fixed point, that does not necessarily coincide.
newtype Fix f = Fix (f (Fix f))
In code this looks like the following.
newtype Mu (f :: * -> *)
= Mu { unFix :: Functor f => f (Mu f) }
data FreeMonoidF a x
= Nil
| Cons a x
instance Functor (FreeMonoidF a) where ...
type FreeMonoid a = Mu (FreeMonoidF a)
-- exhibiting an isomorphism between [a] and FreeMonoid a
fromList :: [a] -> FreeMonoid a
fromList [] = Mu Nil
fromList (x:xs) = Mu (Cons x $ fromList xs)
toList :: FreeMonoidF a -> [a]
toList (Mu Nil) = []
toList (Mu (Cons x xs)) = x : (toList xs)
Given an algebra , there is a unique map from the initial algebra to .
That is, we can construct a morphism from a monoid on .
This is the well known mconcat
in haskell.
If we instead have the algebra then we can construct a morphism .
This is the slightly more general fold
.
There is an obvious generalization for an arbitrary functor , and we call that a catamorphism. The implementation can be easily read off of the diagram.
{-
F(A) <---- F(Mu(F))
| F(f) | ^
| | | unFix
V f V |
A <---- Mu(F)
-}
cata :: Functor f => Algebra f a -> (Mu f -> a)
cata alg = let f = runAlg alg . fmap f . unFix in f
Dually, …
Now we can slap a “co” in front of everything and turn around the arrows. An -coalgebra is an object of and a morphism .
newtype Coalgebra f a
= Coalgebra { runCoalg :: Functor f => f a -> a }
Lemma. If has a terminal coalgebra then witnesses an isomorphism .
The proof strategy is the same as before.
Proof. Consider the -coalgebra . There is a unique coalgebra map . Since is itself a coalgebra map, we have by terminality . Then .
If we return to our example of , then we see that given a coalgebra
we have a map .
This is unfold
in haskell.
Interestingly, while fold
has found its way to many other languages, often under the name reduce
, its dual, unfold
, is relatively obscure.
The natural generalization for an arbitrary functor is called an anamorphism. Again, the implementation can be read directly off of the diagram.
{-
A -----> Mu(F))
| f |
| |
V F(f) V
F(A) -----> F(Mu(F))
-}
ana :: Functor f => Coalgebra f a -> (a -> Mu f)
ana coalg = let f = Mu . fmap f . runCoalg coalg in f
A hylomorphism is an anamorphism followed by a catamorphism.
We might expect an implementation like hylo alg coalg = cata alg . ana coalg
,
but we can do better.
A hylomorphism builds up a big structure of values, then folds it back down.
The full intermediate structure is unnecessary, so we should be able to avoid building it up.
Once again, the implementation is the recursive equation read directly off of the diagram.
{- F(A) ---> F(muF) ---> F(B)
^ |
| |
| V
A ---> muF ---> B
\___________________/^
h -}
hylo :: Functor f => Algebra f b -> Coalgebra f a -> (a -> b)
hylo alg coalg = let h = runAlg alg . fmap h . runCoalg coalg in h
This sort of implementation is always charming to me. We do not need to think about what the function is doing, we simply write down the equation it satisfies, and it just works!