Wie stellen Sie ein Diagramm in Haskell dar?


125

Es ist einfach genug, einen Baum oder eine Liste in Haskell mithilfe algebraischer Datentypen darzustellen. Aber wie würden Sie vorgehen, um eine Grafik typografisch darzustellen? Es scheint, dass Sie Zeiger haben müssen. Ich vermute, Sie könnten so etwas haben

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

Und das wäre machbar. Es fühlt sich jedoch etwas entkoppelt an; Die Verknüpfungen zwischen verschiedenen Knoten in der Struktur "fühlen" sich nicht so solide an wie die Verknüpfungen zwischen den aktuellen vorherigen und nächsten Elementen in einer Liste oder den Eltern und untergeordneten Elementen eines Knotens in einem Baum. Ich habe die Vermutung, dass algebraische Manipulationen am Diagramm, wie ich es definiert habe, durch die durch das Tag-System eingeführte Indirektionsebene etwas behindert würden.

Vor allem dieses Gefühl des Zweifels und die Wahrnehmung der Uneleganz veranlassen mich, diese Frage zu stellen. Gibt es eine bessere / mathematisch elegantere Möglichkeit, Diagramme in Haskell zu definieren? Oder bin ich auf etwas gestoßen, das von Natur aus hart / grundlegend ist? Rekursive Datenstrukturen sind süß, aber dies scheint etwas anderes zu sein. Eine selbstreferenzielle Datenstruktur in einem anderen Sinne als Bäume und Listen selbstreferenziell. Es ist so, als ob Listen und Bäume auf Typebene selbstreferenziell sind, aber Diagramme sind auf Wertebene selbstreferenziell.

Also, was ist wirklich los?


12
Vielleicht interessiert Sie Martin Erwigs Artikel über funktionale Graph-Algorithmen: web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01 . Das fglPaket entwickelte sich daraus.
John L

Die Seite mit 99 Haskell-Problemen zeigt einige Beispiele für Diagramme, die in einem Problemlösungskontext verwendet werden. Es gibt auch ein kurzes Intro zu verschiedenen Darstellungen.
Dopamane

Antworten:


47

Ich finde es auch umständlich, Datenstrukturen mit Zyklen in einer reinen Sprache darzustellen. Es sind die Zyklen, die wirklich das Problem sind; Da Werte gemeinsam genutzt werden können, ist jedes ADT, das ein Mitglied des Typs (einschließlich Listen und Bäume) enthalten kann, tatsächlich eine DAG (Directed Acyclic Graph). Das grundlegende Problem ist, dass, wenn Sie Werte A und B haben, wobei A B und B A enthält, keiner von beiden erstellt werden kann, bevor der andere existiert. Weil Haskell faul ist, können Sie einen Trick verwenden, der als Binden des Knotens bekannt ist , um dies zu umgehen , aber das macht mein Gehirn weh (weil ich noch nicht viel davon getan habe). Ich habe bisher mehr von meiner umfangreichen Programmierung in Mercury als in Haskell gemacht, und Mercury ist streng, so dass das Binden von Knoten nicht hilft.

Wenn ich zuvor darauf gestoßen bin, habe ich normalerweise auf zusätzliche Indirektion zurückgegriffen, wie Sie vorschlagen. häufig durch Verwendung einer Zuordnung von IDs zu den tatsächlichen Elementen und durch Elemente, die Verweise auf die IDs anstelle auf andere Elemente enthalten. Die Hauptsache, die mir dabei nicht gefallen hat (abgesehen von der offensichtlichen Ineffizienz), ist, dass es sich fragiler anfühlte und die möglichen Fehler beim Nachschlagen einer nicht vorhandenen ID oder beim Versuch, dieselbe ID mehreren zuzuweisen, einführte Element. Sie können Code schreiben , so dass diese Fehler nicht auftreten, natürlich, und sogar verstecken sie hinter Abstraktionen , so dass die einzigen Orte , wo solche Fehler könnten sind begrenzt auftreten. Aber es ist noch eine Sache, etwas falsch zu machen.

