web-dev-qa-db-ja.com

Haskellでグラフをどのように表現しますか?

代数データ型を使用してhaskellでツリーまたはリストを表すのは簡単です。しかし、グラフを活版印刷でどのように表現しますか?ポインタが必要なようです。私はあなたが次のようなものを持つことができると思います

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

そしてそれは実行可能です。ただし、少し分離されているように感じます。構造内の異なるノード間のリンクは、リスト内の現在の前の要素と次の要素、またはツリー内のノードの親と子の間のリンクほど堅実に「感じ」ません。定義したグラフで代数的操作を行うと、タグシステムを介して導入される間接性のレベルによって多少妨げられるという予感があります。

私がこの質問をするのは、主にこの疑念と無知の認識です。 Haskellでグラフを定義するより良い/より数学的にエレガントな方法はありますか?または、本質的に難しい/基本的な何かにつまずいたことがありますか?再帰的なデータ構造は便利ですが、これは別のようです。ツリーおよびリストの自己参照とは異なる意味での自己参照データ構造。リストやツリーは型レベルでは自己参照的ですが、グラフは値レベルでは自己参照的です。

それでは、実際に何が起こっているのでしょうか?

115
TheIronKnuckle

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

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

しかし、「Haskell graph」の簡単なグーグルは、私を http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling に導きました。

44
Ben

商の答えでは、怠inessを使用してグラフを表現する方法を見ることができます。これらの表現の問題は、変更が非常に難しいことです。結び目を作るトリックは、一度グラフを作成する場合にのみ有用であり、その後グラフは変更されません。

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

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

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

57
Norman Ramsey

ベンが述べたように、Haskellの循環データは「結び目」と呼ばれるメカニズムによって構築されます。実際には、let句または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を参照して隣接するものを見つけることです。ノード。

34
shang

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

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

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

32
Daniel Wagner

私はいつも「誘導グラフと関数グラフアルゴリズム」でのマーティンエルヴィヒのアプローチが好きでした。これは here と読むことができます。 FWIW、私はかつてScala実装も書きました。 https://github.com/nicolast/scalagraphs を参照してください。

14
Nicolas Trangez

Haskellでグラフを表現するための議論には、Andy Gillの data-reifyライブラリ (ここに 論文 )の言及が必要です。

「結び目」スタイルの表現を使用して、非常にエレガントな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であることを強調したいと思いますが、空が限界です!ノードがその子の一部に初期値をブロードキャストし、特定のノードタイプを構築するための多くの便利な関数を使用します。もちろん、Nodeデータ型とmapDeRef定義はより複雑でした。

3
Artelius

here から取得したグラフのこの実装が気に入っています

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
2
pyCthon