不幸的是,“折叠”这个词有点过分了。
结构递归
如果您希望您的 fold 函数捕获一些结构递归的概念,我认为对于这种类型的树,您不希望它采用两个参数(除了树参数)。
类型
这种折叠函数的类型来自数据类型的构造函数的类型。
在您的情况下,您的数据类型
data Tree a = Node {rootLabel :: a, subForest :: [Tree a]}
只有一个构造函数:
Node :: a -> [Tree a] -> Tree a
因此,除了要折叠的树之外,您的 fold 函数将只接受一个参数。此参数的类型是通过将Tree a
构造函数类型中所有出现的 替换为新的类型变量来获得的,例如b
:
a -> [Tree a] -> Tree a
| | |
| | |
\ / \ / \ /
- - -
a -> [ b ] -> b
类型变量b
也构成函数的返回类型。也就是说,你得到
treeFold :: (a -> [b] -> b) -> Tree a -> b
执行
此函数的实现也遵循数据类型的定义。b
通过递归地将 fold 函数应用于a 的Tree a
类型化部分,您可以获得必须传递给 fold 的第一个参数的类型化值Node
。这里有点棘手的是这些部分存储在一个列表中,因此您还需要映射该列表:
treeFold f (Node x xss) = f x (map (treeFold f) xss)
或者,可以说更好一点:
treeFold f = h
where
h (Node x xss) = f x (map h xss)
关键思想是,您将Node
树值中的构造函数的所有应用程序替换f
为作为第一个参数传递给的函数的应用程序treeFold
。
例子
例如,您现在可以使用treeFold
定义一个对树中所有元素求和的函数:
total :: Num a => Tree a -> a
total = treeFold (\n ns -> n + sum ns)
或者一个简单地返回树包含多少元素的函数:
size :: Tree a -> Int
size = treeFold (\_ ns -> 1 + sum ns)
减少
另一个品牌的折叠是那些对数据结构进行(左偏或右偏)归约的折叠。这些折叠确实通常需要两个额外的参数:
treeFoldr :: (a -> b -> b) -> b -> Tree a -> b
treeFoldl :: (b -> a -> b) -> b -> Tree a -> b
例如,您可以根据辅助功能来实现这些功能flatten
flatten :: Tree a -> [a]
flatten (Node x xss) = x : concatMap flatten xss
并通过重用列表函数foldr
和foldl
前奏:
treeFoldr :: (a -> b -> b) -> b -> Tree a -> b
treeFoldr op e = foldr op e . flatten
treeFoldl :: (b -> a -> b) -> b -> Tree a -> b
treeFoldl op e = foldl op e . flatten
例如:
total = treeFoldl (+) 0
size = treeFoldr (const succ) 0
更多变化
根据您想要概括的概念,还有更多变体。例如,有人可能会争辩说,为了捕获结构递归,不需要/不希望在子森林上进行映射,最终会得到类似的东西
treeFold' :: (a -> c -> b) -> (c, b -> c -> c) -> Tree a -> b
treeFold' node (nil, cons) = h
where
h (Node x xss) = node x (foldr (cons . h) nil xss)
接着
total = treeFold' (+) (0, (+))
size = treeFold' (const succ) (0, (+))