你如何在Haskell中表示graphics?

使用代数数据types在haskell中表示树或列表是很容易的。 但是,你将如何去印刷代表图表? 看来你需要有指针。 我猜你可能有类似的东西

type Nodetag = String type Neighbours = [Nodetag] data Node a = Node a Nodetag Neighbours 

这是可行的。 不过感觉有点分离; 结构中的不同节点之间的链接并不真正像列表中当前的前一个和下一个元素之间的链接或者树中的节点的父节点和子节点那样“感觉”固定。 我有一个预感,在图上进行代数操作,因为我定义它会受到通过标签系统引入的间接级别的阻碍。

主要是这种怀疑和不雅的感觉,使我提出这个问题。 Haskell有更好的/更好的math方法来定义图表吗? 还是我偶然发现了一些本质上很难/根本的东西? recursion数据结构是甜的,但这似乎是别的东西。 一个自我指涉的数据结构与树和列表是如何自我指涉的意义不同。 就像列表和树木在types层面上是自引用的,但graphics在值层面上是自引用的。

那么究竟发生了什么?

我也觉得试图以纯语言来表示具有循环的数据结构是很尴尬的。 这是真正的问题的循环; 因为值可以共享任何可以包含types成员(包括列表和树)的ADT实际上是一个DAG(定向非循环图)。 根本的问题是,如果你有值A和B,A包含B和B包含A,那么在另一个存在之前都不能创build。 因为Haskell是懒惰的,所以你可以使用一个叫做“ 绑结”的技巧来解决这个问题,但是这会让我的大脑受到伤害(因为我还没有做太多工作)。 到目前为止,我已经完成了比哈斯克尔更多的关于水星的大量编程,而水星是严格的,所以打结不起作用。

通常,在我刚刚采取额外的间接性之前,我已经遇到了这个问题,正如你所build议的; 通常通过使用从id到实际元素的映射,并且元素包含对id的引用而不是其他元素。 我不喜欢这样做的主要原因(除了显而易见的低效率之外)是感觉更加脆弱,引入了查找不存在的id或尝试将同一id分配给多个id的可能错误元件。 你可以编写代码,这样当然不会发生这些错误,甚至隐藏在抽象的后面,这样唯一可能出现这种错误的地方是有界的。 但是还有一件事是错的。

然而,“Haskell图”的一个快速的谷歌带我到http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling ,这看起来像一个值得一读。

在上面的答案中,你可以看到如何用懒惰表示一个图表。 这些陈述的问题是他们很难改变。 打结技巧是有用的,只有当你打算build立一个graphics,然后它永远不会改变。

实际上,如果我真的想用我的图表做些什么,我会用更多的行人表示:

  • 边缘列表
  • 邻接名单
  • 为每个节点提供一个唯一的标签,使用标签而不是指针,并保留从标签到节点的有限映射

如果您要频繁更改或编辑图表,我build议使用基于Huet拉链的表示。 这是在GHC内部用于控制stream图的表示。 你可以在这里阅读:

  • 基于Huet拉链的适用性控制stream程图

  • Hoopl:用于数据stream分析和转换的模块化可重用库

正如Ben所提到的,Haskell中的循环数据是由一个“绑结”的机制构build的。 实际上,这意味着我们使用letwhere子句来编写相互recursion的声明,这是因为相互recursion的部分被懒惰地评估。

这是一个图表types的例子:

 import Data.Maybe (fromJust) data Node a = Node { label :: a , adjacent :: [Node a] } data Graph a = Graph [Node a] 

如您所见,我们使用实际的Node引用而不是间接引用。 下面介绍如何实现一个从标签关联列表构造graphics的函数。

 mkGraph :: Eq a => [(a, [a])] -> Graph a mkGraph links = Graph $ map snd nodeLookupList where mkNode (lbl, adj) = (lbl, Node lbl $ map lookupNode adj) nodeLookupList = map mkNode links lookupNode lbl = fromJust $ lookup lbl nodeLookupList 

我们取一个(nodeLabel, [adjacentLabel])对的列表,并通过一个中间的查找列表(它实际上是结扎)来构造实际的Node值。 关键是nodeLookupList (其types为[(a, Node a)] )是使用mkNode构造的, mkNode又转回到nodeLookupList来查找相邻节点。

这是事实,图不是代数的。 为了解决这个问题,你有两个select:

  1. 考虑无限树,而不是图。 将图中的循环表示为其无限展开。 在某些情况下,你可以使用被称为“绑结”的技巧(在这里的一些其他答案中有很好的解释),甚至通过在堆中创build一个循环来在有限的空间中表示这些无限的树; 但是,您将无法从Haskell内部观察或检测这些循环,这使得各种graphics操作变得困难或不可能。
  2. 文献中有许多图代数。 首先想到的是双向图转换的第二部分描述的图构造器的集合。 这些代数保证的通常属性是任何图都可以用代数表示; 然而,批判地说,许多图不会有规范的表示。 所以在结构上检查平等是不够的。 这样做正确归结为find图同构 – 已知是一个难题。
  3. 放弃代数数据types; 通过给它们每个唯一的值(比如Int )并且间接地引用它们而不是代数来显式地表示节点的身份。 通过制作抽象types并为您提供一个为您提供间接接口的方法,这可以变得非常方便。 这是Hackage上的fgl和其他实用图库所采用的方法。
  4. 拿出一个全新的方法,完全符合你的用例。 这是一件非常困难的事情。 =)

