A catamorphism can either deconstruct a value
[1,2,3].reduce((acc, x) => acc + x, 0); // 6
or maintain the structure and act like the identity of the underlying type:
[1,2,3].reduce((acc, x) => acc.concat([x]), []); // [1,2,3]
With lists (or arrays in JS) the catamorphism and the fold (of a foldable container) coincide.
However, with trees they do not:
const treeCata = f => ([x, xs]) =>
f(x) (arrMap(treeCata(f)) (xs));
const arrMap = f => xs =>
xs.map((x, i) => f(x, i));
const arrFold = f => acc => xs =>
xs.reduce((acc, x) => f(acc) (x), acc);
const log = x => (console.log(x), x);
const Node = (x, xs) => ([x, xs]);
const Node_ = x => xs => ([x, xs]);
const tree = Node(1, [
Node(2, [
Node(3, []),
Node(4, [])
]),
Node(5, [])
]);
const foo = treeCata(Node_) (tree);
const bar = treeCata(x => xs => x + arrFold(y => z => y + z) (0) (xs)) (tree);
log(foo);
log(bar);
The role as identity works as expected. The deconstruction, however, is a bit more involved. As a matter of fact I need a list fold to conduct it. I can actually see that there is one layer of deconstruction, because otherwise a list fold wouldn't be enough for a non-linear tree. But still having to use another fold seems odd.
I can only guess that this behavior is related to the terse definition of the used catamorphism. It only takes a single case into account the product Tree a = Node a [Tree a]. Maybe it would be more promising if I'd fully embrace the algebraic structure of the type and distingiush Node/Array constructor and the empty array case.
But does this mean treeCata is not a proper catamorphism? What is it lacking?
There are two kinds of folds.
Structural folds intuitively replace each data constructor of a data structure with a given function.
However, traversal folds traverse the individual elements of a data structure and summarize them.
In Haskell, the Foldable type class implements the traversal folds. The minimum complete definition for the Foldable type class is the
foldrmethod. So, let's implement it for trees.Rules of thumb: