如何在不可变的树状结构上实现堆栈安全操作?

问题描述

我有一个不可变的树状结构:

type Data<A> = Empty | Node<A> | Both<A>

type Empty = { tag: Tag.Empty };
type Node<A> = { tag: Tag.Node,value: A };
type Both<A> = { tag: Tag.Both,left: Data<A>,right: Data<A> };

在此数据结构上实现foldmap甚至flatMap堆栈安全操作的良好起点是什么?

解决方法

让我们从数据构造函数开始。

// data Data a = Empty | Node a | Both (Data a) (Data a)
const Empty = { tag: "Empty" };
const Node = value => ({ tag: "Node",value });
const Both = left => right => ({ tag: "Both",left,right });

// data Cont a b = Left (Data a) (Cont a b) | Right b (Cont a b) | Done
const Left = rightQ => next => ({ tag: "Left",rightQ,next });
const Right = leftA => next => ({ tag: "Right",leftA,next });
const Done = { tag: "Done" };

// data Frame a b = Fold (Data a) (Cont a b) | Apply (Cont a b) b
const Fold = data => cont => ({ tag: true,data,cont });
const Apply = cont => result => ({ tag: false,cont,result });

接下来,我们定义一个堆栈安全的 fold 函数。

// fold :: (b -> b -> b) -> (a -> b) -> b -> Data a -> b
const fold = both => node => empty => data => {
    let frame = Fold(data)(Done);

    while (true) {
        const { data,result } = frame;
        const { value,right } = data || {};
        const { leftA,next } = cont;

        switch (frame.tag ? data.tag : cont.tag) {
        case "Empty":
            frame = Apply(cont)(empty);
            continue;
        case "Node":
            frame = Apply(cont)(node(value));
            continue;
        case "Both":
            frame = Fold(left)(Left(right)(cont));
            continue;
        case "Left":
            frame = Fold(rightQ)(Right(result)(next));
            continue;
        case "Right":
            frame = Apply(next)(both(leftA)(result));
            continue;
        case "Done":
            return result;
        }
    }
};

注意 fold 是一个结构折叠,但我们可以用它来定义 foldr,它是一个遍历折叠。

// id :: a -> a
const id = x => x;

// compose :: (b -> c) -> (a -> b) -> a -> c
const compose = g => f => x => g(f(x));

// flip :: (a -> b -> c) -> b -> a -> c
const flip = f => y => x => f(x)(y);

// foldr :: (a -> b -> b) -> b -> Data a -> b
const foldr = func => flip(fold(compose)(func)(id));

最后,我们可以使用 fold 来定义 mapflatMap

// map :: (a -> b) -> Data a -> Data b
const map = func => fold(Both)(compose(Node)(func))(Empty);

// flatMap :: (Data a) -> (a -> Data b) -> Data b
const flatMap = flip(func => fold(Both)(func)(Empty));
,

正如评论中提到的,您显然需要一个 iterative tree-traversal algorithm 来执行一些标准的函数式编程操作,例如 mapfold(又名 reduce)。链接文章中提到的迭代算法涉及构建节点父节点的堆栈,或者维护从每个节点到其父节点的映射。

这里是 fold 上的 Data<T> 可能的(未经过广泛测试,所以要小心)实现,它在构建所需的输出结构时维护从节点到其父节点的映射。请记住,肯定还有其他算法。我只是选择了这个,因为它最清楚地表达了我如何穿过一棵树的设想:

function fold<T,U>(inData: Data<T>,f: (acc: U,t: T) => U,init: U) {
  const parents: Map<Data<T>,Data<T> | undefined> = new Map();
  let cur: Data<T> | undefined = inData;
  let acc = init;
  while (cur) {
    if ((cur.tag === Tag.Both) && (!parents.has(cur.left))) {
      parents.set(cur.left,cur);
      cur = cur.left;
    } else if ((cur.tag === Tag.Both) && (!parents.has(cur.right))) {
      parents.set(cur.right,cur);
      cur = cur.right;
    } else {
      if (cur.tag === Tag.Node) {
        acc = f(acc,cur.value);
      }
      cur = parents.get(cur);
    }
  }
  return acc;
}

如果您当前的节点是一个 Both 并且您还没有处理过 left 节点,请将当前节点添加到他的 parents 映射中作为 left 节点的父节点,然后沿着树向左走。否则,如果节点是 Both 并且您尚未处理 right 节点,请执行相同的操作但向右。否则,您要么正在处理 EmptyNode 节点,要么正在处理其 Bothleft 节点已被处理的 right 节点......这意味着您然后可以处理当前节点以进行输出,然后返回到父节点。最终,您将尝试走到树根的父节点,然后就完成了。

使这个 fold 的部分是我们在处理 Node 节点时运行累加器回调。同样重要的是要注意 fold 通常取决于遍历的顺序(例如,对于列表,左折叠和右折叠是不同的),因此上面的实现可能不是您想到的(你想要预购?按顺序?后顺序?广度优先?其他东西?)。


这是一个类似的 map 实现:

function map<T,f: (t: T) => U) {
  const parents: Map<Data<T>,Data<T> | undefined> = new Map();
  const mapped: Map<Data<T>,Data<U>> = new Map();
  let cur: Data<T> | undefined = inData;
  while (cur) {
    if ((cur.tag === Tag.Both) && (!parents.has(cur.left))) {
      parents.set(cur.left,cur);
      cur = cur.right;
    } else {
      mapped.set(cur,cur.tag === Tag.Both ? ({
          tag: Tag.Both,left: mapped.get(cur.left)!,right: mapped.get(cur.right)!
        }) : cur.tag === Tag.Empty ? ({
          tag: Tag.Empty
        }) : ({
          tag: Tag.Node,value: f(cur.value)
        })
      );
      cur = parents.get(cur);
    }
  }
  return mapped.get(inData)!;
}

fold类似,但输出处理不同。为 Both 构建输出节点涉及使用已为每个子树构建的输出节点,因此该实现还存储了从输入节点到输出节点的 mapped 映射,以便稍后检索。>


我不打算介绍 flatMap(),但它类似于 map,只是 Node 输入的输出节点是由回调函数直接生成的,而不需要构建.您可以验证这些对合理的树做了合理的事情(底部的 Playground 链接有一些示例)。


Playground link to code

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...