Come rappresenti un grafico in Haskell?


125

È abbastanza facile rappresentare un albero o un elenco in haskell utilizzando tipi di dati algebrici. Ma come rappresenteresti tipograficamente un grafico? Sembra che tu abbia bisogno di suggerimenti. Immagino che potresti avere qualcosa del genere

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

E sarebbe fattibile. Tuttavia sembra un po 'disaccoppiato; I collegamenti tra i diversi nodi nella struttura non "sembrano" così solidi come i collegamenti tra gli elementi precedenti e successivi correnti in un elenco, o i genitori e i figli di un nodo in un albero. Ho la sensazione che fare manipolazioni algebriche sul grafico come l'ho definito sarebbe in qualche modo ostacolato dal livello di indiretto introdotto attraverso il sistema di tag.

È principalmente questa sensazione di dubbio e la percezione di ineleganza che mi fa porre questa domanda. Esiste un modo migliore / più matematicamente elegante per definire i grafici in Haskell? O mi sono imbattuto in qualcosa di intrinsecamente difficile / fondamentale? Le strutture dati ricorsive sono carine, ma questo sembra essere qualcos'altro. Una struttura dati autoreferenziale in un senso diverso dal modo in cui alberi ed elenchi sono autoreferenziali. È come se gli elenchi e gli alberi fossero autoreferenziali a livello di tipo, ma i grafici sono autoreferenziali a livello di valore.

Allora cosa sta succedendo veramente?


12
Potrebbe interessarti l'articolo di Martin Erwig sugli algoritmi dei grafi funzionali: web.engr.oregonstate.edu/~erwig/papers/abstracts.html#JFP01 . Ilfgl pacchetto si è sviluppato da questo.
John L

La pagina dei 99 problemi Haskell mostra alcuni esempi di grafici utilizzati in un contesto di risoluzione dei problemi. Ha anche una breve introduzione su diverse rappresentazioni.
dopamane

Risposte:


47

Trovo anche scomodo cercare di rappresentare strutture di dati con cicli in un linguaggio puro. Sono i cicli che sono davvero il problema; poiché i valori possono essere condivisi qualsiasi ADT che può contenere un membro del tipo (inclusi elenchi e alberi) è in realtà un DAG (grafico aciclico diretto). La questione fondamentale è che se si hanno valori A e B, con A contenente B e B contenente A, nessuno dei due può essere creato prima che esista l'altro. Poiché Haskell è pigro, puoi usare un trucco noto come Tying the Knot per aggirare questo problema, ma questo mi fa male il cervello (perché non ne ho ancora fatto molto). Finora ho fatto più della mia programmazione sostanziale in Mercury che in Haskell, e Mercury è rigoroso, quindi legare i nodi non aiuta.

