这是带有标记节点和边的循环有向图的类型。
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
的签名中添加Ord
或foldMap
约束,这会阻止我的类型成为Foldable
的成员。
*顺便说一句,我们不能为Functor
编写NodeGraph
实例,因为它会破坏图中节点唯一标记的不变量。 (例如,fmap (const "foo")
会将每个节点重新标记为“foo”,尽管它们都有不同的边集!)我们可以(使用适当的newtype
)写一个Functor
但是,它会映射所有 edge 标签。
答案 0 :(得分:3)
目前很少有Foldable
法律,所以你可以做各种各样的事情。实际上,您可以编写几个不同的Foldable
实例,对应于不同的遍历顺序。 Foldable
法律描述了不同Foldable
成员之间的关系,如果类型也是Functor
,则还有与fold
,foldMap
和{fmap
相关的附加法律{1}}。
一些细节:关于foldMap
,foldl
,foldr
,sum
等之间关系的直接“法律”,只是说他们应该采取行动非常类似于他们的默认实现,除了严格。对于fold
,此法律为fold = foldMap id
。如果容器也是Functor
,则有一条法律规定您可以采用其他方式:foldMap f = fold . fmap f
。正如我所说,没有什么太令人兴奋了。
另一方面,我认为尝试将打结与独特的标签结合起来有点滑稽。我不确定你对此有何看法,或者它是否真的有意义。我认为,问题在于虽然共享导致图形在内存中表现出来,但这种共享并没有反映在语言中。在Haskell中,带有循环的图形看起来就像一个无限的树。事实上,对于不会(可能)将其变为无限树的循环图,您几乎无法做到。这就是为什么人们在第一时间使用像Data.Map
这样的东西来表示图形的原因 - 打结并不能提供图形结构的清晰视图。