【问题标题】:Map an arbitrary n-ary Tree with fold用折叠映射任意 n 叉树
【发布时间】:2018-05-14 07:45:08
【问题描述】:

我想要一些通用工具来处理树木。我正在使用 JavaScript,所以我可以强加的东西很少,而且我正在使用我无法更改的现有数据结构。我设法定义了以下内容:

reduceTree :: (T a -> [T a]) -> (b -> T a -> b) -> b -> T a -> b
reduceTree(getChildren, f, accumulator, tree)

(我使用 Haskell 类型签名是因为它们更容易阅读)

这个getChildren 函数是必需的,因为我的树是任意的,我不知道它是如何构造的。

reduceTree 运行良好。但我也想有一个mapTree 函数,最好重用我的reduceTree 函数,但我被卡住了。有什么不对劲,但我不知道是什么。

编辑

我的reduceTree 实现:

export function reduceTree(getChildren, f, accumulator, tree) {
  const children = getChildren(tree);
  if (!children || children.length === 0) {
    return f(accumulator, tree)
  } else {
    const childrenResult = children.reduce(
      (accumulator, subTree) => reduceTree(getChildren, f, accumulator, subTree),
      accumulator
    );
    return f(childrenResult, tree)
  }
}

它已经过测试并且可以工作。

(我用来构造/证明上面的javascript的伪Haskell实现:

reduceTree f a (Node val []) = f a val
reduceTree f a (Node val xs) = f (fold (reduceTree f) acc) val

)

【问题讨论】:

  • 你知道mapTree的签名了吗?
  • 您能否展示一下您对reduceTree 的实现,好吗?我猜它目前不会调用f - 除非你将getChildren 更改为getContents :: T a -> (a, [T a])
  • 啊,确实如此 - 但您的 f 应该采用 a 参数,而不是 T a
  • @Bergi 我通过创建f :: b -> T a -> b 来避免处理节点本身的结构。请记住,我实际上使用的是 javascript,所以一切都很松散。
  • @WillNess, @Bergi 我添加了reduceTree 的实现和我的伪代码。请注意,我的“树”只有具有某些值的节点和一个(可能为空的)子级列表。

标签: javascript haskell tree functional-programming fold


【解决方案1】:

TL;DR:您的伪代码已损坏。解决它的一种方法是

reduceTree :: (b -> a -> b) -> b -> T a -> b
reduceTree f acc (Node val []) = f acc val
reduceTree f acc (Node val ts) = 
    Data.List.foldl (\acc tree -> reduceTree f acc tree) (f acc val) ts

这意味着你的 Javascript 应该是

export function reduceTree(getChildren, f, accumulator, tree) {
  const children = getChildren(tree);
  if (!children || children.length === 0) {
    return f(accumulator, tree)
  } else {
    const childrenResult = children.reduce(
      (accumulator, subTree) => reduceTree(getChildren, f, accumulator, subTree),
      f(accumulator,tree)  // referring to `tree` only for its stored node value, yes?
    );
    return childrenResult;
  }
}

大概 Javascript 在列表中的 reduceleft 折叠(根据 Wikipedia 是这样)。

它执行前序树遍历,相当于本文底部的tfoldl 函数。用它来实现map 并不完全奏效,

tmap f t = reduceTree (\acc val -> Node (f val) ???) ??? t

因为Node :: a -> [T a] -> T a的类型不合适,所以不能适应上面的reducer类型b -> a -> b(需要a -> [b] -> b类型)。

这是因为这种线性折叠本质上是将结构扁平化,将其视为线性序列。

下面是一些无关紧要的细节。


Haskell has it the exact same way 作为Aadit's answer 中的reduceTree 函数。

约翰休斯在他的著名论文“为什么函数式编程很重要”中也有几乎相同的方式,因为

foldTree :: (a -> b -> r) -> (r -> b -> b) -> b -> Tree a -> r 
foldTree f g z (Node x t) = f x . foldr g z . map (foldTree f g z) $ t

他使用了一个等效但更冗长的公式,他称之为redtree,用于“减少树”。它认为

foldTree f g z = reduceTree (\a rs -> f a (foldr g z rs)) 

所以两者几乎是等价的。那么,

map h = reduceTree (Node . h) 
      = reduceTree (\a rs -> Node (h a) rs) 
      = foldTree (Node . h) (:) [] 

没有“零”,即初始累加器值来自于列表中数据定义中没有第二个子句data T a = Node a [T a],而不是List a = Nil | Cons a (List a)

后者的折叠缩减函数将NilCons a r 转换为r,因此它必须具有“零”,即提供给它的默认值;对于前者,它需要Node a [r]r,所以没有Nil 需要处理的情况(参见)。


在 cmets 中从 user Bergia hint 之后,Haskell 包 containers 为这种类型定义了 a Foldable instance

data T a = Node a [T a]

其等价于foldr(为方便起见,带有翻转的参数),是

tfoldr :: (a -> b -> b) -> T a -> b -> b 
tfoldr f (Node x ts) z = f x $ Data.List.foldr ($) z [tfoldr f t | t <- ts]

确实穿过状态/累加器!也可以写成

tfoldr :: (a -> b -> b) -> T a -> b -> b 
tfoldr f (Node x ts) z = f x . Data.List.foldr (.) id [tfoldr f t | t <- ts] $ z

