即使你不想做双函子的事情,也有多个遍历顺序!我将讨论如何为一个无聊的旧仿函数做这件事;额外的类型参数也可以处理,但这样做会分散核心思想。所以这是我的无聊函子树类型:
data Tree x = Node x [Tree x]
进行广度优先遍历的传统方法是,作为中间步骤,生成一个列表列表。对于外部列表,树的每一层都有一个元素。像这样:
notQuiteBF :: Tree x -> [[x]]
notQuiteBF (Node x children) = [x] : (map concat . transpose . map notQuiteBF) children
那么实际的广度优先遍历就是这些列表的串联。
bf :: Tree x -> [x]
bf = concat . notQuiteBF
[x] 的优点在于它有足够的信息来迭代树中的值。不幸的是,知道如何重新排序来自多个孩子的遍历的信息还不够:我们知道第一个孩子的节点和第二个孩子的节点的广度优先排序,但我们不知道每个元素的深度是多少,所以我们不能把它们编织在一起。
某个聪明的家伙问了这个问题:如果我们只记得那个深度信息怎么办?所以在notQuiteBF中,我们使用了更丰富的结构。 [[x]] 的好处在于它有足够的信息来重新排序元素,即使我们是从本质上对树节点的深度优先访问来构建它的。不幸的是,如果我们需要重建树的形状,信息还不够:我们知道每个级别的元素序列是什么,但我们不知道每个元素与哪个父元素相关联。
所以现在我问:如果我们只记得那些额外的信息怎么办?方法如下:我们将返回 [[[x]]] 作为中间结构,而不是 [[x]]。和以前一样,外部列表每个深度有一个元素。下一层在前一个深度中每个节点有一个元素;最后一层具有与该父级关联的子级。
我们来看一个例子:
a
/ \
/ \
/ \
b c
/ \ |
d e f
| / \
g h i
对于这棵树,我们得到以下列表列表,带有提示性空格:
[[[a ]]
,[ [b ,c ]]
,[ [d ,e ],[f ]]
,[ [],[g ], [h ,i ]]
,[ [], [],[]]
]
嗯……要重新构建树,我们实际上更喜欢以相反的顺序。
[[[],[],[]]
,[[],[g],[h,i]]
,[[d,e],[f]]
,[[b,c]]
,[[a]]
]
我们先写重构算法。
rebuild :: [[[x]]] -> [Tree x]
rebuild = concat . go [] where
go trees [] = trees
go trees (xss:xsss) = go (weirdZipWith Node xss trees) xsss
weirdZipWith :: (x -> y -> z) -> [[x]] -> [y] -> [[z]]
weirdZipWith f [] _ = []
weirdZipWith f ([]:xss) ys = [] : weirdZipWith f xss ys
weirdZipWith f _ [] = []
weirdZipWith f ((x:xs):xss) (y:ys)
= let (b, e) = splitAt 1 (weirdZipWith f (xs:xss) ys)
in map (f x y:) b ++ e
在 ghci 中尝试一下:
> rebuild [["","",""],["","g","hi"],["de","f"],["bc"],["a"]]
[Node 'a' [Node 'b' [Node 'd' [],Node 'e' [Node 'g' []]],Node 'c' [Node 'f' [Node 'h' [],Node 'i' []]]]]
看起来不错。现在是另一个方向。与上面的notQuiteBF 略有不同。
bf :: Tree x -> [[[x]]]
bf (Node x children) = [[x]] : [concat (concat b)] : e where
(b, e) = splitAt 1 . map concat . transpose . map bf $ children
我们可以仔细检查我们的工作:
> quickCheck (\t -> (rebuild . reverse . bf) t == [t :: Tree Int])
+++ OK, passed 100 tests.
有了这些工具,编写Applicative 遍历非常容易:我们只需按正确顺序构建元素列表,在每个元素上调用f,同时保留列表结构,然后重建树。所以:
bfTraverse :: Applicative f => (x -> f y) -> Tree x -> f (Tree y)
bfTraverse f = id
. fmap (head . rebuild . reverse)
. traverse (traverse (traverse f))
. bf
(可能需要相当微妙的论据才能确信head 在这里是安全的!)在 ghci 中尝试一下:
> bfTraverse (\x -> putStrLn [x] >> pure (toUpper x)) (Node 'a' [Node 'b' [Node 'd' [],Node 'e' [Node 'g' []]],Node 'c' [Node 'f' [Node 'h' [],Node 'i' []]]])
a
b
c
d
e
f
g
h
i
Node 'A' [Node 'B' [Node 'D' [],Node 'E' [Node 'G' []]],Node 'C' [Node 'F' [Node 'H' [],Node 'I' []]]]