Eine schnelle Google- Suche nach "Haskell-Grafik" führte mich jedoch zu http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , was nach einer lohnenden Lektüre aussieht.


62

In Shang's Antwort können Sie sehen, wie man ein Diagramm mit Faulheit darstellt. Das Problem bei diesen Darstellungen ist, dass sie sehr schwer zu ändern sind. Der Trick zum Verknüpfen von Knoten ist nur dann nützlich, wenn Sie ein Diagramm einmal erstellen möchten und es sich danach nie mehr ändert.

In der Praxis verwende ich die Fußgängerdarstellungen , wenn ich tatsächlich etwas mit meinem Diagramm tun möchte :

  • Kantenliste
  • Adjazenzliste
  • Geben Sie jedem Knoten eine eindeutige Bezeichnung, verwenden Sie die Bezeichnung anstelle eines Zeigers und behalten Sie eine endliche Zuordnung von Beschriftungen zu Knoten bei

Wenn Sie das Diagramm häufig ändern oder bearbeiten möchten, empfehle ich die Verwendung einer Darstellung basierend auf Huets Reißverschluss. Dies ist die Darstellung, die intern in GHC für Kontrollflussdiagramme verwendet wird. Sie können hier darüber lesen:


2
Ein weiteres Problem beim Binden des Knotens besteht darin, dass es sehr leicht ist, ihn versehentlich zu lösen und viel Platz zu verschwenden.
Hugomg

Mit Tuft's Website scheint (zumindest im Moment) etwas nicht zu stimmen, und keiner dieser Links funktioniert derzeit. Ich habe es geschafft, einige alternative Spiegel für diese zu finden: Ein anwendbares
-transformation

37

Wie Ben erwähnte, werden zyklische Daten in Haskell durch einen Mechanismus konstruiert, der "Binden des Knotens" genannt wird. In der Praxis bedeutet dies, dass wir gegenseitig rekursive Deklarationen mit letoder whereKlauseln schreiben , was funktioniert, weil die gegenseitig rekursiven Teile träge ausgewertet werden.

Hier ist ein Beispiel für einen Diagrammtyp:

import Data.Maybe (fromJust)

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

data Graph a = Graph [Node a]

Wie Sie sehen können, verwenden wir tatsächliche NodeReferenzen anstelle von Indirektion. Hier erfahren Sie, wie Sie eine Funktion implementieren, die das Diagramm aus einer Liste von Beschriftungszuordnungen erstellt.

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

Wir nehmen eine Liste von (nodeLabel, [adjacentLabel])Paaren auf und konstruieren die tatsächlichen NodeWerte über eine Zwischen-Lookup-Liste (die das eigentliche Knotenbinden durchführt). Der Trick ist, dass nodeLookupList(der den Typ hat [(a, Node a)]) mit konstruiert wird mkNode, was sich wiederum auf das bezieht nodeLookupList, um die benachbarten Knoten zu finden.


20
Sie sollten auch erwähnen, dass diese Datenstruktur keine Grafiken beschreiben kann. Es beschreibt nur ihre Entfaltungen. (unendliche Entfaltungen im endlichen Raum, aber immer noch ...)
Rotsor

1
Beeindruckend. Ich hatte nicht die Zeit, alle Antworten im Detail zu untersuchen, aber ich werde sagen, dass das Ausnutzen einer solchen faulen Bewertung so klingt, als würden Sie auf dünnem Eis skaten. Wie einfach wäre es, in eine unendliche Rekursion zu geraten? Immer noch großartiges Zeug und fühlt sich viel besser an als der Datentyp, den ich in der Frage vorgeschlagen habe.
TheIronKnuckle

@ TheIronKnuckle nicht zu viel Unterschied als die unendlichen Listen, die Haskellers die ganze Zeit verwenden :)
Justin L.

37

