Recursion Schemes, Part V: Hello, Hylomorphisms
2018-04-17
Previous installments: 1, 2, 3, 4, 4½.
Thus far, we’ve explored a myriad array of recursion schemes. Catamorphisms and anamorphisms fold and unfold data structures, paramorphisms and apomorphisms fold with additional information, and histomorphisms and futumorphisms allow us to fold using historical values and unfold with user-defined control flow.
Given each fold—cata
, para
, histo
—we derived its corresponding unfold by ‘reversing the arrows’ of the fold—put another way, we computed the categorical dual of each fold operation. Given the fact that we can derive an unfold from a fold (and vice versa), and given the powerful tool in our toolbox that is function composition, an important question we can ask is “what happens when we compose an unfold with a fold?” In this entry, we’ll explore the structures generated from such compositions. (This post is literate Haskell; you can find the code here.)
Folds and Matter
Meijer et. al answered the above question in Bananas, Lenses, Envelopes, and Barbed Wire. They called this concept—unfolding a data structure from a seed value, then computing a final result by folding over the data structure thus produced—a hylomorphismIf you Google ‘hylomorphism’, the results will be almost exclusively concerned with Aristotle’s philosophical theory of the same name. Though Aristotle’s concept is not particularly relevant to the study of recursion schemes, we’ll discuss why this name is appropriate for the computation that our hylomorphism performs.
. The hylomorphism is sometimes referred to as a ‘refold’, which is a slightly more approachable but not particularly illustrative name—I prefer to think of a hylomorphism as a ‘producer-consumer function’, where the unfold produces values for the fold to consume.
If you grasp the concept of a catamorphism (a fold) and an anamorphism (an unfold), a <> is easy: it’s just the latter followed by the former. The definition follows:
hylo :: Functor f => Algebra f b -> Coalgebra f a -> a -> b
= ana coalg >>> cata alg hylo alg coalg
Pretty straightforward, right? Unfold with ana
and the provided coalgebra, then fold with cata
and the provided algebra.
The ‘hylo’ in ‘hylomorphism’ comes from the Greek hyle, ὕλη, meaning ‘matter’. The ancient Greeks used ‘matter’ to mean the substance out of which an object is formed (‘morpho‘); as such, we can read ‘hylomorphism’ as a function that forms a result object out of some intermediate, constituent matter.
Something interesting to note is that Term
, the fixed point of a Functor
, appears nowhere in the signature of hylo
. Though ana
produces and cata
consumes a Term f
, this is elided in the type signature, a hidden detail of the implemementation: all that is necessary is a Functor
instance to parameterize the algebra and coalgebra. (Of course, your input a
or your output b
could be a Term
over some Functor
. But it doesn’t have to be.) Similarly, Kmett’s formulation of hylo makes no Base
functor visible.
Highs, Lows, and hylo
The hylomorphism is more than an elegant result—it generalizes many computations that we as programmers encounter in our day-to-day work. The canonical example is the factorial function, but an abstraction that encapsulates building then collapsing a data structure is far more useful than yet another cute way to compute fac(n)
. Though we often don’t notice the underlying generality, we’re using hylomorphisms every time we:
aggregate and compute properties of data structures, e.g. determining the mean or median or outliers present in a set of numeric data;
interpret or compile a final result from some textual representation of a nested structure
apply recursive divide-and-conquer techniques, e.g. quicksort, mergesort, or the fast Fourier transform;
determine differences between data structures, e.g. edit distance or Levenshtein distance over strings.
Let’s put hylo
to work. We’ll build a RPN calculator with hylo
. Given the string + 1 2
, our calculator should compute 1 + 2
, and given 2 1 12 3 / - +
it should calculate (2 + 1) - (12 / 3)
: every RPN postfix expression has one unambiguous parse, obviating the need for parentheses associated with infix operators. Our coalgebra will unfold a list of operations from a seed (a string), producing a list of numbers and operators, and the algebra will consume the generated list, ultimately yielding a stack of numbers.
As I just mentioned, the input to an RPN calculator consists of two kinds of values: mathematical operations (addition, multiplication, &c.) and integer literals. We’ll define a Token
datatype upon which our calculator will operate:
data Token
= Lit Int
| Op (Int -> Int -> Int)
Note that our Op
constructor contains a binary function Int -> Int -> Int
, rather than a string representation of the relevant operation. While this precludes a Show
instance for Token
, since functions have no meaningful string representation at runtime, it will simplify the implementation: when we parse an Op
, we’ll store the Haskell function that corresponds to the operator, so that when we perform computations we need only call the stored function with the arguments present on the stack.
We need to be able to read a Token
out of a string. If we were more principled and honest people, we would use a parsing library like megaparsec
or trifecta
, or even a Maybe
monad to represent parse failures—but in an effort to keep things simple, let’s make this function pure using read
, which fails at runtime if someone decides to get saucy and provide invalid data.
parseToken :: String -> Token
"+" = Op (+)
parseToken "-" = Op (-)
parseToken "*" = Op (*)
parseToken "/" = Op div
parseToken = Lit $ read num parseToken num
Nothing too difficult here. We pattern-match on a given string; given a mathematical operator, we return an Op
containing the corresponding Haskell function; otherwise, we use read
to yield an Int
and wrap it in a Lit
.
The easiest way to represent a LIFO stack in Haskell is with a list: we push with a cons operator (:
) and pop by dropping the first item in the list (tail
). As such, we’ll need a Term
-compatible (parameterized) list type. Though last time we explored how the Base
type family allows us to use Haskell’s []
list type with recursion schemes, we’ll roll our own here.
data List a b
= Cons a b
| Nil
deriving (Show, Eq, Functor)
Now we have a Token
type to operate on and a List
type to store tokens. Our next objective is to define a coalgebra that builds a List
of Token=s from a =String
. Remember the definition of coalgebras from part II:
type Coalgebra f a = a -> f a
The seed value a
will be a String
, while the container type f
will be List Token
. We’ll write the type signature of our coalgebra now:
parseRPN :: Coalgebra (List Token) String
Keep in mind that List Token
here is partially-applied, as List
has three arguments, being of kind * -> * -> *
. If we were to expand the f a
, we would yield the type List Token String
:
parseRPN :: String -> List Token String
This makes sense. In each step of our unfold we return a List value containing a Token
value and the remaining String
that we have yet to parse, unless the result is Nil
, at which point we stop unfolding, yielding the list. Because Nil
contains no children of type a
or b
, an occurrence of Nil
can assume whatever type we need them to be—here Token
and String
.
Now let’s implement the body of rpn
. The simplest case handles the empty string: if there’s no more input to parse, we terminate the unfold by returning Nil
. (ana
knows to stop unfolding if it encounters Nil
because the recursive fmap
calls will cease: Nil
contains no child nodes into which to recurse.)
"" = Nil parseRPN
The case for a nonempty string is more interesting. Given a string str
, we take as many characters from it as we can, until we encounter a space. We then pass that chunk into parseToken
, sticking its result into the a
field of Cons
, then drop all spaces in the remainder of the string and stick it into the b
field of the Cons
. We’ll use Haskell’s span
function to do that, which takes a predicate and returns a tuple containing the items that satisfy the predicate and those that don’t.
= Cons token newSeed
parseRPN str where (x, rest) = span (not . isSpace) str
= parseToken x
token = dropWhile isSpace rest newSeed
Let’s look at the function all together:
parseRPN :: Coalgebra (List Token) String
"" = Nil
parseRPN = Cons token newSeed
parseRPN str where (x, rest) = span (not . isSpace) str
= parseToken x
token = dropWhile isSpace rest newSeed
Not too shabby! Six lines of code, two cases, no compiler warnings. (And this would be even cleaner if we used an actual parser.) If we run ana parseRPN
with 3 4 +
as a seed value, we yield a result equivalent to the list Lit 3, Lit 4, Op +, Nil
.
It’s time to write our evaluator. Let’s consult the definition of an Algebra
:
type Algebra f a = f a -> a
Our container type f
will be, as in parseRPN
, a List Token
. But our result type a
will differ: rather than operating on strings, we want a stack of integers to which we can append (with Lit
) and upon which we can operate (with Op
). Let’s make a type alias:
type Stack = [Int]
And now we can set down a type signature for our evaluator:
evalRPN :: Algebra (List Token) Stack
But here we encounter a dilemma! Given a reverse-Polish expression: 2 3 +
or 4 2 5 * + 1 3 2 * + /
, we need to compute the result left-to-right, pushing literals onto the stack and performing the operations we find on the values in the stack. This means our evaluator must work from the left (in the manner of foldl
) rather than from the right (a la foldr
). But our old friend cata
is a right fold—it travels all the way to the Nil
at the end of the list and then propagates its result from the right. How do we work around this, given that hylo
provides us no opportunity to reverse the parsed list of tokens (an admittedly kludgy fix)?
The answer is simple—our result type will not be an ordinary Stack
value. We will instead use a function that takes and returns a Stack
: Stack -> Stack
. The ultimate result of this catamorphism will be such a function—we kick off the computation by invoking it with an empty stack. Since the leftmost element was evaluated most recently, the aggregated function will operate on the leftmost element first. Further invocations will operate on each subsequent item, left-to-right, until we encounter the Nil
element and cease computation. And, conveniently, the value we provide to this function at the top-level will be the initial stack that this calculator uses.
If this is difficult to visualize, the following diagram may help:
The fact that we can transform cata
, a rightward fold, into a leftward fold by switching from computing a value to computing a function on values is utterly staggering to me. By adding the most fundamental concept in functional programming—a function—we yield additional power from cata
, the lowliest of recursion schemes. This strategy is a well-known idiom in the functional-programming world: this stack is an example of a ‘difference structure’, similar to the difference list that is used in Haskell’s Show
construct.
Let’s rewrite evalRPN
to use Stack -> Stack
as its carrier type:
evalRPN :: Algebra (List Token) (Stack -> Stack)
That looks right. Our algebra takes a list of tokens and returns a function that takes and returns a stack. When hylo
completes, we’ll yield such a function; the value that we provide to that function will be used as the initial stack. To check our assumptions, we can expand the definition of evalRPN:
evalRPN :: List Token (Stack -> Stack) -> (Stack -> Stack)
When folding over a list, we need to consider two cases: Nil
and Cons
. The Nil
case falls out quite easily: we simply return the identity function, as there is no data with which we would modify a passed-in stack.
Nil stack = stack -- aka `id` evalRPN
Though we are technically returning a function from evalRPN
, Haskell allows us to write this function in a more beautiful way: rather than returning an explicit function λstack -> stack
, we can move that stack
argument into the parameter list of evalRPN
Put another way, Haskell makes no syntactic distinction between the types a -> (b -> c)
and a -> b -> c
.
.
Now let’s handle the case of adding a new value onto the stack. Our Cons
constructor provides two values: a Lit
that contains an integer, and our accumulator/carrier type, a function from Stack
to Stack
. We’ll call that cont
, since we’ll continue evaluation by invoking it. (If, for some reason, we wanted to terminate early, we would return a function that did not invoke the provided continuation.) As such, evalRPN
will take a stack, push the integer from the Lit
value onto that stack, and invoke cont
to continue to the next stage:
Cons (Lit i) cont) stack = cont (i : stack) evalRPN (
The case of applying a function to the stack is similar, except we have to introspect the top two values so as to have some operands to the provided Op
. As such, we pattern-match on the Cons
structure over which we are folding in order to pop off its top two values. We then apply those operands to the function inside the Op
, append that value to the stack, and invoke cont
to proceed to the next stage.
Cons (Op fn) cont) (a:b:rest) = cont (fn b a : rest)
evalRPN (= error ("too few arguments on stack: " <> show stack) evalRPN _ stack
If there are too few values on the stack, we call error
to bail outWe could ensure that there are always sufficient values on our stack: if our calculator is initialized with an infinite list for a stack (such as [0, 0..]
, an infinite sequence of zeroes), we could omit the error case.
. After applying the function contained in the Op
value to these two values, we append the result of this function to the remainder of the list, then call the continuation to proceed to the next computational stage.
Let’s take a look at the evalRPN
function, assembled in one place:
evalRPN :: Algebra (List Token) (Stack -> Stack)
Nil stack = stack
evalRPN Cons (Lit i) cont) stack = cont (i : stack)
evalRPN (Cons (Op fn) cont) (a:b:rest) = cont (fn b a : rest)
evalRPN (= error ("too few arguments on stack: " <> show stack) evalRPN _ stack
I find this a lovely definition: it shows clearly that evaluation terminates in the Nil
case, and continues in the Cons
cases by virtue of invoking the carried cont
function.
Now we have a coalgebra (the parser) and an algebra (the evaluator, in continuation-passing style). Let’s put it all together—we can start by interrogating GHCi as to the type of passing these to hylo
.
> :t hylo evalRPN parseRPN λ
That makes sense: the String
parameter is our input, and the Stack
parameter is the initial value of the RPN machine’s stack. So now we can build a top-level rpn
function that takes a string, invoking the result of hylo
with the provided string and an empty initial stack:
rpn :: String -> Stack
= hylo evalRPN parseRPN s [] rpn s
We can test this by evaluating it in GHCi:
> rpn "15 7 1 1 + - / 3 * 2 1 1 + + -" λ
[5]
Dope.
Though an RPN calculator isn’t enormously complicated, I’d argue that our implementation demonstrates the virtue of recursion schemes: by separating what we’re doing from how we’re doing it, we draw attention to the meat of the problem—parsing from a string and operating on a stack—without concerning ourselves with the details of aggregating data from an input string or iterating over a parsed sequence of tokens. The machinery of unfolding and folding is all contained within hylo
: all we have to worry about is the core of our problem. And that’s pretty remarkable.
Further Efficiency
We don’t need to invoke cata and ana explicitly to build a hylomorphism. We can build hylo
just out of the algebra and coalgebra itself.
hylo' :: Functor f => Algebra f b -> Coalgebra f a -> a -> b
= coalg >>> fmap (hylo' alg coalg) >>> alg hylo' alg coalg
Though this definition is arguably less indicative of the fact that a hylomorphism is the composition of an an anamorphism and catamorphism, it bears a compelling property: it entails half as many calls to fmap
as does the previous definition.
Our original hylo
unfolded our List
to its maximum extent, entailing O(n) calls to fmap
, where n is the number of tokens passed to rpn
. Subsequently, that structure is torn down with cata
, using an additional O(n) calls to fmap. In contrast, this new definition of hylo
only recurses O(n) rather than O(2n) times: as soon as the unfolding completes and the recursive fmap
invocations bottom out, each level of the structure is passed directly to alg
as the stack unwinds. This is a significant optimization, especially for deeply-nested structures!
Time is Running Out (and In)
Though Meijer et al. introduced the hylomorphism along with the catamorphism and anamorphism, Uustalu and Vene’s paper does not mention what happens when you compose a histomorphism and futumorphism. It appears to have taken until roughly 2008 (nine whole years!), when Edward Kmett and the #haskell IRC channel dubbed it the chronomorphism—chrono (χρόνος) being the prefix related to time.
The definition of the chronomorphism follows from that of the hylomorphism:
chrono :: Functor f => CVAlgebra f b -> CVCoalgebra f a -> a -> b
= futu cvcoalg >>> histo cvalg chrono cvalg cvcoalg
Pretty straightforward: futu
unfolds a structure multiple layers at a time (thanks to the power of the free monad), and histo
tears it down.
Unfortunately, coming up with a useful example of chronomorphisms is a bit more difficult than that of a hylomorphism. The plant-growing example in part IV of this series comes close—we used a futumorphism to generate plant life, but only used a catamorphism, rather than a histomorphism, to render the resulting plant. We could have expressed that catamorphism as a histomorphism, as we showed when we implemented cata
in terms of histo
, but bringing in the power of histomorphisms and not using them is pretty pointless. I haven’t been able to find a useful or illustrative of chrono
in action (if you know of one, get in touch!) but I at least have the reassurance that its discoverer himself can’t think of one either. chrono
can, however, be used to implement the dynamorphism, a scheme specialized towards solving dynamic programming problems, which we will discuss in future installments. (It’s possible that Uustalu and Vene neglected to mention the chronomorphism for precisely this reason—it’s hard to find a truly compelling use case for it.)
Taking Shortcuts with Elgot (Co)Algebras
Histomorphisms are useful: building up and then collapsing some intermediate structure is a pattern worth abstracting, as separating ‘what’ from ‘how’ always gains us some degree of insight into our code. But in practice, this process of construction and destruction is often interrupted. Perhaps, during the construction of our intermediate structure, we determine that the input data violates our assumptions, requiring us to terminate the construction early; perhaps, during destruction, we enter an optimizable state that allows us to skip future destruction steps.
While we could use failure monads over hylo
to represent these patterns, a paper by Jiří Adámek, Stefan Milius, and Jiří Velebil, entitled Elgot Algebras, provides us with a category-theoretical treatment of this pattern, avoiding the hornet’s nest that is impurity. Named after Calvin Elgot, an American mathematician who worked for many decades in the intersection between software engineering and pure mathematics, Elgot algebras and coalgebras generalize hylomorphisms, catamorphisms, and apomorphisms in a manner both elegant and useful. The paper itself is extremely dense, but Kmett, as per usual, has done the community a great service in translating it to Haskell.
Let’s consider the type signature of a hylomorphism. Rather than just repeat our first type signature, let’s look at hylo
after we expand the Algebra
and Coalgebra
type synonyms.
hylo :: Functor f => (f b -> b) -> (a -> f a) -> a -> b
This tells us, given a F-coalgebra over a
and an F-algebra over b
, how to get from an a
to a b
. But what if we could take a shortcut? If, in our coalgebra (the ‘construction’ function), we could short-circuit the whole hylomorphism, returning a plain b
value, we could provide this refold function with an escape hatch—without having to worry about the semantics of Maybe
or Either
or Except
or whatever failure monad we would use with plain hylo
.
To allow this, our coalgebra, a -> f a
, has to be able to return one of two values—an f a
, which continues the unfold, or a b
, short-circuiting it. Haskell provides us a mechanism to return one of two values, of course: the trusty Either
type. Changing our coalgebra to return an Either
type yields us with the type signature for Elgot
, the Elgot algebra:
elgot :: Functor f => Algebra f b -> (a -> Either b (f a)) -> a -> b
We’ll use an auxiliary functions to define Elgot algebras: |||
(pronounced ‘fanin’). It is an infix form of the either
helper function: given two functions, one of type b -> a
and the other of type c -> a
, it creates a function that takes Either
a b
or a c
and returns an a
.
(|||) :: (b -> a) -> (c -> a) -> (Either b c -> a)
Reading |||
as ‘or’ (as you may have done already) can be a helpful mnemonic: we can see that f g
returns a function that uses f
or g
.
Defining elgot
follows straightforwardly from the above optimized definition of hylo
. We begin by invoking our coalgebra. If we get a Right
value out of it, we recurse into it, eventually passing this layer of the computation on to our coalgebra—in other words, it behaves like a normal call to hylo
. But if we get a Left
value, we just stop, performing no operation on the value contained therein.
elgot :: Functor f => Algebra f b -> (a -> Either b (f a)) -> a -> b
= coalg >>> (id ||| (fmap (elgot alg coalg) >>> alg)) elgot alg coalg
Let’s use an Elgot algebra to bring some sense of safety to our above RPN calculator. Calling error
on invalid input is a bad look: this is Haskell, and we can do better. Let’s start by writing a custom type to represent success and failure.
data Result
= Success Stack
| ParseError String
| TooFewArguments Stack
deriving (Eq, Show)
As with the previous incarnation of this function, we’ll use continuation-passing style, but instead of a function over Stack
values, our continuation will handle Result
values. We’re gonna be mentioning functions of type Result -> Result
a few times here, so we’ll make a type alias for it.
type Cont = Result -> Result
We’ll start by rewriting parseToken
. Rather than returning a plain Token
and failing at runtime if provided an invalid numeric value, we’ll use readMaybe
to yield a Maybe Int
, returning an Either
that contains an early-termination function over Result=s or an integer wrapped by a =Lit
constructor
safeToken :: String -> Either Cont Token
"+" = Right (Op (+))
safeToken "-" = Right (Op (-))
safeToken "*" = Right (Op (*))
safeToken "/" = Right (Op div)
safeToken = case readMaybe str of
safeToken str Just num -> Right (Lit num)
Nothing -> Left (const (ParseError str))
Similarly, we rewrite parseToken
to invoke safeToken
. The do
-notation provided by Haskell beautifies the nonempty-string case, binding a successful parse into the parsed
result when we encounter a Right
value and implicitly terminating should safeToken
return Left
, the failure case.
safeParse :: String -> Either Cont (List Token String)
"" = return Nil
safeParse = do
safeParse str let (x, rest) = span (not . isSpace) str
let newSeed = dropWhile isSpace rest
<- safeToken x
parsed return $ Cons parsed newSeed
To tie all this together, we rephrase the definition of safeEval
. We have to make our pattern-matching slightly more specific—we have to match on Success
values to get a stack out of them—but we can also remove the call to error
in this function. When handling an arithmetic operation, if there are too few values on the stack to proceed, we call the continuation with a TooFewArguments
value
safeEval :: Algebra (List Token) Cont
Cons (Lit i) cont) (Success stack) = cont (Success (i : stack))
safeEval (Cons (Op fn) cont) (Success s) = cont (case s of
safeEval (:b:rest) -> Success (fn b a : rest)
(a-> TooFewArguments s)
_ = result safeEval _ result
That’s two uses of error
removed—a victory in the struggle against partial functions and runtime crashes! Furthermore, the error handling becomes tacit in the definition of safeParse
: no case statements or calls to throw
are required, as the do-notation over Either
handles the Left
case for us.
Invoking these functions is just as simple as it was when we used hylo
. We replace hylo
with elgot
, and pass an empty Success
value to evaluate the continuation left-to-right over the provided string.
safeRPN :: String -> Result
= elgot safeEval safeParse s (Success []) safeRPN s
Other examples of using Elgot algebras are few and far between, but the excellent Vanessa McHale has an example of them on her blog, in which she uses them to calculate the Collatz sequence of a provided integer, yielding performance comparable to an imperative, lower-level Rust implementation.
Reversing the Arrows, Again
In the defintion of elgot
above, we used ORRRR
to handle the Either case: in performing id
(no operation) on a Left
value and recursing on a Right
value, we gained a clarity of definition—but more importantly, we make it easy to reverse the arrows. Every time we reverse the arrows on a fold, we yield the corresponding unfold: but here, reversing the arrows on an Elgot coalgebra, we yield a hylomorphism that can short-circuit during destruction, rather than construction.
We know how to reverse most of the operations in the above definition: alg
becomes coalg
and vice versa, >>>
becomes <<<
and vice versa, and id
stays the same, being its own dual. The ORRRR
may be slightly less obvious, but if we remember that the tuple value ((a, b)
) is dual to Either a b
, we yield the &&&
operator, pronounced ‘fanout’:
(&&&) :: (a -> b) -> (a -> c) -> (a -> (b, c))
Whereas ORRRR
took two functions and used one or either of them to deconstruct an Either
, &&&
takes two functions and uses both of them to construct a tuple: given one of type a -> b
and the other of type a -> c
, we can apply them both on a given a
to yield a tuple of type (b, c)
. Again, reading the triple-ampersand as ‘and’ can be a useful memonic: f &&& g
returns a function that uses both f
‘and’ g
.
Now we know how to reverse every operation in elgot
. Let’s do so:
= alg <<< (id &&& (fmap (coelgot alg coalg) <<< coalg)) coelgot alg coalg
Feeding this into GHCi and querying its type yields the following lovely signature:
coelgot :: Functor f => ((a, f b) -> b) -> (a -> f a) -> a -> b
Our algebra, which previously took an f b
, now takes a tuple—(a, f b)
. That a
is the same a
used to generate the f b
we are examining. The ‘shortcut’ behavior here is slightly more subtle than that present in the definition of elgot
—it depends on Haskell’s call-by-need semantics. If we never observe the second element of the tuple—the f b
—it will never be evaluated, and as such neither will any of the computations used to construct it!
By replacing a -> f a
with its natural type synonym, Coalgebra
, we yield a unified definition of coelgot
.
coelgot :: Functor f => ((a, f b) -> b) -> Coalgebra f a -> a -> b
= alg <<< (id &&& (fmap (coelgot alg coalg) <<< coalg)) coelgot alg coalg
We can think of Elgot algebras as a hylomorphism built out of an RCoalgebra
and an Algebra
. Dually, we can think of Elgot coalgebras as hylomorphisms built out of an RAlgebra
and a Coalgebra
. We can also build a more powerful hylomorphism out of both an RAlgebra
and RCoalgebra
:
hypo :: Functor f => RAlgebra f b -> RCoalgebra f a -> a -> b
= apo rcoalg >>> para ralg hypo ralg rcoalg
As far as I can tell, this construction (though it is not particularly groundbreaking) hasn’t been named in the literature before—if you know its name, drop me a line. I referred to it as an “R-hylomorphism”, but I like Rob Rix’s term for it, the “hypomorphism”, a clever amalgam of its component parts. I leave the deforestation stage, analogous to hylo
, as an exercise for the reader.
Acknowledgments
As always, I would like to thank Manuel Chakravarty for his patience and kindness in checking this series for accurately. Colin Barrett, Ross Angle, and Scott Vokes also provided valuable feedback.
I remain very grateful your readership. The next entry will discuss the fusion laws that folds, unfolds, and refolds all possess, and how we can use these laws to make real-world use of these constructs extremely fast.