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#JFP01fglパッケージには、このうち開発しました。
John L

99のHaskellの問題ページショーのコンテキストを問題解決に使用されるグラフの例をいくつか。また、さまざまな表現についての短い紹介もあります。
ドパマン

回答:


47

また、純粋な言語で循環するデータ構造を表現しようとするのも厄介です。本当に問題なのはサイクルです。値を共有できるため、タイプ(リストやツリーを含む)のメンバーを含むことができるすべてのADTは、実際にはDAG(有向非巡回グラフ)です。基本的な問題は、AとBの値があり、AがBを含み、BがAを含む場合、どちらも他方が存在する前に作成できないことです。Haskellは怠惰なので、これを回避するためにTying the Knotとして知られているトリックを使用できますが、それは私の脳を傷つけます(まだ私はそれの多くをしていないためです)。私はこれまで、HaskellよりもMercuryでの実質的なプログラミングを多く行ってきました。Mercuryは厳密なので、結び目を結ぶことは役に立ちません。

通常、私が追加の間接参照に頼る前にこれに遭遇したとき、あなたが示唆しているように; 多くの場合、IDから実際の要素へのマップを使用し、要素に他の要素ではなくIDへの参照を含めます。(明らかな非効率性は別として)それを行うことについて私が気に入らなかった主なことは、それがより壊れやすく感じられ、存在しないIDを検索したり、同じIDを複数に割り当てようとしたりする可能性のあるエラーが発生することです素子。もちろん、これらのエラーが発生しないようにコードを記述したり、抽象化の背後に隠したりして、そのようなエラー発生する可能性のある場所だけを制限することもできます。しかし、間違いを犯すことは、まだもう1つあります。

しかし、「Haskellグラフ」をすばやくグーグル検索すると、http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handlingにつながりました。


62

Shangの回答では、遅延を使用してグラフを表す方法を確認できます。これらの表現の問題は、変更が非常に難しいことです。結び目を結ぶトリックは、グラフを1度作成するだけで、その後は変更されない場合にのみ役立ちます。

実際に、グラフで実際に何かを実行したい場合は、より多くの歩行者表現を使用します。

  • エッジリスト
  • 隣接リスト
  • 各ノードに一意のラベルを付け、ポインターの代わりにラベルを使用し、ラベルからノードへの有限マップを保持します

グラフを頻繁に変更または編集する場合は、Huetのジッパーに基づく表現を使用することをお勧めします。これは、GHCで制御フローグラフに対して内部的に使用される表現です。あなたはそれについてここで読むことができます:


2
結び目を結ぶもう1つの問題は、誤って結び目をほどいて、多くのスペースを浪費することが非常に簡単なことです。
hugomg 2012年

タフトのWebサイトに問題があるようです(少なくとも現時点では)。これらのリンクはどちらも現在機能していません。私はこれらの代替ミラーをいくつか見つけることに成功しました:HuetのジッパーHooplに
gntskn

37

ベンが述べたように、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 '16 / 03/12

1
ワオ。すべての回答を詳細に検討する時間はありませんでしたが、このような遅延評価を利用すると、薄い氷の上でスケートをしているように聞こえます。無限の再帰に陥るのはどれほど簡単でしょうか?それでも素晴らしいものであり、私が質問で提案したデータ型よりもずっと気分が良いです。
TheIronKnuckle 2012年

@TheIronKnuckleは、Haskellersが常に使用する無限リストよりも大きな違いはありません:)
Justin L.

37

