您如何在Haskell中表示图?


125

使用代数数据类型很容易在haskell中表示树或列表。但是,您将如何以印刷方式表示图形?看来您需要指针。我猜你可能会喜欢

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

那将是可行的。但是,感觉有点分离。结构中不同节点之间的链接并不像列表中当前的上一个元素和下一个元素之间的链接或树中节点的父级和子级之间的链接那样“牢固”。我有一种预感,即在我定义的图形上进行代数运算会受到标记系统引入的间接级别的影响。

正是这种怀疑和对自卑的感觉使我提出了这个问题。在Haskell中是否存在一种更好/更数学上优雅的图形定义方法?还是我偶然发现了一些本来就困难/基本的东西?递归数据结构很不错,但这似乎是另外一回事。与树和列表如何自引用不同意义上的自引用数据结构。就像列表和树在类型级别是自引用的,而图在值级别是自引用的。

那么到底是怎么回事?


12
您可能对Martin Erwig关于功能图算法的论文感兴趣:web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01。该fgl包开发的这一点。
约翰L

99只哈斯克尔问题页面显示了解决问题的上下文中使用图形的一些例子。它还简要介绍了不同的表示形式。
dopamane's

Answers:


47

我还发现尝试用纯语言用循环来表示数据结构很尴尬。真正的问题是周期。因为可以共享值,所以可以包含该类型成员(包括列表和树)的任何ADT实际上是DAG(有向无环图)。根本的问题是,如果您具有值A和B,且A包含B,而B包含A,那么这两个值都不能在另一个值存在之前创建。因为Haskell很懒,所以您可以使用一种称为“ 打结”的技巧来解决此问题,但这会使我的大脑受伤(因为我还没有做很多事情)。到目前为止,我在Mercury中进行的大量编程工作比Haskell多,而且Mercury严格,因此打结无济于事。

正如您所建议的那样,通常在遇到附加间接访问之前,我会遇到此问题。通常是使用从ID到实际元素的映射,并让元素包含对ID的引用,而不是对其他元素的引用。我不喜欢这样做的主要事情(除了明显的低效率)是感觉更加脆弱,引入了查找不存在的id或试图将同一个id分配给多个id的可能错误。元件。您可以编写代码,这样就不会发生这些错误,甚至可以将其隐藏在抽象后面,从而限制可能发生此类错误的唯一位置。但这仍然是错误的另一件事。

但是,一个用于“ Haskell图”的快速Google使我进入了http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling,这看起来很值得一读。


62

在shang的答案中,您可以看到如何使用懒度表示图。这些表示的问题在于它们很难更改。打结技巧仅在您要一次构建图形并且此后永不更改的情况下才有用。

在实践中,应该其实我是想这样做的东西与我的图表,我用的是比较平淡的表示:

  • 边列表
  • 邻接表
  • 给每个节点一个唯一的标签,使用标签代替指针,并保持从标签到节点的有限映射

如果您要经常更改或编辑图形,建议您使用基于Huet拉链的表示形式。这是GHC内部用于控制流图的表示。你可以在这里读到它:


2
打结的另一个问题是,很容易意外解开结并浪费大量空间。
hugomg 2012年

塔夫脱(Tuft)的网站似乎存在问题(至少目前是这样),而且这些链接目前都无法正常工作。我设法找到了一些替代的镜像:基于Huet的Zipper的应用控制流图Hoopl:用于数据流分析和转换的模块化,可重用的库
gntskn 2016年

37

正如Ben所提到的,Haskell中的循环数据是通过一种称为“打结”的机制构造的。实际上,这意味着我们使用letor where子句来编写相互递归的声明,这是有效的,因为相互递归的部分是惰性计算的。

这是一个示例图形类型:

import Data.Maybe (fromJust)

data Node a = Node
    { label    :: a
    , adjacent :: [Node a]
    }

data Graph a = Graph [Node a]

如您所见,我们使用实际Node引用而不是间接引用。以下是实现从标签关联列表构建图形的函数的方法。

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类型(类型为[(a, Node a)]mkNode,而该引用又返回nodeLookupList来查找相邻节点。


20
您还应该提到,此数据结构无法描述图形。它仅描述了它们的发展。(在有限空间中无限展开,但仍然...)
Rotsor 2012年

1
哇。我没有时间详细检查所有答案,但是我会说,利用像这样的惰性评估听起来像是在溜冰上滑冰。陷入无限递归有多容易?还是很棒的东西,并且感觉比我在问题中提出的数据类型好得多。
TheIronKnuckle 2012年

@TheIronKnuckle与Haskellers一直使用的无限列表没有太大区别:)
Justin L.

37

