Foldable是否应该只返回一次所有结果?

时间:2015-01-24 00:20:55

标签: haskell graph typeclass contract foldable

这是带有标记节点和边的循环有向图的类型。

import qualified Data.Map as M
import Data.Foldable
import Data.Monoid

data Node n e = N n [(e, Node n e)]  -- the node's label and its list of neighbors
newtype Graph n e = G (M.Map n (Node n e))

为了处理图形有一个循环的情况,它是possible来“打结”并在有限空间中创建无限递归图。

type GraphInput n e = M.Map n [(e, n)]

mkGraph :: Ord n => GraphInput n e -> Graph n e
mkGraph spec = G $ nodeMap
    where nodeMap = M.mapWithKey mkNode (makeConsistent spec)
          -- mkNode :: n -> [(e, n)] -> Node n e
          mkNode lbl edges = N lbl $ map getEdge edges
          -- We know that (!) can't fail because we ensured that
          -- all edges have a key in the map (see makeConsistent)
          getEdge (e, lbl) = (e, nodeMap ! lbl)

makeConsistent :: Ord n => GraphInput n e -> GraphInput n e
makeConsistent m = foldr addMissing m nodesLinkedTo
    where addMissing el m = M.insertWith (\_ old -> old) el [] m
          nodesLinkedTo = map snd $ join $ M.elems m

通过将图形视为节点集合,我们可以编写一个执行深度优先遍历的Foldable实例。*

newtype NodeGraph e n = NG {getNodeGraph :: Graph n e}

instance Foldable (NodeGraph e) where
    foldMap f (NG (G m)) = foldMap mapNode (M.elems m)
        where mapNode (N n es) = f n `mappend` foldMap mapEdge es
              mapEdge (e, n) = mapNode n

然而,即使对于简单的树形图,这也会产生重复的元素:

--   A
--  / \    X
-- B   C
--     |
--     D
ghci> let ng = NG $ mkGraph [('A', [(1, 'B'), (1, 'C')]), ('C', [(1, 'D')]), ('X', [])]
ghci> let toList = Data.Foldable.foldr (:) []
ghci> toList ng
"ABCDBCDDX"

当图形有一个循环时,效果更加显着 - foldMap永远递归!循环中的项目会重复,并且永远不会返回一些元素!

这可以吗? Foldable的实例可以不止一次地返回它的某些元素,还是我违反了该类的合同?实例可以无限循环结构的一部分吗?我一直在寻找关于这个问题的指导 - 我希望有一套'可折叠的法律'来解决这个问题 - 但是我无法在网上找到任何关于这个问题的讨论。


摆脱这种情况的一种方法是在我遍历图表时“记住”已经访问过的元素。但是,这会在Eq的签名中添加OrdfoldMap约束,这会阻止我的类型成为Foldable的成员。

*顺便说一句,我们不能为Functor编写NodeGraph实例,因为它会破坏图中节点唯一标记的不变量。 (例如,fmap (const "foo")会将每个节点重新标记为“foo”,尽管它们都有不同的边集!)我们可以(使用适当的newtype)写一个Functor但是,它会映射所有 edge 标签。

1 个答案:

答案 0 :(得分:3)

目前很少有Foldable法律,所以你可以做各种各样的事情。实际上,您可以编写几个不同的Foldable实例,对应于不同的遍历顺序。 Foldable法律描述了不同Foldable成员之间的关系,如果类型也是Functor,则还有与foldfoldMap和{fmap相关的附加法律{1}}。

一些细节:关于foldMapfoldlfoldrsum等之间关系的直接“法律”,只是说他们应该采取行动非常类似于他们的默认实现,除了严格。对于fold,此法律为fold = foldMap id。如果容器也是Functor,则有一条法律规定您可以采用其他方式:foldMap f = fold . fmap f。正如我所说,没有什么太令人兴奋了。

另一方面,我认为尝试将打结与独特的标签结合起来有点滑稽。我不确定你对此有何看法,或者它是否真的有意义。我认为,问题在于虽然共享导致图形在内存中表现出来,但这种共享并没有反映在语言中。在Haskell中,带有循环的图形看起来就像一个无限的树。事实上,对于不会(可能)将其变为无限树的循环图,您几乎无法做到。这就是为什么人们在第一时间使用像Data.Map这样的东西来表示图形的原因 - 打结并不能提供图形结构的清晰视图。