所以有几个不同的选择:
喜结连理
在 Haskell 中有很多方法可以对图形进行编码。最简单的方法是使用称为“打结”的过程在树数据结构中创建循环。例如,对这个图进行编码:
.--.
A -- B -- C -- D -./
| | | |
E -- F G -- H
| |
+--------------+
您可以简单地将节点写为其名称和子节点列表:
data Node = Node String [Node]
instance Eq Node where
Node a _ == Node b _ = a == b
my_graph = [a, b, c, d, e, f, g, h] where
a = Node "A" [b, e]
b = Node "B" [a, c, f]
c = Node "C" [b, d, g]
d = Node "D" [c, d, h]
e = Node "E" [a, f, h]
f = Node "F" [b, e]
g = Node "G" [c, h]
h = Node "H" [d, e, g]
这有很多方便:您现在可以像任何其他 Haskell 数据结构一样使用尾递归函数遍历数据结构。要终止循环,请在递归时将当前项目附加到 path 变量上,递归逻辑应该说的第一件事是 | nodeelempath = ... 以随心所欲地处理循环。
另一方面,您的次要一致性问题已经演变成非常棘手的一致性问题。例如考虑这两者之间的区别:
-- A has one child, which is itself; B has one child, which is A.
let a = Node "A" [a]; b = Node "B" [a] in [a, b]
-- this looks almost the same but if you descend far enough on B you find an
-- impostor node with the wrong children.
let a = Node "A" [a]
impostor = Node "A" [b]
b = Node "B" [Node "A" [Node "A" [impostor]]]
in [a, b]
所以这很糟糕,我唯一真正的答案是,“通过转换为以下之一来标准化......”。
无论如何,上面的技巧也被称为 mutual recursion 和 letrec,基本上意味着在 where 或 let 子句中,所有你放在那里的定义可以“看到对方”。这不是懒惰的属性;你也可以用严格的语言制作上述数据结构——但是函数式严格语言的语言设计理解这种方式的相互递归定义可能有点困难. (使用非函数式语言,您只需根据需要创建指针。)
显式命理
现在想一想您将如何获取我们上面的图表,并将其转换为您的表示形式。最简单的方法是通过包含Array 的中间人步骤:
import From.Above.Code (Node)
import Data.Array
type Graph = Array [Int]
graph :: [Node] -> Maybe Graph
graph nodes = fmap (array (1, length nodes)) . sequence $ map format nodes where
indices = zip nodes [1..]
pair x y = (x, y)
format node@(Node _ children) = do -- in the Maybe monad
index <- lookup node indices
index_list <- sequence $ map (flip lookup indices) children
return (index, index_list)
现在,一致性问题要少得多,现在都可以通过编程方式加以缓解。但是,如果您想以编程方式使用 State monad 创建这样的图,并且您希望暂时使数据结构处于不一致的状态,直到读取正确的节点,这些一致性问题可能会起到一定的作用。唯一的缺点是当你将图表写入文件时,它看起来有点难以理解,因为数字不如字符串友好:
array (1, 8) [
(1, [2, 5]),
(2, [1, 3, 6]),
(3, [2, 4, 7]),
(4, [3, 4, 8]),
(5, [1, 6, 8]),
(6, [2, 5]),
(7, [3, 8]),
(8, [4, 5, 7])]
您可以使用Map String [String] 来解决此问题,以权衡访问变为O(log n)。在任何情况下,您都应该了解该表示:您将希望转换为 IntMap [Int],然后在执行您提议的“完整性检查”时返回。
一旦你有了这些,事实证明你可以使用支持Array Int Node来创建递归[Node],如上:
nodesFromArray arr = nodes where
makeNode index children = Node (show index) [backingArray ! c | c <- children]
backingArray = array (bounds arr) [(i, makeNode i c) | (i, c) <- assocs arr]
nodes = map makeNode arr
边列表
一旦你有了上面的列表(Map.toList 或 Array.assocs),边列表就变得很容易了:
edges_from_array = concatMap . uncurry (fmap . pair) . assocs
另一面稍微复杂一些,可以直接完成你想要做的事情:
import Data.Map (Map)
import Data.Set (Set)
import qualified Data.Map as Map
import qualified Data.Set as Set
makeGraphMap vertices edges = add edges (Map.fromList $ blankGraph vertices) where
blankGraph verts = zip verts (repeat Set.empty)
setInsert x Nothing = Just $ Set.singleton x
setInsert x (Just set) = Just $ Set.insert x set
add [] graphMap = fmap Set.toList graphMap
add ((a, b) : es) graphMap = Map.alter (setInsert b) a verts
也就是说,我们使用将键映射到其子集的映射来遍历边列表;我们将其初始化为映射到空集的顶点列表(这样我们就可以在图中断开连接的单个节点),然后通过将值插入键的集合来遍历边,如果我们不创建该集合看钥匙。