所以对上述每个select都有利有弊。 select一个最适合你的人。

我一直很喜欢Martin Erwig在“归纳图和函数图algorithm”中的方法,你可以在这里阅读。 FWIW,我也曾经写过一个Scala实现,请参阅https://github.com/nicolast/scalagraphs

其他一些人已经简要地提到了fgl和Martin Erwig的归纳图和函数图algorithm ,但是可能值得写出一个答案,实际上给出归纳表示法背后的数据types。

Erwig在他的论文中提出了以下几种types:

 type Node = Int type Adj b = [(b, Node)] type Context ab = (Adj b, Node, a, Adj b) data Graph ab = Empty | Context ab & Graph ab 

(在fgl的表示略有不同,并且很好地使用了types类,但是这个想法本质上是一样的)。

Erwig正在描述一个多图,其中节点和边都有标签,并且所有的边都是指向的。 一个Node有一个typesa a的标签; 边缘有一些types的标签bContext简单地说是(1)指向特定节点的标记边的列表,(2)所讨论的节点,(3)节点的标签,以及(4) 节点指向的标记边的列表。 然后可以将一个Graph归纳为Empty或作为一个Context合并(与& )到一个现有的Graph

正如Erwig指出的那样,我们不能自由地使用Empty&生成一个Graph ,因为我们可能使用ConsNil构造函数生成一个列表,或者使用LeafBranch Tree 。 太不像列表(正如其他人所提到的),不会有任何一个Graph规范表示。 这些是至关重要的差异

尽pipe如此,这种表示如此强大,与典型的Haskell列表和树的表示如此相似,这里的Graph数据types是归纳定义的 。 事实上,列表是归纳定义的,它允许我们在它上简洁地匹配模式,处理一个元素,recursion处理列表的其余部分; 同样,Erwig的归纳表示允许我们一次recursion处理一个graphicsContext 。 这种graphics的表示方式本身就是一种简单的定义graphics( gmap )映射的方法,也是一种在graphics( ufold )上执行无序折叠的方法。

在这个页面上的其他评论是伟大的。 然而,我写这个答案的主要原因是,当我读到“图不是代数”这样的短语的时候,我担心有些读者会不可避免地带出一种(错误的)印象,就是没有人find代表图的好方法在Haskell中允许对它们进行模式匹配,映射它们,折叠它们,或者通常做一些我们习惯使用列表和树的很酷的function性的东西。

任何关于在Haskell中表示图的讨论都需要提到Andy Gill的数据库 文件 (这里是论文 )。

可以使用“打结”风格表示来制作非常优雅的DSL(参见下面的示例)。 但是,数据结构的使用是有限的。 吉尔的图书馆可以让你两全其美。 你可以使用“绑结”DSL,然后把基于指针的图转换成基于标签的图,这样你就可以运行你select的algorithm。

这是一个简单的例子:

 -- Graph we want to represent: -- .----> a <----. -- / \ -- b <------------. \ -- \ \ / -- `----> c ----> d -- Code for the graph: a = leaf b = node2 ac c = node1 d d = node2 ab -- Yes, it's that simple! -- If you want to convert the graph to a Node-Label format: main = do g <- reifyGraph b --can't use 'a' because not all nodes are reachable print g 

要运行上述代码,您需要以下定义:

 {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeFamilies #-} import Data.Reify import Control.Applicative import Data.Traversable --Pointer-based graph representation data PtrNode = PtrNode [PtrNode] --Label-based graph representation data LblNode lbl = LblNode [lbl] deriving Show --Convenience functions for our DSL leaf = PtrNode [] node1 a = PtrNode [a] node2 ab = PtrNode [a, b] -- This looks scary but we're just telling data-reify where the pointers are -- in our graph representation so they can be turned to labels instance MuRef PtrNode where type DeRef PtrNode = LblNode mapDeRef f (PtrNode as) = LblNode <$> (traverse f as) 

我想强调,这是一个简单的DSL,但天空的极限! 我devise了一个非常有特色的DSL,其中包括一个很好的树形语法,用于让一个节点向其子节点广播一个初始值,还有许多用于构造特定节点types的便利函数。 当然,Node数据types和mapDeRef定义涉及更多。

我喜欢从这里取得的这个图表的实现

 import Data.Maybe import Data.Array class Enum b => Graph ab | a -> b where vertices :: a -> [b] edge :: a -> b -> b -> Maybe Double fromInt :: a -> Int -> b