Di solito quando mi sono imbattuto in questo prima ho solo fatto ricorso a ulteriori riferimenti indiretti, come stai suggerendo; spesso utilizzando una mappa dagli id ​​agli elementi effettivi e facendo in modo che gli elementi contengano riferimenti agli id ​​invece che ad altri elementi. La cosa principale che non mi è piaciuta di farlo (a parte l'ovvia inefficienza) è che sembrava più fragile, introducendo i possibili errori di cercare un ID che non esiste o provare ad assegnare lo stesso ID a più di uno elemento. Puoi scrivere codice in modo che questi errori non si verifichino, ovviamente, e persino nasconderlo dietro astrazioni in modo che gli unici punti in cui potrebbero verificarsi tali errori siano delimitati. Ma è ancora un'altra cosa da sbagliare.

Tuttavia, un rapido google per "Haskell graph" mi ha portato a http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , che sembra una lettura utile.


62

Nella risposta di shang puoi vedere come rappresentare un grafico usando la pigrizia. Il problema con queste rappresentazioni è che sono molto difficili da cambiare. Il trucco del nodo è utile solo se hai intenzione di costruire un grafico una volta e successivamente non cambia mai.

In pratica, se dovessi effettivamente voler fare qualcosa con il mio grafico, utilizzo le rappresentazioni più pedonali:

  • Elenco dei bordi
  • Elenco di adiacenza
  • Assegna un'etichetta univoca a ciascun nodo, usa l'etichetta invece di un puntatore e mantieni una mappa finita dalle etichette ai nodi

Se hai intenzione di cambiare o modificare frequentemente il grafico, ti consiglio di utilizzare una rappresentazione basata sulla cerniera di Huet. Questa è la rappresentazione utilizzata internamente in GHC per i grafici del flusso di controllo. Puoi leggerlo qui:


2
Un altro problema con il nodo è che è molto facile scioglierlo accidentalmente e sprecare molto spazio.
hugomg

Qualcosa sembra essere sbagliato nel sito web di Tuft (almeno al momento) e nessuno di questi collegamenti funziona attualmente. Sono riuscito a trovare alcuni mirror alternativi per questi: un grafico del flusso di controllo applicativo basato su Zipper di Huet , Hoopl: una libreria modulare e riutilizzabile per l'analisi e la trasformazione del flusso di dati
gntskn

37

Come ha detto Ben, i dati ciclici in Haskell sono costruiti da un meccanismo chiamato "legare il nodo". In pratica, significa che scriviamo dichiarazioni reciprocamente ricorsive usando leto whereclausole, il che funziona perché le parti reciprocamente ricorsive vengono valutate pigramente.

Ecco un esempio di tipo di grafico:

import Data.Maybe (fromJust)

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

data Graph a = Graph [Node a]

Come puoi vedere, usiamo Noderiferimenti reali invece di riferimenti indiretti. Ecco come implementare una funzione che costruisce il grafico da un elenco di associazioni di etichette.

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

Prendiamo un elenco di (nodeLabel, [adjacentLabel])coppie e costruiamo i Nodevalori effettivi tramite una lista di ricerca intermedia (che fa il nodo effettivo). Il trucco è che nodeLookupList(che ha il tipo [(a, Node a)]) è costruito usando mkNode, che a sua volta rimanda al nodeLookupListper trovare i nodi adiacenti.


20
Dovresti anche ricordare che questa struttura di dati non è in grado di descrivere i grafici. Descrive solo il loro svolgimento. (dispiegamenti infiniti nello spazio finito, ma comunque ...)
Rotsor

1
Wow. Non ho avuto il tempo di esaminare tutte le risposte in dettaglio, ma dirò che sfruttare una valutazione pigra come questa suona come se stessi pattinando su ghiaccio sottile. Quanto sarebbe facile scivolare nella ricorsione infinita? Ancora cose fantastiche e si sente molto meglio del tipo di dati che ho proposto nella domanda.
TheIronKnuckle

@TheIronKnuckle non fa troppa differenza rispetto alle liste infinite che Haskellers usa sempre :)
Justin L.

37

È vero, i grafici non sono algebrici. Per affrontare questo problema, hai un paio di opzioni:

  1. Invece di grafici, considera alberi infiniti. Rappresenta i cicli nel grafico come i loro infiniti dispiegamenti. In alcuni casi, puoi usare il trucco noto come "legare il nodo" (spiegato bene in alcune delle altre risposte qui) per rappresentare anche questi alberi infiniti nello spazio finito creando un ciclo nell'heap; tuttavia, non sarai in grado di osservare o rilevare questi cicli dall'interno di Haskell, il che rende difficile o impossibile una serie di operazioni sui grafici.
  2. Ci sono una varietà di algebre di grafi disponibili in letteratura. La prima che mi viene in mente è la raccolta di costruttori di grafi descritti nella sezione due di Trasformazioni di grafi bidirezionali . La solita proprietà garantita da queste algebre è che qualsiasi grafo può essere rappresentato algebricamente; tuttavia, criticamente, molti grafici non avranno una rappresentazione canonica . Quindi controllare l'uguaglianza strutturalmente non è sufficiente; farlo correttamente si riduce a trovare l'isomorfismo del grafico, noto per essere un problema difficile.
  3. Abbandona i tipi di dati algebrici; rappresentano esplicitamente l'identità del nodo assegnando loro valori univoci (diciamo, Ints) e facendo riferimento ad essi indirettamente anziché algebricamente. Questo può essere reso molto più conveniente rendendo il tipo astratto e fornendo un'interfaccia che manipola l'indirizzamento per te. Questo è l'approccio adottato, ad esempio, da fgl e da altre pratiche librerie di grafici su Hackage.
  4. Trova un approccio completamente nuovo che si adatta esattamente al tuo caso d'uso. Questa è una cosa molto difficile da fare. =)

Quindi ci sono pro e contro per ciascuna delle scelte precedenti. Scegli quello che ti sembra migliore.


"non sarai in grado di osservare o rilevare questi cicli dall'interno di Haskell" non è esattamente vero - c'è una libreria che ti permette di fare proprio questo! Vedi la mia risposta.
Artelius

i grafici ora sono algebrici! hackage.haskell.org/package/algebraic-graphs
Josh.F

16

Alcuni altri hanno menzionato brevemente fgli grafici induttivi e gli algoritmi dei grafi funzionali di Martin Erwig , ma probabilmente vale la pena scrivere una risposta che dia effettivamente un'idea dei tipi di dati dietro l'approccio della rappresentazione induttiva.