Es ist wahr, Graphen sind nicht algebraisch. Um dieses Problem zu lösen, haben Sie mehrere Möglichkeiten:

  1. Betrachten Sie anstelle von Diagrammen unendliche Bäume. Stellen Sie Zyklen im Diagramm als ihre unendlichen Entfaltungen dar. In einigen Fällen können Sie den als "Binden des Knotens" bekannten Trick verwenden (der in einigen der anderen Antworten hier gut erklärt wird), um diese unendlichen Bäume sogar im endlichen Raum darzustellen, indem Sie einen Zyklus im Haufen erstellen. Sie können diese Zyklen jedoch nicht in Haskell beobachten oder erkennen, was eine Vielzahl von Diagrammoperationen schwierig oder unmöglich macht.
  2. In der Literatur gibt es eine Vielzahl von Graphalgebren. Das erste, was mir in den Sinn kommt, ist die Sammlung von Diagrammkonstruktoren, die in Abschnitt 2 von Bidirektionalisierende Diagrammtransformationen beschrieben wird . Die übliche Eigenschaft, die durch diese Algebren garantiert wird, ist, dass jeder Graph algebraisch dargestellt werden kann; Kritisch gesehen haben viele Graphen jedoch keine kanonische Darstellung. Eine strukturelle Überprüfung der Gleichstellung reicht also nicht aus. Wenn man es richtig macht, läuft man darauf hinaus, einen Graphisomorphismus zu finden - bekanntermaßen ein schwieriges Problem.
  3. Geben Sie algebraische Datentypen auf; Stellen Sie die Knotenidentität explizit dar, indem Sie ihnen jeweils eindeutige Werte (z. B. Ints) geben und indirekt und nicht algebraisch auf sie verweisen. Dies kann erheblich komfortabler gestaltet werden, indem der Typ abstrakt gemacht und eine Schnittstelle bereitgestellt wird, die die Indirektion für Sie in Einklang bringt. Dies ist der Ansatz von z. B. fgl und anderen praktischen Grafikbibliotheken zu Hackage.
  4. Überlegen Sie sich einen brandneuen Ansatz, der genau zu Ihrem Anwendungsfall passt. Dies ist eine sehr schwierige Sache. =)

Jede der oben genannten Optionen hat Vor- und Nachteile. Wählen Sie die für Sie am besten geeignete aus.


"Sie werden diese Zyklen nicht in Haskell beobachten oder erkennen können" ist nicht genau richtig - es gibt eine Bibliothek, mit der Sie genau das tun können! Siehe meine Antwort.
Artelius

Grafiken sind jetzt algebraisch! hackage.haskell.org/package/algebraic-graphs
Josh.F

16

Einige andere haben kurz fglMartin Erwigs induktive Graphen und funktionale Graphenalgorithmen erwähnt , aber es lohnt sich wahrscheinlich, eine Antwort zu schreiben, die tatsächlich einen Eindruck von den Datentypen hinter dem Ansatz der induktiven Darstellung vermittelt.

Erwig stellt in seiner Arbeit folgende Typen vor:

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

(Die Darstellung in fglist etwas anders und verwendet Typklassen - aber die Idee ist im Wesentlichen dieselbe.)

Erwig beschreibt einen Multigraph, in dem Knoten und Kanten Beschriftungen haben und in dem alle Kanten gerichtet sind. A Nodehat eine Art Etikett a; Eine Kante hat eine Art Etikett b. A Contextist einfach (1) eine Liste von markierten Kanten, die auf einen bestimmten Knoten zeigen, (2) der betreffende Knoten, (3) die Beschriftung des Knotens und (4) die Liste von markierten Kanten, die vom Knoten zeigen. A Graphkann dann induktiv entweder als Emptyoder als ContextVerschmelzung (mit &) mit einem Bestehenden verstanden werden Graph.

Wie Erwig Notizen können wir erzeugen nicht frei eine Graphmit Emptyund &, wie wir eine Liste mit der generieren könnte Consund NilKonstrukteuren oder eine Treemit Leafund Branch. Im Gegensatz zu Listen (wie andere bereits erwähnt haben) wird es auch keine kanonische Darstellung von a geben Graph. Dies sind entscheidende Unterschiede.

