user5536315
user5536315

Reputation:

How to map a tree in a stack safe manner?

There are different ways to map a tree recursively:

const reduceTree = (f, node) => {
  const go = ([x, xs]) => f(x, xs.map(go));
  return go(node);
};

const mapTree = (f, node) =>
  reduceTree((x, xs) => Node_(f(x), xs), node);

const Node_ = (x, xs) => ([x, xs]);
const Node = (x, ...xs) => ([x, xs]);

const tree = Node(1,
  Node(2,
    Node(3),
    Node(4,
      Node(5))),
  Node(6),
  Node(7,
    Node(8),
    Node(9),
    Node(10),
    Node(11)));

console.log(
  mapTree(x => 2 * x, tree));

This is in fact an elegant solution, however, it is not stack safe. Since the recursive call (xs.map(go)) isn't in tail position, I can't fall back to a trampoline and it doesn't seem trivial to me to transform the algorithm in tail recursive form.

The usual way to do this is probably a CPS transformation, which kind of obfuscates the computation. Maybe there is another way to achieve stack safety - with a generator, for instance? Are there general rules for such a transformation, which can be applied in an almost mechanical way?

I am primarily interested in the process of doing this transformation not in the final result.

Upvotes: 2

Views: 185

Answers (1)

Mulan
Mulan

Reputation: 135415

Starting with a well-formed mutually recursive pair -

// map :: (a -> b, Tree a) -> Tree b
const map = (f, [ v, children ]) =>
  Node (f (v), ...mapAll (f, children))

// mapAll :: (a -> b, [ Tree a ]) -> [ Tree b ]
const mapAll = (f, nodes = []) =>
  nodes .map (n => map (f, n))

We first remove the eager Array.prototype.map which cannot possibly allow a tail form -

const map = (f, [ v, children ]) =>
  Node (f (v), ...mapAll (f, children))

const mapAll = (f, [ node, ...more ]) =>
  node === undefined
    ? []
    : [ map (f, node), ...mapAll (f, more) ]

Next add the parameter for CPS conversion -

const identity = x =>
  x

const map = (f, [ v, children ], k = identity) =>
  mapAll (f, children, newChilds =>        // tail
    k (Node (f (v), ...newChilds)))        // tail

const mapAll = (f, [ node, ...more ], k = identity) =>
  node === undefined
    ? k ([])                               // tail
    : map (f, node, newNode =>             // tail
        mapAll (f, more, moreChilds =>     // tail
          k ([ newNode, ...moreChilds ]))) // tail

Verify the results in your own browser below

const Node = (x, ...xs) =>
  [ x, xs ]

const identity = x =>
  x

const map = (f, [ v, children ], k = identity) =>
  mapAll (f, children, newChilds =>
    k (Node (f (v), ...newChilds)))
  
const mapAll  = (f, [ node, ...more ], k = identity) =>
  node === undefined
    ? k ([])
    : map (f, node, newNode =>
        mapAll (f, more, moreChilds =>
          k ([ newNode, ...moreChilds ])))

const tree = 
  Node
    ( 1
    , Node
        ( 2
        , Node (3)
        , Node
            ( 4
            , Node (5)
            )
        )
    , Node (6)
    , Node
        ( 7
        , Node (8)
        , Node (9)
        , Node (10)
        , Node (11)
        )
    )

console.log (map (x => x * 10, tree))

Note, CPS on its own doesn't make the program stack-safe. It's just the form required to put the code on a trampoline of your choice.

Upvotes: 0

Related Questions