Recursion Schemes, Part VI: Comonads, Composition, and Generality
2019-02-28
Previous installments: 1, 2, 3, 4, 4½, 5.
Now that we’ve covered folds (cata
, para
, and histo
), unfolds (ana
, apo
, and futu
), and refolds (hylo
, hypo
, elgot
, and chrono
), I hope I have showed that recursion schemes are a useful tool to organize programs, beautify code, and clarify human intent. The above schemes provide sufficient bells and whistles so as to be an essential part of a functional programmer’s toolbox.
Yet if we allow ourselves a greater degree of generality in the definition and construction of these recursion schemes, we unlock an underlying similarity between all these definitions. This underlying similarity was outlined by Tarmo Uustalu, Varmo VeneYou might remember Uustalu and Vene from Primitive(Co)Recursion and Course-of-Value (Co)Iteration, Categorically, which introduced the histomorphism and futumorphism (as covered in the third part of this series).
, and Alberto Pardo in their 2001 paper Recursion Schemes from Comonads. As is often the case with papers describing recursion schemes, this work is dense and somewhat unapproachable. Initially I deemed Uustalu’s exploration of these similarities elegant but inessential: it was my intention to discuss these results on the merits of their beauty rather than their applicability to real-world situations. However, a close read of Edward Kmett’s recursion-schemes
library revealed an implementation of the core ideas behind Recursion Schemes from Comonads, and some experimentation revealed to me how and why this paper is useful.
The essence of Haskell is composition. Functions, folds, lenses, algebras—composition is the melody with which all the disparate elements of Haskell are in tune. Why, then, can I not compose recursion schemes? For example, I have para
, which lets me look at the original values alongside the fold’s values in-flight, and histo
, which provides a lookup table for previously calculated compositions, but what if I need both these properties to define a given fold? Or what if I want to create a new hylo
-esque combinator out of the composition of a histomorphism and an apomorphism?
Thanks to Uustalu et al.’s ideas, our toolbox of recursion schemes becomes something more: rather than limiting us to the dozen or so predefined recursion schemes, we can build combinators capable of combining and composing existing schemes into new schemes, building a solution tailor-made to the needs of the situation. This result is more than a beautiful implementation: it unifies a set of related but disparate tools into a single cohesive abstraction. I hope in this article to outline these ideas, though the density of the paper itself means I won’t be stepping closely through it.
This article uses the Base
-functor formulation of recursion schemes, as described in Kmett’s library, rather than the ad-hoc library we’ve designed in previous installments of this series. If you want an overview of why this formulation is useful, my prior post on the matter may be helpful.
Lurking Similarities
cata :: (Base t a -> a) -> t -> a
para :: (Base t (t, a) -> a) -> t -> a
histo :: (Base t (Cofree (Base t) a) -> a) -> t -> a
These are the type signatures of cata
, para
, and histo
. Each differs from the other in one place: the last parameter to the type Base t
, their Base
-algebras. For cata
, the carrier is a plain old a
value, corresponding to previous results (if any) of the stages of the fold. For para
, it’s (t, a)
, because at each stage of a paramorphism the original t
value is visible alongside the in-flight a
. And for histo
, the carrier type is Cofree (Base t) a
, representing an adaptive, ‘shapeshifting’ cache structure around the original t
, storing all previously-computed a
values.
At first glance, these three values don’t seem to share any essential similarity. But we can change that, by applying the Identity
functor.
newtype Identity a = Identity { runIdentity :: a }
A value of type Identity a
just wraps a single a
. Though it doesn’t appear to be particularly useful, since you can substitute an a
for any Identity
without having to construct or destructure an Identity
value, its use lies in the set of interfaces to which it conforms. In this case, rather than using a bare a
as the carrier type for cata
, we can use Identity
, because the original a
is isomorphic to an Identity a
value.
cata :: (Base t (Identity a) -> a) -> t -> a
para :: (Base t (t, a) -> a) -> t -> a
histo :: (Base t (Cofree (Base t) a) -> a) -> t -> a
Now we have three type signatures, each bearing a different carrier type: Identity
for cata
, the tuple (t,)
for para
, and Cofree (Base t)
for histo
. Each of these types represents a container or context that holds further a
values, which we unify at each step of the fold into a single a
value that is then passed to further iterations of the fold.
There is a deep similarity between these three types, though it may not appear so at first glance: they all implement the Comonad
typeclass.
Comonads for the Impatient
The theory behind comonads is rich and deep, and I have neither room nor time to give a complete outline thereof. I recommend reading Bartosz Milewski’s excellent and rigorous overview of how to derive and define comonadic structures, as well as Gabriella Gonzales’s application of comonadic abstractions to real-world problems. For our purposes, it suffices to define the Comonad
typeclass and outline its interface.
class Functor w => Comonad w where
extract :: w a -> a
duplicate :: w a -> w (w a)
This is an interface with two functions:
extract
, which, given some data typew
Most documentation usesw
to represent types that implementComonad
, probably becausec
is often used in bindings of values, and becausew
looks like a flipped—that is to say, arrow-reversed—version ofm
, which is used forMonad
type variables.
wrapping ana
value, pulls out and returns thata
. This is dual toMonad
’sreturn
, which stuffs ana
inside a monadic type.duplicate
, which, given aw a
, wraps it in a furtherw
-container, returning aw (w a)
. This is dual to Monad’sjoin
.
Let’s implement Comonad
for the data types we’ve discussed thus far.
instance Comonad Identity where
Identity a) = a
extract (= Identity i duplicate i
The instance for Identity
is pretty straightforward: in extract
, we pattern-match on the Identity
to reveal its contents, and to duplicate
, we just apply the Identity
constructor again, yielding an Identity (Identity a)
.
instance Comonad ((,) a) where
= b
extract (_, b) = (a, (a, b)) duplicate (a, b)
For tuples ((,)
), extract
targets the second element of the tuple. Correspondingly, duplicate
replaces the second element of the provided tuple with a copy of itself, yielding a tuple containing a tuple.
instance Functor f => Comonad (Cofree f) where
:< _) = a
extract (a :< f) = (a :< f) :< fmap duplicate f duplicate (a
Because Cofree
can be seen as a recursive tuple type, the Comonad
instance for Cofree
is spiritually similar. Given some Cofree f a
value, extract
instance yields the a
contained therein, while duplicate w
creates a new Cofree
datum with w
as its annotation, keeping the recursive f
as its self-recursive type but mapping duplicate
therein to ensure that all recursive elements are properly duplicated.
Comonads, Transform and Roll Out
One of the primary idioms for constructing Haskell programs is monad transformers: the programmer, faced with some sort of program and its requirements, maps each of these requirements to some computational context—the State
transformer for programs that need stateful values, the Reader
transformer for those requring an immutable environment, or the LogicT
transformer for backtracking-amenable logic programming. The programmer then builds their own Monad
by composing each of these elements, for example:
newtype MyProgram = MyProgram { run :: StateT MyState (ReaderT MyEnv (LogicT Identity)) a }
deriving (MonadState MyState, MonadReader MyEnv)
By defining this monad, this programmer has built a mini-DSL for describing the capabilities of our program. The MonadState
and MonadReader
interfaces provide her with built-in functionality for accessing the program’s environment and modifying it’s stateful parameter. We call these contexts—this StateT s
, this ReaderT e
, this LogicT
—monad transformers, because each allows us to transform some other monad. Think of it like some layered hard candy: each layer provides a different capability/flavor, down to the “core” monad/chewy center on which all previous layers are built. (This “core” monad is almost always either Identity
or IO
, to represent pure and impure computations respectively.)
Because transformers exist for monads and comonads are dual to monads, we can postulate that that there exist transformers for comonads. And indeed, comonad transformers are a well-established construct, though less well-known than their monadic duals. Let’s take a look at one of the simpler such transformers, the Env
comonad transformer.
data EnvT env w a = EnvT env (w a)
deriving Functor
Given an environment type env
, an inner Comonad
w
, and a wrapped type a
, the EnvT
transformer builds a new comonad with all the capabilities of that w
, but that has the added capability of consulting an environment of type env
. If this looks like the tuple type ,
to you, you’re right: you can see EnvT
as a tuple type specifically constructed to hold comonadsIn other words, EnvT env w a
is representationally equal to (env, w a)
, and EnvT env Identity a
is isomorphic to (env, a)
. We can also see EnvT
as dual in nature to ReaderT
: it binds a formerly-free variable on which some further computation depends.
. To provide a measure of harmony between EnvT
and (,)
, we can define a ComonadEnv
typeclass:
class Comonad w => ComonadEnv e w | w -> e where
ask :: w a -> e
This interface provides us with a generalized ask
function capable of extracting an environment (e
) from any comonad supporting the notion of environments.
instance ComonadEnv e (EnvT e w) where
EnvT e _) = e ask (
This instance for EnvT
is pretty trivial: we just return the env
parameter. (We don’t use extract
, since that would target the a
, rather than the env
, of some EnvT env w a
.)
instance ComonadEnv e ((,) e) where
= fst ask
Similarly, we can define ask
over tuples, where ask
retrieves the first element of the tuple. Though this may seem like a lot of typing for little benefit, the ComonadEnv
typeclass provides us with a generalized interface to the environment parameter associated with any environmental Comonad
. Thus, when we build new Comonad
types out of these comonad transformers, we can always use ask
to extract the environment, saving us from having to memorize an extraction function for each comonad. We’ll use this later to provide a fluent interface over the comonads we construct with transformers.
class Comonad w => ComonadCofree f w | w -> f where
unwrap :: w a -> f (w a)
instance ComonadCofree f (Cofree f) where
:< r) = r
unwrap (_
Similarly, the ComonadCofree
interface provides an abstraction over the Cofree
comonad: the unwrap
function, given a Cofree f a
extracts the self-similar recursive entity, of type f (Cofree f a)
. If we build a new comonad with Cofree
at its core, we can implement the ComonadCofree
interface and use the same unwrap
function for both Cofree
and our new comonad. Indeed, let’s do that.
A Comonad of One’s Own
Let’s define a comonad that combines the Env
comonad and the Cofree
comonad: this resulting entity will have both access to an environment and to a contained self-similar recursive entity.
newtype Ledger t f a = Ledger { getLedger :: EnvT t (Cofree f) a } deriving Functor
We’ll call it Ledger
, as this data structure is capable of recording past computations (Cofree f
), along with the environment provided (EnvT t
) to each computation, much as an accountant’s ledger can record past transactions and the information associated therewith. The getLedger
record selector allows us to turn a Ledger t f a
back into an equivalent EnvT
over Cofree
; we’ll use this in definitions of Comonad
typeclasses.
instance Functor f => Comonad (Ledger t f) where
= getLedger >>> extract -- delegate to EnvT's extract
extract @(Ledger w) = Ledger (l <$ w) -- add a new Ledger layer to the input duplicate l
Due to a limitation of GHC, we can’t automatically derive an instance of Comonad
for Ledger
, but it’s not too painful to do so by hand. Similarly, we can write instances for ComonadEnv
and ComonadCofree
. (I’ve annotated these instances with their type signatures, thanks to GHC’s InstanceSigs
extension, for the sake of clarity.)
instance Functor f => ComonadEnv t (Ledger t f) where
ask :: Ledger t f a -> t
= getLedger >>> ask -- delegate to EnvT, again
ask
instance Functor f => ComonadCofree f (Ledger t f) where
unwrap :: Ledger t f a -> f (Ledger t f a)
= getLedger >>> unwrap >>> fmap Ledger -- delegate to EnvT+Cofree's unwrap unwrap
Now that we have this comonad, we can pose a question: what kind of fold would a Ledger t f
generate? Since the core of this comonad is Cofree
, it would presumably be like histo
—that is, capable of consulting a record of previously-computed f
-results—with behavior similar to that of para
, providing access to the original, unprocessed t
-values from the beginning of each stage of the fold. We could refer to this scheme as a histoparamorphism, or perhaps a parahistomorphism.
Our first instinct might be to sit down and manually derive a definition of this recursion scheme, like we did for cata
, para
, and histo
. But we don’t have to! The contribution of Recursion Schemes from Comonads is that there exists a generalized catamorphism capable of deriving a recursion scheme for any Comonad
, as long as we provide a function called a distributive law, that describes how operations percolate through and transform a given comonad. This means we never have to write our own recursion schemes: we can lean on the generalized catamorphism, gcata
. This provides us a composable, plug-and-play interface to recursion schemes. No longer are we limited to these three built-in combinators—instead, we can build our own, out of compositional, reusable parts, without the repetitive and error-prone process of deriving a recursion scheme for every task.
To do this, and to understand how it works, we’ll need to look at how these distributive laws and this generalized catamorphism are implemented.
The Means of Distribution
To understand the nature of distributive laws, it helps to examine the ones provided to us by the recursion-schemes
package. Let’s start with the simplest such law, the law for Identity
, out of which the gcata
function (which we will soon define) yields the catamorphism. (Distributive laws in recursion-schemes
are generally named distFoo
, where Foo
is replaced by the name of the recursion scheme to which this law gives rise.)
distCata :: Functor f => f (Identity a) -> Identity (f a)
= Identity (fmap runIdentity f) distCata f
This law states that we can, given an f
wrapping an Identity
, turn it into an Identity
wrapping an f
. In other words, we’re distributingYou might remember the term ‘distributive law’ from elementary algebra: we say that multiplication distributes over addition, in that 5 * (4 + 3)
is equivalent to (5 * 4) + (5 * 3)
. Given a multiplication operation over an addition, we can distribute that multiplication inside the components of that addition, in essence converting from a product of sums to a sum of products.
occurrences of f
from outside an Identity
comonad to inside. We can look at the distributive law for Cofree
, distHisto
, and see that it has a similar shape.
distHisto :: Functor f => f (Cofree f a) -> Cofree f (f a)
= fmap extract f :< fmap unwrap f distHisto
Just as distCata
moved an f (Identity a)
inside an Identity
, distHisto
moves an f (Cofree f a)
inside a Cofree
. And a corresponding distributive law for para
existsIn the name of didacticism, I fibbed a little on this definition: in recursion-schemes
, this law is expressed not with a plain Functor f
, but with a Base t
functor, due to implementation details (distPara
is actually implemented with distZygo
, the distributive law for the zygomorphism, which we won’t cover in this post.)
:
distPara :: Comonad f => f (t, a) -> (t, f a)
= (fst (extract f), fmap snd f) distPara f
Please note that these implementations differ from those in the recursion-schemes
library, which uses even-more-general combinators.
distLedger :: Comonad f => f (Ledger t f a) -> Ledger t f (f a)
Working of the examples provided by the prior distributive laws, we can postulate that a distributive law for Ledger
would look something like the above. And indeed, with a little elbow grease we can write a law ourselves that fits into this pattern:
= Ledger (EnvT environ cofree) where
distLedger f = ask (extract f)
environ = fmap extract f :< fmap distInnards f
cofree Ledger (EnvT _ (x :< y))) = distHisto y distInnards (
There is something immediately off-putting about this declaration: it’s complicated. Firstly, it relies on a Comonad
instance, whereas previous distributive laws were able to get away with just a Functor
. And secondly, it requires a good deal of pattern-matching to successfully zero in on the Cofree
structure over which we need to distribute. This means that as we adjust the definition of Ledger
, we’ll need to manually fix this pattern-matching code, which is no fun at all. But there is hope: recursion-schemes
contain combinators that automatically derive distributive laws for us. But to see this in action, we need to stop beating around the proverbial bush and take a look at the definition of the generalized catamorphism.
Glorious gcata
In the recursion-schemes
package, we find the generalized catamorphism gcata
defined thus:
gcata :: (Recursive t, Comonad w)
=> (forall b. Base t (w b) -> w (Base t b)) -- ^ a distributive law
-> (Base t (w a) -> a) -- ^ a (Base t)-w-algebra
-> t -- ^ fixed point
-> a
This is… well, it’s a lot to take in. I’ve omittedIf you’re really curious, it is mostly identical to the definition of cata
, except at each stage of the fold, after recursing into the subterms, we call duplicate
on the Comonad
inside the functor through which we are recursing. The distributive law transforms that functor-of-comonads into a comonad-of-functors, which is then destructed with extract
and sent through one last pass of the provided W-algebra to yield a result type. The distributive law describes how the fold is propagated through the given comonad, and the Comonad
typeclass gives us the vocabulary to construct and remove the extra scaffolding upon which the distributive law depends.
the implementation, as it is somewhat dense, but we can start understanding gcata
through its type signature. Let’s take a look at the first parameter, described in the documentation as the distributive law:
forall b . Base t (w b) -> w (Base t b)
If we recall part 4.5 of this series, we’ll remember that recursion-schemes
provides the Base
type family. Given some data type t
, Base t
is the parameterized version of t
, adding an extra type variable and replacing recursive occurrences of t
with this variable. We can mentally substitute f
for this Base t
, which yields something much more like the distributive laws we covered earlier:
forall b . f (w b) -> w (f b)
This is congruent with our earlier examples: given some comonad w
and a Base
functor f
, this distributive law describes how a f
containing w
values can be turned into a w
containing f
values. And in the subsequent parameter to gcata
, we can substitute f
for Base t
yet again:
-> a) (f (w a)
This looks a lot like our definition of Algebra
, which was f a -> a
. Yet instead of a functor wrapping just a
values, this functor wraps w a
values. We’ll call this a w
-algebra. Stripped of recursion-schemes
’s machinery for Base
(which, though it provides a significant measure of real-world convenience, can clutter up definitions such as these), and by defining type synonyms for distributive laws and w
algebras, we can take a look at the essence of gcata
:
type WDistLaw f w = forall b . f (w b) -> w (f b)
type WAlgebra f w a = f (w a) -> a
gcata :: (Functor f, Comonad w)
=> WDistLaw f w -- ^ a distributive law
-> WAlgebra f w a -- ^ a w-algebra returning 'a'
-> Term f
-> a
This is pretty remarkable. Simply by specifying a Comonad
and providing a distributive law for it, gcata
becomes capable of doing the job of cata
, para
, and histo
, all stemming from a single definition. All you need to do is provide the required distributive law. We can take a look at the type signatures that occur when we feed gcata
one of the dist
-family of distributive laws.
> :t gcata distCata λ
gcata distCata
:: Recursive t => (Base t (Identity a) -> a) -> Term f -> a
That looks identical to the Identity
-based cata
that we derived above! Let’s throw a type synonym in here, as we did for the original formulation of cata
, representing the w-algebra
type WAlgebra t w a = Base t (w a) -> a
cata :: Recursive t => WAlgebra t Identity a -> t -> a
= gcata distCata cata
Aside from the Identity
comonad, this definition is identical to the standard formulation of cata
. In addition, we can define para
and histo
with gcata
.
para :: (Recursive t, Corecursive t) => WAlgebra t ((,) t) a -> t -> a
= gcata distPara
para
histo :: (Recursive t) => WAlgebra t (Cofree (Base t)) a -> t -> a
= gcata distHisto histo
So what happens if we plug in our distLedger
function into gcata
?
> :t gcata distLedger λ
gcata distLedger
:: (Recursive t, Comonad (Base t)) => (Base t (Ledger t (Base t) a) -> a) -> t -> a
That’s almost correct—we have a Ledger
-based W-algebra as the first parameter—but a look at the type constraints shows that this definition is slightly wrong. Restricting this function to types that provide an instance of Comonad
for their Base
functor is much too restrictive, given that most Base
functors don’t admit a definition of Comonad
. Our error lies in the fact that our distLedger
function used comonadic extract
to extract the environment from a f (Ledger env f a)
. However, if we’re dealing with Base
functors, we can use the Corecursive
typeclass, which provides an embed
that serves, in this case, the purposes of extract
, without any Comonad
constraint. We’ll use an equality constraint \~
(provided by the TypeFamilies
extension) to specify that f
is, in this case, equivalent to Base t
, to keep clutter out of the right-hand-side of the definition
distLedger' :: (Corecursive t, f ~ Base t) => f (Ledger t f a) -> Ledger t f (f a)
= Ledger (EnvT environ cofree) where
distLedger' f = embed (fmap ask f)
environ = fmap extract f :< fmap distInnards f
cofree Ledger (EnvT _ (x :< y))) = distHisto y distInnards (
Now we can build a Ledger
-powered recursion scheme, without requring any errant Comonad
constraints in its signature.
histoPara :: (Recursive t, Corecursive t) => WAlgebra t (Ledger t (Base t)) a -> t -> a
= gcata distLedger' histoPara
Yet we are still left with the problem that haunted us in the previous section: distLedger'
is brittle and difficult to understand. To solve this, recursion-schemes
has one last trick up its abstraction-drunk sleeve.
Zero-Effort Distributive Laws
Inside the guts of Data.Functor.Foldable
, there lurks a nasty-looking function called distParaT
:
distParaT :: (Corecursive t, Comonad w)
-> (forall b. Base t (w b) -> w (Base t b))
-> Base t (EnvT t w a)
-> EnvT t w (Base t a)
As with so much in recursion-schemes
, it’s not clear at first what this function does, given its lack of documentation. But if we start adding parentheses in the signature, something jumps out at us:
distParaT :: (Corecursive t, Comonad w)
-> (forall b. Base t (w b) -> w (Base t b))
-> (Base t (EnvT t w a) -> EnvT t w (Base t a))
Adding parentheses, and thus making the currying explicit, we see that distParaT
both takes and returns a distributive law. The first parameter is a distributive law describing how a comonad w
distributes over a Base
functor: we’ve seen this before, in the first argument to gcata
. The return type is much more interesting: given some distributive law, distParaT
builds another distributive law, wrapping the comonad w
in an EnvT
and distributing appropriately. In this sense, we can think of distParaT
as a distributive-law-transformer: given some ‘base’ distributive law over w
, distParaT
gives us a distributive law over EnvT env w
. That’s pretty remarkable!
To verify this, we can yield distPara
by passing in distCata
:
> :t distParaT distCata λ
distParaT distCata
:: (Corecursive t) => Base t (EnvT t Identity a) -> EnvT t Identity (Base t a)
As we discussed, EnvT
is a comonadic take on the tuple type (,)
. As such, if we mentally substitute (,)
for EnvT
, we yield a definition equivalent to distPara
! (We have to use EnvT
here rather than plain old (,)
because we are dealing in comonad transformers: there exists no TupleT
transformer, since it would be the same as EnvT
).
We’ve established that our Ledger
comonad is the composition of the Env
and Cofree
comonads, the former atop the latter. Since distParaT
transforms distributive laws into EnvT
-compatible laws, and we already have a distributive law for Cofree
, distHisto
. What happens if we pass distHisto
to distParaT
?
> :t distParaT distHisto λ
distParaT distHisto
:: Corecursive t
=> Base t (EnvT t (Cofree (Base t)) a)
-> EnvT t (Cofree (Base t)) (Base t a)
This yields us something almost identical to Ledger
—remember that Ledger t f a
wraps a EnvT t (Cofree f) a
. Now we can, with some invocations of the Ledger
constructor and getLedger
destructor, write distLedger
without a single pattern-matchGiven that the result of distParaT distHisto
is isomorphic to Ledger t (Base t) a
, we ought to be able to apply Data.Coerce.coerce
to it and have the Ledger
and getLedger
constructors and eliminators applied for us; GHC, however, can’t yet prove that this is a well-founded coercion. We have at least the small consolation that GHC will optimize away the overhead of wrapping and unwrapping Ledger
values.
:
distLedger'' :: Corecursive t => Base t (Ledger t (Base t) a) -> Ledger t (Base t) (Base t a)
= fmap getLedger >>> distParaT distHisto >>> Ledger distLedger''
The upshot of all of this is that, thanks to the generality of gcata
, you can combine arbitrary capabilities, from any type of fold, into a bespoke fold that exactly fits the problem at hand. Furthermore, you never need to write a distributive law by hand: the distributive-law-transformers like distParaT
and its siblings distGHisto
It’s unclear to me why this is called distGHisto
rather than distHistoT
; drop me a line should you know.
and distZygoT
make it straightforward to derive, given a comonad built of comonad transformers, a well-typed distributive law. Every recursion scheme is, under the hood, wrought of the same material. There is a underlying order and rhythm shared between all recursion schemes, that, in my view, elevates this approach from ‘hey, this is cool’ to something that shows us deep and fundamental aspects of the nature of recursive computations. Just as the integral and differential calculus allowed Newton and Leibniz to unify the treatment of curves, motion, and infinitesimals, Recursion Schemes from Comonads allows us to unify folds, dynamic programming, and mutually-recursive computations (zygo
). There is a beauty to this treatment of recursion that is symphonic in its harmony.
Reversing the Arrows, One Last Time
gcata
is not the only generalized recursion scheme. There exists its categorical dual, gana
, the generalized anamorphism, an unfold operation derived by reversing the arrows in gcata
.
gana :: (Corecursive t, Monad m)
=> (forall b. m (Base t b) -> Base t (m b)) -- ^ a distributive law
-> (a -> Base t (m a)) -- ^ a Base-t-m coalgebra
-> a -- ^ a seed
-> t
Note that where gcata
entailed a constraint of kind (Recursive t, Comonad w)
, gana
takes the dual of both these typeclasses: Recursive
becomes Corecursive
and Comonad
becomes Monad
. In addition, we reversed the arrows within the distributive law: whereas the distributive laws for folds turned functors wrapping comonads into comonads wrapping functors, the distributive laws for unfolds turn monads wrapping functors into functors wrapping monads.
distAna :: Functor f => Identity (f a) -> f (Identity a)
distApo :: Recursive t => Either t (Base t a) -> Base t (Either t a)
distFutu :: Functor f => Free f (f a) -> f (Free f a)
The distributive law for ana
is almost identical to that for cata
, since Identity
is dual to itself. By contrast, the distributive law for apo
(the apomorphism, dual to the paramorphism) must deal with Either
values, since the dual of (a, b)
is Either a b
. Similarly, distFutu
deals in Free
, dual to distHisto
’s use of Cofree
recursion-schemes
also provides transformers capable of building distributive laws for monads, so you need not write them by hand.
Similarly, there exists a ghylo
combinator that generalizes hylo
, the refold. hylo
was already powerful and general, given the set of problems to which it is amenable, but ghylo
takes it even farther: you can build a refold out of futu
and apo
, or ana
and histo
, or any other combination of the comonadic recursion schemes.
Au Revoir, Recursion Schemes
As always, I would like to thank Manuel Chakaravarty for checking this series for accuracy. He has done me an extraordinary kindness in lending his time and attention to this series, and it is infinitely better for it. I also need to thank Colin Barrett for his support and insight, and Rob Rix for the motivation and kindness he continually shows me. Ed Kmett, and all the recursion-schemes
contributors, also deserve many thanks for creating and maintaining such a superlative and essential library. I would also like to thank everyone who read these monographs, especially those who found errors therein: getting this stuff right is hard, and I appreciate your patience in the face of the bugs that have crept in.
There are many more recursion schemes I am leaving undiscussed: zygomorphisms, mutumorphisms, Fokkinga’s prepromorphisms and postpromorphisms, Mendler-style catamorphisms and anamorphisms, Vanessa McHale’s entangled morphisms (dendro-, scolio-, and chema-), and the nigh-legendary zygohistomorphic prepromorphism. But at this point, after five years spent thinking and writing about recursion schemes, I’ve decided to put an end to this blog series. I hope, in the future, to have the time to work on a larger, more definitive reference work, covering all the known recursion schemes as well as the topics I had to gloss over. Until then, I’m looking forward to writing about something else; if you’ve read all of what I had to write here, I truly appreciate it. The response to this series has been deeply fulfilling.
Thank you!