Was diese Darstellung jedoch so leistungsfähig und den typischen Haskell-Darstellungen von Listen und Bäumen so ähnlich macht, ist, dass der GraphDatentyp hier induktiv definiert wird . Die Tatsache, dass eine Liste induktiv definiert ist, ermöglicht es uns, die Musterübereinstimmung so prägnant zu gestalten, ein einzelnes Element zu verarbeiten und den Rest der Liste rekursiv zu verarbeiten. Ebenso ermöglicht es Erwigs induktive Darstellung, einen Graphen einzeln rekursiv zu verarbeiten Context. Diese Darstellung eines Diagramms eignet sich für eine einfache Definition einer Methode zum Abbilden eines Diagramms ( gmap) sowie zum Durchführen ungeordneter Falten über Diagramme ( ufold).

Die anderen Kommentare auf dieser Seite sind großartig. Der Hauptgrund, warum ich diese Antwort geschrieben habe, ist jedoch, dass ich beim Lesen von Sätzen wie "Diagramme sind nicht algebraisch" befürchte, dass einige Leser unweigerlich den (falschen) Eindruck erwecken, dass niemand einen guten Weg gefunden hat, Diagramme darzustellen in Haskell auf eine Weise, die es ermöglicht, Muster auf ihnen abzugleichen, sie zuzuordnen, sie zu falten oder im Allgemeinen die coolen, funktionalen Dinge zu tun, die wir von Listen und Bäumen gewohnt sind.


14

Ich mochte immer Martin Erwigs Ansatz in "Induktive Graphen und funktionale Graphenalgorithmen", den Sie hier lesen können . FWIW, ich habe auch einmal eine Scala-Implementierung geschrieben, siehe https://github.com/nicolast/scalagraphs .


3
Um dies sehr grob zu erweitern, erhalten Sie einen abstrakten Diagrammtyp, auf dem Sie Musterübereinstimmungen vornehmen können. Der notwendige Kompromiss, damit dies funktioniert, besteht darin, dass die genaue Art und Weise, wie ein Diagramm zerlegt werden kann, nicht eindeutig ist, sodass das Ergebnis einer Musterübereinstimmung implementierungsspezifisch sein kann. In der Praxis ist das keine große Sache. Wenn Sie neugierig sind, mehr darüber zu erfahren, habe ich einen einführenden Blog-Beitrag geschrieben, der vielleicht gelesen werden könnte.
Tikhon Jelvis

Ich werde mir die Freiheit nehmen und Tikhons nettes Gespräch über dieses begriffs.com/posts/2015-09-04-pure-functional-graphs.html posten .
Martin Capodici

5

Jede Diskussion über die Darstellung von Graphen in Haskell erfordert die Erwähnung der Data-Reify-Bibliothek von Andy Gill (hier ist das Papier ).

Mit der Darstellung im "Tying-the-Knot" -Stil können sehr elegante DSLs erstellt werden (siehe Beispiel unten). Die Datenstruktur ist jedoch von begrenztem Nutzen. Gill's Bibliothek bietet Ihnen das Beste aus beiden Welten. Sie können eine DSL-Funktion zum Verknüpfen des Knotens verwenden, dann aber das zeigerbasierte Diagramm in ein beschriftungsbasiertes Diagramm konvertieren, damit Sie die Algorithmen Ihrer Wahl darauf ausführen können.

Hier ist ein einfaches Beispiel:

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

Um den obigen Code auszuführen, benötigen Sie die folgenden Definitionen:

{-# 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)

Ich möchte betonen, dass dies ein vereinfachtes DSL ist, aber der Himmel ist die Grenze! Ich habe ein sehr funktionsfähiges DSL entworfen, einschließlich einer schönen baumartigen Syntax, mit der ein Knoten einen Anfangswert an einige seiner untergeordneten Knoten sendet, und vielen praktischen Funktionen zum Erstellen bestimmter Knotentypen. Natürlich waren der Node-Datentyp und die mapDeRef-Definitionen viel komplizierter.


2

Ich mag diese Implementierung eines Diagramms von hier

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.