Programming with bananas and barbed wire. Part 1

This post is the first in a two-part series on recursion schemes. Here, I want to build intuiton and understanding of what recursion schemes are and how to write them yourself. In the second part we will explore Ed Kmett’s recursion-schemes library, to show you, how to use recursion schemes in real-life Haskell code. Examples in the following text are intentionally kept as simple as possible, to aid in understanding of underlying ideas more than the examples themselves. Let us jump right in.

Expressing recursion

We usually write recursive data types like

data Expr = Const Int | Add Expr Expr | Mul Expr Expr

This is a common way of writing a syntax tree, but it is not without downsides. Consider the following function that pretty-prints a tree made from this datatype.

print :: Expr -> String print ( Const i) = show i print ( Add a b) = print a ++ " + " ++ print b print ( Mul a b) = print a ++ " * " ++ print b

This is the usual way of defining recursion we are used to everywhere — we define it explicitly. What if instead of pretty printing the syntax tree we wanted to calculate a numeric value of an operation it describes? We would have to write a function just like print , manually recursing into individual elements. While there is a little chance of making an error in this simple example, mistakes while writing recursive functions are not unheard of. There is also a possible performance penalty, as GHC is way better in optimizing non-recursive then recursive code.

Can we do better? Possibly. Consider the following data type, where we are using a type parameter for encoding recursion.

data ExprF a = Const Int | Add a a | Mul a a deriving ( Show , Eq )

The idea here is that we will use something called a ‘fixed point’ of a functor to encode recursive behavior inside a somehow. Before we get to that, let us prove our type is, indeed, a functor. To do that, we have to implement fmap that abides by the functor laws. I propose the following fmap

fmap f ( Const i) = Const i fmap f ( Add a a) = Add (f a) (f a) fmap f ( Mul a a) = Mul (f a) (f a)

So by applying f with fmap we unpack a level out of a functor, apart from the Const case, where we return the thing itself. The reason for passing i through is that fmap has type a -> b , but the type of i is always Int , so we cannot match the types in general.

Our good old functor laws are

fmap id == id fmap (f . g) == fmap f . fmap g

To check that the first law is fulfilled, insert id for the function f and the proof will flow from definition above. The second case for

Add will look as follows. It is the same for Mul , since they have the same structure.

Add (f . g $ a) (f . g $ b) = fmap f ( Add (g a) (g b)) = fmap f . fmap g $ Add a b

so it is fulfilled as well. Of course, we can skip this bit in GHC thanks to DeriveFunctor extension that allows us to derive fmap automatically. We will reference this implementation later though, so it’s best if it is written down explicitly.

Representing our type as a fixed point

We have a type ExprF a , which we know is a functor. This data type looks a bit useless, given that the type itself depends on the amount of levels of nesting

> :t Const 2 Const 2 :: ExprF a > :t Add (Const 1) (Const 2) Add (Const 1) (Const 2) :: ExprF (ExprF a)

And so on. Even worse, the type demands that all branches have the same depth, so it looks effectively unusable. But I’ve mentioned fixed point as something that can help encoding arbitrary recursive behaviour in a , what is it?

data Fix f = Fix (f ( Fix f)) --or data Fix f = Fix { outF :: f ( Fix f)}

In mathematics, a fixed point is an argument of a function that is mapped to itself by that function. If we expand one layer from Fix f , the inner application, we get

Fix (f ( Fix f)) = Fix (f ( Fix (f ( Fix f))))

and we can do it forever. So Fix f is an infinite chain of Fix applications, and infinite chains do not care about one more application. If we apply Fix f to Fix f , we get Fix f again and that is what is meant by a fixed point in this context.

Let us define a simple syntax tree in terms of the initial Expr

Mul ( Add (( Const 2 ) ( Const 2 ))) ( Const 2 )

We should be able to represent the same thing with our ExprF a . Let us start with the innermost Const

Fix $ Const 2

Checking the types checks out

> :t Fix $ Const 2 Fix $ Const 2 :: Fix ExprF

We do not have to write an infinite chain because Const does not have any a s in its type definition. To get the whole thing, we have to wrap inside Fix on each level

let fixedExpr = Fix ( Mul ( Fix ( Add ( Fix $ Const 2 ) ( Fix $ Const 2 ))) ( Fix $ Const 2 ))

The types check out again. I bet this was easier than it looked like at the beginning of this story.

Algebras

An algebra is an ‘unwrapping’ function.

type Algebra f a = f a -> a

So for our ExprF type, applying it to String will give us an algebra ExprF String -> String

printAlg :: ExprF String -> String printAlg ( Const i) = show i printAlg ( Add a b) = "(" ++ a ++ " + " ++ b ++ ")" printAlg ( Mul a b) = a ++ " * " ++ b

