【问题标题】:Haskell idiom for stateful tree transformation用于状态树转换的 Haskell 成语
【发布时间】:2020-12-25 21:19:35
【问题描述】:
data Tree a b = Leaf a | Branch b [Tree a b]

给定一对函数f :: a -> a'g :: b -> b',我可以轻松地将Tree a b 转换为Tree a' b'

type Transform a b = a -> b
treeTransform :: Transform leaf leaf' ->
                 Transform branch branch' ->
                 Tree leaf branch ->
                 Tree leaf' branch'
treeTransform f _ (Leaf a) = Leaf (f a)
treeTransform f g (Branch b ts) = Branch (g b) (map (treeTransform f g) ts)

这棵树是一个双函子,上面的treeTransform只是一个bimap。没什么特别的。

现在当我需要通过fg 线程化状态时会发生什么?

type StatefulTransform s a a' = s -> a -> (s, a')

statefulTreetransform :: StatefulTransform state leaf leaf' ->
                         StatefulTransform state branch branch' ->
                         state ->
                         Tree leaf branch ->
                         Tree leaf' branch'

现在实现这个函数的方法不止一种,因为遍历树有不同的方法。

我可以使用深度优先遍历来实现转换,但是广度优先遍历是一个绊脚石。从树中提取数据到列表广度优先相对容易。转换提取的数据也很简单。但是如何将转换后的数据弯曲回原来的树形呢?

【问题讨论】:

标签: haskell


【解决方案1】:

即使你不想做双函子的事情,也有多个遍历顺序!我将讨论如何为一个无聊的旧仿函数做这件事;额外的类型参数也可以处理,但这样做会分散核心思想。所以这是我的无聊函子树类型:

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' []]]]

【讨论】:

    猜你喜欢
    • 2019-12-30
    • 1970-01-01
    • 2020-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-29
    • 1970-01-01
    • 2021-08-10
    相关资源
    最近更新 更多