Catamorphism和Haskell中的树遍历

16

我迫不及待地希望了解与此SO问题相关的catamorphism:链接 :)

我只练习过《Real World Haskell》教程的开始部分。因此,也许我现在会问太多了,如果是这样,请告诉我应该学习哪些概念。

下面,我引用了维基百科关于catamorphism的代码示例链接.

我想知道您对下面的foldTree(一种遍历树的方式)的看法,以及与另一个处理树的SO问题和答案n-ary tree traversal相比如何。(独立于二进制或非二进制,我认为下面的catamorphism可以编写成管理n-ary tree的方式)

我在注释中写出了我理解的内容,如果您能纠正我并澄清一些事情,我将不胜感激。

{-this is a binary tree definition-}
data Tree a = Leaf a
            | Branch (Tree a) (Tree a)

{-I dont understand the structure between{} 
however it defines two morphisms, leaf and branch 
leaf take an a and returns an r, branch takes two r and returns an r-} 
data TreeAlgebra a r = TreeAlgebra { leaf   :: a      -> r
                                   , branch :: r -> r -> r }

{- foldTree is a morphism that takes: a TreeAlgebra for Tree a with result r, a Tree a
and returns an r -} 
foldTree :: TreeAlgebra a r -> Tree a -> r
foldTree a@(TreeAlgebra {leaf   = f}) (Leaf   x  ) = f x
foldTree a@(TreeAlgebra {branch = g}) (Branch l r) = g (foldTree a l) (foldTree a r)

现在我遇到很多困难,我似乎猜到 morphism leaf 会应用于任何 Leaf

但是为了实际使用这段代码,foldTree 需要输入一个定义了 morphism leaf 的 TreeAlgebra,这样才能做些什么吗?
但是在这种情况下,在 foldTree 代码中我会期望 {f = leaf} 而不是相反的

如果你能给予任何解释,将不胜感激。


无关的注释:标签“catamporphisms”拼错了,多了一个‘p’。显然我还不够酷可以编辑它,因为那将构成创建新标签。(耶稣流泪了。) - Derrick Turk
@Derrick Turk:这个标签下只有三个问题,重新打标签不会太难。 - fuz
@FUZxxl:显然,您需要1500个声望才能创建新的标签,而在当时,“catamorphism”尚不存在。 - ephemient
2个回答

29

不太确定你在问什么。但是,是的,你需要将与树相关的TreeAlgebra提供给foldTree,以便执行你想要在树上进行的计算。例如,要对Int类型的树中的所有元素求和,你可以使用以下代数:

sumAlgebra :: TreeAlgebra Int Int
sumAlgebra = TreeAlgebra { leaf = id
                         , branch = (+) }

这意味着,要得到一个叶节点的总和,请对叶节点中的值应用id(不做任何操作)。要得到一个分支的总和,请将每个子节点的总和相加。

能够使用(+)来表示分支,而不是像\x y -> sumTree x + sumTree y这样,是范畴论的基本属性。这意味着,要在某个递归数据结构上计算一些函数f,只需要知道其直接子节点的值即可。

Haskell是一种非常独特的语言,因为我们可以抽象地形式化范畴论的思想。让我们创建一个数据类型来描述树中的单个节点,其参数为其子节点:

data TreeNode a child
    = Leaf a
    | Branch child child

看到这里你是否发现了我们做了什么?我们用我们选择的一种类型替换了递归子节点。这样做是为了在折叠时能够放置子树的总和。
现在是真正神奇的部分。我将使用伪Haskell编写这个代码 - 使用真正的Haskell也是可能的,但我们必须添加一些注释来帮助类型检查器,这可能有点令人困惑。我们取一个参数化数据类型的“不动点”——也就是构造一个数据类型T,使得T = TreeNode a T。他们称这个运算符为Mu。
type Mu f = f (Mu f)

请仔细看这里。 Mu 的参数不是像 Int 或者 Foo -> Bar 那样的类型。它是一个类型构造器,比如 Maybe 或者 TreeNode Int——Mu 本身的参数需要一个参数。(抽象出类型构造器的可能性是 Haskell 类型系统在表达能力方面真正突出的一点之一)。
因此,类型 Mu f 被定义为使用 f 并用 Mu f 本身来填充其类型参数。我将定义一个同义词来减少一些噪音:
type IntNode = TreeNode Int

扩展Mu IntNode,我们得到:

Mu IntNode = IntNode (Mu IntNode)
           = Leaf Int | Branch (Mu IntNode) (Mu IntNode)

你看到了吗?Mu IntNode如何等价于你的Tree Int?我们刚刚撕开了递归结构,然后使用Mu将其重新组合在一起。这使我们能够同时讨论所有Mu类型。这为我们定义所需的内容提供了便利。

让我们定义:

type IntTree = Mu IntNode

我说catamorphism的基本属性是,为了计算某个函数f,只需获得其直接子节点f的值即可。我们称正在尝试计算的类型为r,数据结构为node(IntNode可能是这个的一个实例)。因此,要计算特定节点上的r,我们需要将节点替换为其子节点的r。该计算的类型为node r -> r。因此,catamorphism表示如果我们有其中一种计算方式,则我们可以计算整个递归结构(请记住,递归在此处显式地用Mu表示)的r。
cata :: (node r -> r) -> Mu node -> r

让我们以实际例子来说明,具体如下所示:
cata :: (IntNode r -> r) -> IntTree -> r

重新阐述一下,如果我们可以取一个具有其子节点为r的节点并计算出r,那么我们就可以计算整个树的r
为了实际计算这个值,我们需要node成为一个Functor,也就是说,我们需要能够在节点的子节点上映射任意函数。
fmap :: (a -> b) -> node a -> node b

对于IntNode,这可以直接完成。

fmap f (Leaf x) = Leaf x                  -- has no children, so stays the same
fmap f (Branch l r) = Branch (f l) (f r)  -- apply function to each child

现在,我们终于可以给定义一个定义了(Functor node约束只是说node具有适当的fmap):
cata :: (Functor node) => (node r -> r) -> Mu node -> r
cata f t = f (fmap (cata f) t)

我将参数名称t用作“tree”助记值的翻译。这是一个抽象、密集的定义,但实际上非常简单。它表示:对于每个t的子节点(它们本身是Mu node),递归执行cata f——我们正在对树执行的计算——以获取node r,然后将该结果传递给f来计算t本身的结果。
将其与开始部分联系起来,您正在定义的代数本质上是定义node r -> r函数的一种方式。事实上,给定TreeAlgebra,我们可以轻松获得折叠函数。
foldFunction :: TreeAlgebra a r -> (TreeNode a r -> r)
foldFunction alg (Leaf a) = leaf alg a
foldFunction alg (Branch l r) = branch alg l r

因此,树形消解可以通过以下方式基于通用的消解进行定义:
type Tree a = Mu (TreeNode a)

treeCata :: TreeAlgebra a r -> (Tree a -> r)
treeCata alg = cata (foldFunction alg)

我时间不够了。我知道这些内容非常抽象,但希望至少能给你提供一个新的视角来帮助你的学习。祝你好运!


4

我认为你在问关于{}的问题。早先有一个好的讨论{}的问题。这些被称为Haskell记录语法。另一个问题是为什么要构建代数。这是一种典型的函数范式,其中将数据泛化为函数。

最著名的例子是Church构造自然数,其中f= +1z=00=z1=f z2=f(f z)3=f(f(f z)), 等等...

你看到的本质上是相同的想法应用于树。研究Church的例子,树就会变得清晰明了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接