以您更容易实施的为准。这是实现后序树遍历;对于通常的预购遍历使用

tfoldl :: (a -> b -> b) -> T a -> b -> b
tfoldl f (Node x ts) z = Data.List.foldr (>>>) id [tfoldl f t | t <- ts] $ f x z
                 -- // = tfoldl f tn (... (tfoldl f t2 (tfoldl f t1 (f x z))) ...)

(f &gt;&gt;&gt; g) x = g (f x),或者

tfoldl :: (b -> a -> b) -> T a -> b -> b
tfoldl f (Node x ts) z = Data.List.foldr (>>>) id [tfoldl f t | t <- ts] $ f z x
                 -- // = tfoldl f tn (... (tfoldl f t2 (tfoldl f t1 (f z x))) ...)

相当于本文开头的代码,直到参数的顺序。

【讨论】:

  • 谢谢!这是一些非常有用的解释,它对我帮助很大。我最终使用了 Aadit 的解决方案,但在阅读您的答案之前我不明白。
  • @Luftzig 谢谢你,还有我,在写之前。 :) 有趣的是,一种折叠如何让您轻松地重新创建结构,而另一种则破坏它。在用正确的折叠实现tail 时有这样的事情,这很难做到 IIRC,而且对于变形来说是微不足道的。这里肯定有一些更深的电流......
  • re:线性,这意味着 很容易将函数映射到节点值同时 将 Tree 转换为 List。只是在这个过程中丢失了树结构。也许可以通过用一些“标记”以某种方式装饰减速器函数的输出类型,然后重新创建结构来恢复它。 --- 要问自己的相反问题可能是,是否可以用 Aadit 的折叠将一棵树展平成一个列表。
  • @WillNess 也许你的foldTree 又名redtree 太普通了,但我认为它是一颗实用的珍珠。与 Aadit 变质的等价对我来说并不明显,b/c 它包含一个正确的折叠,因此失去了非线性结构。但是由于foldTree 有两个不同的功能,它可以以某种方式检索结构。这是一个了不起的组合器,感谢您从论文中挖掘出来。
  • @scriptum John Hughes's foldTree,我只是在语法上对其进行了调整。 :) 这篇论文以更详细的方式介绍了它,我为自己认识到这种模式并将递归调用放入组合链中而感到非常自豪。 :) 它还具有here BTW。我确实记得写过这个答案,但不是关于它的所有细节。 :) 我想你已经把你的手指放在了它上面,两个功能的区别。
【解决方案2】:

我看到你的树数据结构定义如下:

data T a = Node a [T a]

如果是这样,那么你的树数据结构的折叠将是:

reduceTree :: (a -> [b] -> b) -> T a -> b
reduceTree f = let g (Node a xs) = f a (map g xs) in g

您现在可以使用reduceTree 定义mapTree,如下所示:

mapTree :: (a -> b) -> T a -> T b
mapTree f = reduceTree (Node . f)

将其全部转换为 JavaScript:

const Node = (a, xs) => ({a, xs});

const reduceTree = (f, node) => {
    const g = node => f(node.a, node.xs.map(g));
    return g(node);
};

const mapTree = (f, node) => reduceTree((a, xs) => Node(f(a), xs), node);

const tree = Node(2, [ Node(3, [ Node(11, [])
                               , Node(13, []) ])
                     , Node(5, [])
                     , Node(7, [ Node(17, [])
                               , Node(19, []) ]) ]);

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

希望对您有所帮助。

【讨论】:

  • 我认为reduceTree 应该有签名(a -&gt; b -&gt; b) -&gt; T a -&gt; b - 而不是bs 的列表。当然这个定义更适合你的map
  • 不,Node 有一个子列表。这些孩子中的每一个都需要转换为b,才能将Node 转换为b。将一个b 作为输入是没有意义的。如果Node 没有孩子怎么办?你会从哪里变出b
  • @Bergi Haskell has it the exact same way as here。约翰休斯在他的论文“为什么函数式编程很重要”had it almost the same, asfoldTree :: (a -&gt; b -&gt; r) -&gt; (r -&gt; b -&gt; b) -&gt; b -&gt; Tree a -&gt; r ; foldTree f g z (Node x t) = f x . foldr g z . map (foldTree f g z) $ t。有了它,foldTree f g z = reduceTree (\a rs -&gt; f a (foldr g z rs))map h = reduceTree (Node . h) = reduceTree (\a rs -&gt; Node (h a) rs) = foldTree (Node . h) (:) []
  • @Bergi 缺少“零”,即初始累加器值来自数据定义中缺少第二个子句,T a = Node a [T a]a.o.t.列表'List a = Nil | Cons a (List a)。所以前者的代数是NodeF a r -&gt; rdata NodeF a r = NodeF a [r],第二个是ConsF a r -&gt; rdata ConsF a r = NilF | ConsF a r(在recursion-schemes中讲)。所以没有 Nil 的情况需要处理。
  • 谢谢大家!我最终使用了您的版本,但每个人都非常有帮助!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-04
  • 2013-07-20
  • 2021-07-11
  • 2021-05-04
  • 1970-01-01
相关资源
最近更新 更多