代数データ型を使用してhaskellでツリーまたはリストを表すのは簡単です。しかし、グラフを活版印刷でどのように表現しますか?ポインタが必要なようです。私はあなたが次のようなものを持つことができると思います
type Nodetag = String
type Neighbours = [Nodetag]
data Node a = Node a Nodetag Neighbours
そしてそれは実行可能です。ただし、少し分離されているように感じます。構造内の異なるノード間のリンクは、リスト内の現在の前の要素と次の要素、またはツリー内のノードの親と子の間のリンクほど堅実に「感じ」ません。定義したグラフで代数的操作を行うと、タグシステムを介して導入される間接性のレベルによって多少妨げられるという予感があります。
私がこの質問をするのは、主にこの疑念と無知の認識です。 Haskellでグラフを定義するより良い/より数学的にエレガントな方法はありますか?または、本質的に難しい/基本的な何かにつまずいたことがありますか?再帰的なデータ構造は便利ですが、これは別のようです。ツリーおよびリストの自己参照とは異なる意味での自己参照データ構造。リストやツリーは型レベルでは自己参照的ですが、グラフは値レベルでは自己参照的です。
それでは、実際に何が起こっているのでしょうか?
また、データ構造を純粋な言語のサイクルで表現しようとするのは厄介です。本当に問題なのはサイクルです。値を共有できるため、そのタイプのメンバー(リストおよびツリーを含む)を含むことができる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 に導きました。
商の答えでは、怠inessを使用してグラフを表現する方法を見ることができます。これらの表現の問題は、変更が非常に難しいことです。結び目を作るトリックは、一度グラフを作成する場合にのみ有用であり、その後グラフは変更されません。
実際には、グラフで何かdoしたい場合は、より歩行者の表現を使用します。
グラフを頻繁に変更または編集する場合は、Huetのジッパーに基づいた表現を使用することをお勧めします。これは、制御フローグラフ用にGHCで内部的に使用される表現です。あなたはそれについてここで読むことができます:
ベンが述べたように、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
を参照して隣接するものを見つけることです。ノード。
確かに、グラフは代数的ではありません。この問題に対処するには、いくつかのオプションがあります。
Int
s)をそれぞれ与え、代数的ではなく間接的にそれらを参照することにより、ノードIDを明示的に表します。これは、型を抽象化し、インダイレクションをジャグリングするインターフェイスを提供することにより、非常に便利になります。これは、たとえば fgl およびHackageのその他の実用的なグラフライブラリで採用されているアプローチです。したがって、上記の選択にはそれぞれ長所と短所があります。あなたに最適と思われるものを選択してください。
私はいつも「誘導グラフと関数グラフアルゴリズム」でのマーティンエルヴィヒのアプローチが好きでした。これは here と読むことができます。 FWIW、私はかつてScala実装も書きました。 https://github.com/nicolast/scalagraphs を参照してください。
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定義はより複雑でした。
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