不太确定你在问什么。但是,是的,你需要将与树相关的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)
我时间不够了。我知道这些内容非常抽象,但希望至少能给你提供一个新的视角来帮助你的学习。祝你好运!