確かに、グラフは代数的ではありません。この問題に対処するには、いくつかのオプションがあります。

  1. グラフの代わりに、無限ツリーを検討してください。グラフのサイクルを無限の展開として表します。場合によっては、「結び目を結ぶ」というトリック(他の回答のいくつかで詳しく説明されています)を使用して、ヒープにサイクルを作成することにより、これらの無限ツリーを有限空間で表すことさえできます。ただし、Haskell内からこれらのサイクルを観察または検出できなくなり、さまざまなグラフ操作が困難または不可能になります。
  2. 文献にはさまざまなグラフ代数があります。最初に頭に浮かぶのは、グラフ変換双方向化のセクション2で説明されているグラフコンストラクターのコレクションです。これらの代数によって保証される通常の特性は、グラフを代数的に表現できることです。ただし、決定的に重要なのは、多くのグラフが正規表現を持たないことです。したがって、構造的に等しいことを確認するだけでは十分ではありません。これを正しく行うと、グラフの同型性を見つけることができます-難しい問題の一部として知られています。
  3. 代数的データ型をあきらめる; 一意の値(たとえばInts)を与え、代数的ではなく間接的にそれらを参照することにより、ノードのアイデンティティを明示的に表します。これは、型を抽象化し、間接参照を制御するインターフェースを提供することで、大幅に便利になります。これは、例えばfglHackageに関する他の実用的なグラフライブラリが採用しているアプローチです。
  4. ユースケースに正確に適合する、まったく新しいアプローチを考え出します。これは非常に難しいことです。=)

したがって、上記の選択肢にはそれぞれ長所と短所があります。あなたに最適と思われるものを選択してください。


「Haskell内からこれらのサイクルを観察または検出することはできません」というのは正確には当てはまりません。まさにそれを可能にするライブラリーがあります。私の答えを見てください。
Artelius

グラフは代数的です!hackage.haskell.org/package/algebraic-graphs
Josh.F

16

他にもfgl、Martin 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ます。Aは、Context単に(1)ポインティング標識されたエッジのリストである特定のノード、(2)当該ノード、(3)ノードのラベル、及び(4)ポインティング標識されたエッジのリストからノード。A Graphは、帰納的にEmpty、または既存のものへのContext(との&)マージとして考えることができGraphます。

Erwigが指摘しているように、andおよびコンストラクター、またはwith およびでリストを生成する可能性があるため、Graphwith Emptyおよびを自由に生成することはできません。また、リストとは異なり(他の人が述べたように)、の正規表現はありません。これらは決定的な違いです。&ConsNilTreeLeafBranchGraph

それにもかかわらず、この表現を非常に強力にし、リストとツリーの典型的なHaskell表現に非常に似ているのは、Graphここでのデータ型が帰納的に定義されていることです。リストが帰納的に定義されているという事実は、リストのパターンマッチングを簡潔に行い、単一の要素を処理し、リストの残りを再帰的に処理することを可能にするものです。同様に、Erwigの帰納的表現によりContext、一度に1つずつ再帰的にグラフを処理できます。このグラフの表現は、グラフをマップする方法(gmap)の簡単な定義と、グラフを順序付けずに折りたたむ方法()に役立ちますufold

このページの他のコメントは素晴らしいです。しかし、私がこの回答を書いた主な理由は、「グラフは代数的ではない」などのフレーズを読んだときに、グラフを表現するための良い方法が見当たらないという(誤った)印象を避けてしまう読者がいることを恐れているからです。 Haskellでは、パターンマッチング、それらへのマッピング、それらの折りたたみ、または一般的にリストやツリーで慣れているようなクールで機能的なことを実行できるようにしています。


14

「誘導グラフと関数グラフアルゴリズム」では、Martin Erwigのアプローチが好きでし。FWIW、私はかつてScala実装を書いたことがあります。https://github.com/nicolast/scalagraphsを参照してください


3
これを非常に大まかに拡張すると、パターンマッチングが可能な抽象的なグラフタイプが得られます。この作業を行うために必要な妥協点は、グラフを分解できる正確な方法が一意ではないため、パターン一致の結果が実装固有になる可能性があることです。実際には大したことではありません。あなたがそれについてもっと知りたいと思っているなら、私は読み物を怒らせるかもしれない紹介ブログ投稿を書きました。
Tikhon Jelvis 14


5

Haskellでグラフを表現することについての議論には、Andy Gillのdata-reifyライブラリ(ここに論文あります)への言及が必要です。

「結び目」スタイル表現を使用して、非常にエレガントなDSLを作成できます(以下の例を参照)。ただし、データ構造の用途は限られています。ギルのライブラリーは、両方の長所を備えています。「結び目」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を設計しました。もちろん、ノードのデータ型と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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.