Wait, but it cannot be of any use, I can hear you say. We do not have any way of applying that function unwrapping strings into strings to our fixedExpr . Fear not!

Catamorphisms

cata :: Functor f => (f b -> b) -> Fix f -> b cata f = f . fmap (cata f) . outF

Calling cata printAlg fixedExpr gives "(2 + 2) * 2" . WOW! The first time I got this I got so excited, this is amazing. Awesome. How does it work? outF unwraps one level from fixedExpr then fmap which we wrote some time ago recurses inside. When it gets to leafs which are Const i , it converts each to a string. Then f s level up are called, but they already have their arguments as strings. We have eliminated recursion from our functions altogether! The only thing we had to define was what happens to a single element of ExprF String . This is an essence of functional programming, to compose a program from multiple parts, each focused on doing it’s own thing well.

We have achieved so much so easily, can we get an integer value for the operation defined by same tree? We just need a simple new algebra:

getValue :: ExprF Int -> Int getValue ( Const i) = i getValue ( Add a b) = a + b getValue ( Mul a b) = a * b

cata getValue fixedExpr => 8

Notice how, again, we just have to describe what happens to a single element, and all the internal types are Int s. This keeps our code concise and fast. There is only one place where recursion happens, in the definition of cata . Once we’re sure it is without errors, the possibility of making a mistake writing recursion disappears.

Anamorphism

We can abstract away folds, but can we unfold from a single value using a similar scheme? Sure we can. For that we need an opposite of algebra, a coalgebra

type Coalgebra f a = a -> f a unwrap :: Coalgebra ExprF Int unwrap i | i < 4 = Add (i + 1 ) (i + 2 ) | otherwise = Const i

Anamorphism is a kind of opposite of a catamorphism, so let us see what we get if we just flip functions around in the catamorphism. We have to remember to wrap where cata unwraps:

ana f = Fix . fmap (ana f) . f

ana unwrap 1 => No instance for (Show (Fix ExprF)) arising from a use of ‘print’

Oh, there is no automatic way of printing Fix , which may be infinite. But we know how to print it, just use our catamorphism:

cata printAlg $ ana unwrap 1 => "(((4 + 5) + 4) + (4 + 5))"

Nice.

Paramorphisms

Notice that neither of these functions has access to the original structure. Stages higher up in a catamorphism (fold) only see the pretty-printed versions of elements down the tree, if we are creating a string-based representation. In many real-world tasks, this is not enough, and we need access to the original values along with their output representations. It would be a bit silly to parse output strings when folding, and it would negate any possible advantages of recursion schemes.

Algebras that carry that information are called R-Algebras

type RAlgebra f a = f ( Fix f, a) -> a

So instead of just accepting an element inside a functor, we get a tuple with the original entry and the pretty-printed element. A recursion scheme accepting this type is called a paramorphism

para :: forall f a . ( Functor f) => RAlgebra f a -> Fix f -> a para rAlg = rAlg . fmap fanout . outF where fanout :: Fix f -> ( Fix f, a) fanout t = (t, para rAlg t)

Notice how types perfectly match up — so much so that we can almost write these schemes by intuition. If you’re typing this in ghci, enable ScopedTypeVariables . It is needed for types f and a in fanout to be interpreted by ghc as the same as f and a as in the definition of para . Explicit forall just signals that both f and a should be available as the same types in fanout .

A cool example of a paramorphism would be to sum the additions together, if it turns we want to shorten the output for some important reason

concatSums :: RAlgebra ExprF String concatSums ( Const i) = show i concatSums ( Add (aExpr, _) (bExpr, _)) = show $ cata getValue aExpr + cata getValue bExpr concatSums ( Mul (_, a) (_, b)) = a ++ " * " ++ b

Here, we have used both — catamorphism for wrapping up the sums and paramorphism for neatly doing everything in one step.

Notice how Const i behaves differently then Add and Mul . We can see why it is so from the definition of fmap above. fmap something (Const i) equals Const i by definition, so it allows us to populate leaves in the tree without calling fanout and to have a neat starting point for providing two arguments to Add and Mul

Coolness.

Summing up

We have learnt how to abstract recursion away to transform recursive code into one that exploits recursion inherently present in the datatype itself. We now know what a fixed point of a functor is and can write five different algebras before morning coffee. If you have a minute, please let me know how this tutorial worked for you. I would love to use your feedback when writing part 2.

Next part

In part 2, we will explore the recursion-schemes library to move our understanding into the real world Haskell. The ideas will be broadly the same, with some magic sauce to make these functions even more generic. You can still use what you have learned here in your code as is, though, so feel free to play with it.

Further reading

In researching this post I have mainly used the following resources:

Please enable JavaScript to view the comments powered by Disqus.