的确,图不是代数的。要解决此问题,您有两种选择:

  1. 代替图,考虑无限树。在图中将循环表示为无限展开。在某些情况下,可以通过在堆中创建循环来使用称为“打结”的技巧(在此处的其他一些答案中也有很好的解释)来甚至在有限空间中表示这些无限树。但是,您将无法在Haskell中观察或检测到这些循环,这将使各种图形操作变得困难或不可能。
  2. 文献中有各种各样的图代数。首先想到的是图双向构造的第二部分中描述的图构造函数的集合。这些代数保证的通常特性是,任何图都可以用代数表示。但是,至关重要的是,许多图将不具有规范表示。因此,从结构上检查相等是不够的。正确地做到这一点归结为找到图的同构性-众所周知这是一个难题。
  3. 放弃代数数据类型;通过为它们赋予每个唯一值(例如Ints)并间接而不是代数地引用它们,从而明确表示节点身份。通过将类型抽象化并提供一个为您处理间接操作的接口,可以大大简化此操作。这是通过,例如,所采用的方法FGL和Hackage其他实际图形库。
  4. 提出一种完全适合您的用例的全新方法。这是一件非常困难的事情。=)

因此,上述每种选择都有其优缺点。选择最适合您的一个。


“您将无法在Haskell中观察或检测到这些循环”并不是完全正确的-有一个库可以让您做到这一点!看我的答案。
Artelius


16

其他一些人已经简要提到了fglMartin Erwig的归纳图和功能图算法,但是可能值得写一个答案,实际上给出归纳表示方法背后的数据类型。

Erwig在他的论文中提出以下类型:

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

(中的表示fgl形式略有不同,并且很好地利用了类型类-但思想本质上是相同的。)

Erwig正在描述一个多图,其中节点和边都有标签,并且所有边都指向其中。A Node具有某种类型的标签a;边缘有某种标签b。甲Context只是(1)指向标记边缘的列表,以一个特定的节点,(2)所讨论的节点,(3)的节点的标签,和(4)指向标记边缘的列表该节点。Graph然后可以将A 归纳为一个EmptyContext&现有的合并Graph

正如Erwig所言,我们不能随意生成Graphwith Empty&,因为我们可能会生成带有Consand Nil构造函数的列表,或者带有Treewith Leaf和的列表Branch。同样,与列表不同(正如其他人提到的那样),不会有任何规范的表示形式Graph。这些是至关重要的差异。

但是,使该表示如此强大且与列表和树的典型Haskell表示如此相似的原因在于,Graph这里的数据类型是归纳定义的。归纳定义列表的事实使我们可以如此简洁地在其上进行模式匹配,处理单个元素并递归处理列表的其余部分。同样,Erwig的归纳表示允许我们一次递归地处理一个图Context。图形的这种表示形式使其很容易定义对图形进行映射的方法(gmap),以及对图形进行无序折叠的方法(ufold)。

此页面上的其他评论都很棒。但是,我写这个答案的主要原因是,当我阅读诸如“图形不是代数”之类的短语时,我担心有些读者会不可避免地摆脱(错误的)印象,即没人能找到代表图形的好方法在Haskell中,这种方式允许对它们进行模式匹配,在其上进行映射,对其进行折叠,或者通常做一些我们用来处理列表和树的凉爽,功能性的工作。


14

我一直喜欢Martin Erwig在“归纳图和功能图算法”中的方法,您可以在此处阅读。FWIW,我也曾经写过一个Scala实现,请参阅https://github.com/nicolast/scalagraphs


3
为了对此进行非常粗略的扩展,它为您提供了一种抽象的图形类型,您可以在其上进行模式匹配。进行这项工作的必要折衷方案是,图的确切分解方法不是唯一的,因此模式匹配的结果可能是特定于实现的。实际上这没什么大不了的。如果您想了解更多信息,我写了一篇介绍性博客文章,可能会使您大为恼火。
Tikhon Jelvis,2014年

我要一个自由和张贴吉洪的谈,谈得很好abouit这个begriffs.com/posts/2015-09-04-pure-functional-graphs.html
马丁·卡波迪奇

5

在Haskell中对图形表示的任何讨论都需要提及Andy Gill的数据验证库本文为)。

“打结”样式表示可用于制作非常精美的DSL(请参见下面的示例)。但是,数据结构用途有限。吉尔(Gill)的图书馆可让您两全其美。您可以使用“打结” DSL,然后将基于指针的图形转换为基于标签的图形,以便可以在其上运行选择的算法。

这是一个简单的例子:

-- Graph we want to represent:
--    .----> a <----.
--   /               \
--  b <------------.  \
--   \              \ / 
--    `----> c ----> d

-- Code for the graph:
a = leaf
b = node2 a c
c = node1 d
d = node2 a b
-- 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 a b = 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,但这是极限!我设计了一个功能非常强大的DSL,包括一个漂亮的树状语法(用于使节点向其子节点广播初始值)以及许多用于构造特定节点类型的便利功能。当然,Node数据类型和mapDeRef定义要涉及得多。


2

我喜欢从这里拍摄的图形的这种实现

import Data.Maybe
import Data.Array

class Enum b => Graph a b | a -> b where
    vertices ::  a -> [b]
    edge :: a -> b -> b -> Maybe Double
    fromInt :: a -> Int -> b
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.