Nel suo articolo, Erwig presenta i seguenti tipi:

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

(La rappresentazione in fglè leggermente diversa e fa buon uso delle classi di tipi, ma l'idea è essenzialmente la stessa.)

Erwig sta descrivendo un multigrafo in cui nodi e bordi hanno etichette e in cui tutti i bordi sono diretti. A Nodeha un'etichetta di qualche tipo a; un bordo ha un'etichetta di qualche tipo b. A Contextè semplicemente (1) un elenco di bordi etichettati che puntano a un particolare nodo, (2) il nodo in questione, (3) l'etichetta del nodo e (4) l'elenco dei bordi etichettati che puntano dal nodo. A Graphpuò quindi essere concepito induttivamente come uno Emptyo l'altro, o come un Contextfuso (con &) in un esistente Graph.

Come osserva Erwig, non possiamo generare liberamente un Graphcon Emptye &, poiché potremmo generare un elenco con i costruttori Conse Nil, o un Treecon Leafe Branch. Inoltre, a differenza degli elenchi (come altri hanno menzionato), non ci sarà alcuna rappresentazione canonica di un file Graph. Queste sono differenze cruciali.

Tuttavia, ciò che rende questa rappresentazione così potente, e così simile alle rappresentazioni Haskell tipiche di elenchi e alberi, è che il Graphtipo di dati qui è definito in modo induttivo . Il fatto che una lista sia definita in modo induttivo è ciò che ci permette di abbinare in modo così sintetico su di essa, elaborare un singolo elemento ed elaborare ricorsivamente il resto della lista; allo stesso modo, la rappresentazione induttiva di Erwig ci consente di elaborare ricorsivamente un grafico uno Contextalla volta. Questa rappresentazione di un grafico si presta a una semplice definizione di un modo per mappare su un graph ( gmap), così come un modo per eseguire pieghe non ordinate su graph ( ufold).

Gli altri commenti su questa pagina sono fantastici. Il motivo principale per cui ho scritto questa risposta, tuttavia, è che quando leggo frasi come "i grafici non sono algebrici", temo che alcuni lettori avranno inevitabilmente l'impressione (errata) che nessuno abbia trovato un modo carino per rappresentare i grafici in Haskell in un modo che consente la corrispondenza dei modelli su di essi, mappandoli, ripiegandoli o in generale facendo il tipo di cose interessanti e funzionali che siamo abituati a fare con elenchi e alberi.


14

Mi è sempre piaciuto l'approccio di Martin Erwig in "Grafici induttivi e algoritmi di grafi funzionali", che puoi leggere qui . FWIW, una volta ho scritto anche un'implementazione Scala, vedi https://github.com/nicolast/scalagraphs .


3
Per espandere questo aspetto in modo molto approssimativo, ti dà un tipo di grafico astratto su cui puoi trovare la corrispondenza. Il compromesso necessario per far funzionare questo è che il modo esatto in cui un grafico può essere scomposto non è univoco, quindi il risultato di un pattern match può essere specifico dell'implementazione. Non è un grosso problema in pratica. Se sei curioso di saperne di più, ho scritto un post introduttivo sul blog che potrebbe essere una lettura.
Tikhon Jelvis

Prenderò una libertà e post chiacchierata di Tikhon abouit questo begriffs.com/posts/2015-09-04-pure-functional-graphs.html .
Martin Capodici

5

Qualsiasi discussione sulla rappresentazione di grafici in Haskell necessita di una menzione della libreria data-reify di Andy Gill (ecco l'articolo ).

La rappresentazione in stile "legare il nodo" può essere utilizzata per creare DSL molto eleganti (vedere l'esempio sotto). Tuttavia, la struttura dei dati è di uso limitato. La libreria di Gill ti offre il meglio di entrambi i mondi. È possibile utilizzare un DSL "vincolante", ma poi convertire il grafico basato sul puntatore in un grafico basato sull'etichetta in modo da poter eseguire gli algoritmi preferiti su di esso.

Qui c'è un semplice esempio:

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

Per eseguire il codice precedente avrai bisogno delle seguenti definizioni:

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

Voglio sottolineare che questa è una DSL semplicistica, ma l'unico limite è il cielo! Ho progettato un DSL molto ricco di funzionalità, inclusa una bella sintassi ad albero per avere un nodo che trasmette un valore iniziale ad alcuni dei suoi figli e molte funzioni utili per costruire tipi di nodi specifici. Naturalmente, il tipo di dati Node e le definizioni mapDeRef erano molto più coinvolti.


2

Mi piace questa implementazione di un grafico preso da qui

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.