Suppose you have a recursive `tree` data structure, and another one `tree' x` where `tree' x` is just like `tree`, but with recursive occurrences of `tree` replaced with type x. So maybe something like, in Haskell-ish syntax:
type tree = Leaf int | Node tree tree
type tree' x = Leaf int | Node x x
Then one can easily write a `fold` over trees with type fold: tree -> (tree' x -> x) -> x
where the second parameter (called an algebra) evaluates the result over a tree' assuming recursive subtrees have been replaced with the result of their evaluation (this is the key).Now the beauty of recursion schemes is that whenever `tree'` is a functor, you can get `tree` (as a fixpoint: `tree = tree' tree`) and `fold` for free (generally called `cata`), which can be seen by rewriting `fold` above using just `map`.
This skips a fair amount of boilerplate code you no longer have to touch when modifying recursive types.
They say a picture is worth a thousand words, so hopefully this clears up the difference betwene `out` and `toTreeF`:
-- | Our basic binary tree type
data Tree a = Leaf a | Node (Tree a) (Tree a)
-- | A pattern functor for our binary tree
data TreeF a r = LeafF a | NodeF r r
-- | The fixed point of 'f'.
newtype Fix f = Fix { unfix :: f (Fix f) }
-- | Given an F-Algebra on 'f' we have a catamorphism on 'Fix f'
cata :: Functor f => Algebra f a -> Fix f -> a
cata alg fix = alg $ fmap (cata alg) $ unfix fix
-- | We can sum the elements of a 'Fix (TreeF Int)' using 'cata'
sumTreeF :: Fix (TreeF Int) -> Int
sumTreeF = cata $ \case
LeafF i x -> x + i
NodeF l r x -> x + l + r
-- | To operate our binary tree we need to map from 'Tree a' to 'TreeF
-- a'.
toTreeF :: Tree a -> Fix (TreeF a)
toTreeF = \case
Leaf a -> LeafF a
Node l r -> NodeF (toTreeF l) (toTreeF r)
sumTree :: Tree Int -> Int
sumTree = sumTreeF . toTreeF
I didn't compile this code but it should be correct. out :: Fix (TreeF a) -> TreeF a (Fix (TreeF a))
toTreeF -> Tree a -> Fix (TreeF a) type Tree a = Fix (TreeF a)
so that `toTreeF` is just `unfix` (and is effectively free, as I mentioned above)From some of the sibling comments, seems like there is a strong connection between these recursion schemes and iterator strategies?
if so how is that different from Observable streams or event emitters.
That's an interesting insight - thanks
def sum(xs):
if xs == []:
return 0
else:
x = xs.pop(0)
return x + sum(xs)
This sort of reduction operation is very broadly useful, so it has been abstracted in frameworks like MapReduce as well as library functions like functools.reduce (https://docs.python.org/3/library/functools.html#functools.r...). Recursion schemes build on explicit reduce functions by being strongly typed (which, in addition to reducing bugs, enables a something like a super-powered visitor pattern), very orthogonal (which reduce redundancy and code duplication as a user of the abstraction), and very general (which let you solve a lot of problems with the same small set of programming tools without needing to remember special cases). In particular, the way that they look at data structures lets you interleave the recursion across nested data structures in a way that would be a huge pain in the butt with e.g. Python's __iter__ interface. There are some other nifty things that the approach brings to the table as well, but I think those are the major wins from the perspective of someone not already interested in strongly-typed functional programming.While I covered the case of things that are sort of like reduce (called catamorphisms in the language of recursion schemes), this paper also has analogous abstractions for things that are sort of like itertools.accumulate (https://docs.python.org/3/library/itertools.html#itertools.a..., called anamorphisms in recursion schemes), as well as combinations thereof (called hylomorphisms). They all use a relatively small number of building blocks and combine in a quite beautiful and useful way, but it's hard to describe precisely without leaning quite heavily on the language of strongly-typed functional programming.