web-dev-qa-db-fra.com

Comment représentez-vous un graphique dans Haskell?

Il est assez facile de représenter un arbre ou une liste dans haskell à l'aide de types de données algébriques. Mais comment procéder pour représenter typographiquement un graphique? Il semble que vous ayez besoin de pointeurs. Je suppose que tu pourrais avoir quelque chose comme

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

Et ce serait réalisable. Cependant, cela semble un peu découplé; Les liens entre les différents nœuds de la structure ne "se sentent" pas aussi solides que les liens entre les éléments précédents et suivants actuels dans une liste, ou les parents et les enfants d'un nœud dans une arborescence. J'ai l'impression que faire des manipulations algébriques sur le graphique tel que je l'ai défini serait quelque peu gêné par le niveau d'indirection introduit par le système de balises.

C'est avant tout ce sentiment de doute et cette perception d'inélégance qui me font poser cette question. Existe-t-il une manière meilleure/plus élégante mathématiquement de définir des graphiques dans Haskell? Ou suis-je tombé sur quelque chose d'intrinsèquement difficile/fondamental? Les structures de données récursives sont douces, mais cela semble être autre chose. Une structure de données auto-référentielle dans un sens différent de la façon dont les arbres et les listes sont auto-référentiels. C'est comme si les listes et les arbres sont auto-référentiels au niveau du type, mais les graphiques sont auto-référentiels au niveau de la valeur.

Alors qu'est-ce qui se passe vraiment?

115
TheIronKnuckle

Je trouve également gênant d'essayer de représenter des structures de données avec des cycles dans un langage pur. Ce sont les cycles qui sont vraiment le problème; parce que les valeurs peuvent être partagées, tout ADT pouvant contenir un membre du type (y compris les listes et les arbres) est en réalité un DAG (Directed Acyclic Graph). Le problème fondamental est que si vous avez des valeurs A et B, avec A contenant B et B contenant A, aucune ne peut être créée avant que l'autre existe. Parce que Haskell est paresseux, vous pouvez utiliser une astuce connue sous le nom de Tying the Knot pour contourner cela, mais cela me fait mal au cerveau (parce que je ne l'ai pas encore fait beaucoup). J'ai fait plus de ma programmation substantielle dans Mercury que Haskell jusqu'à présent, et Mercury est strict, donc les nœuds ne sont pas utiles.

Habituellement, lorsque j'ai rencontré cela auparavant, je viens de recourir à une indirection supplémentaire, comme vous le suggérez; souvent en utilisant un mappage des identifiants aux éléments réels, et en ayant des éléments contenant des références aux identifiants au lieu d'autres éléments. La principale chose que je n'ai pas aimé faire (à part l'inefficacité évidente) est qu'elle se sentait plus fragile, introduisant les erreurs possibles de recherche d'un identifiant qui n'existe pas ou d'essayer d'attribuer le même identifiant à plusieurs. élément. Vous pouvez bien sûr écrire du code afin que ces erreurs ne se produisent pas et même le cacher derrière des abstractions afin que les seuls endroits où de telles erreurs puissent se produire sont délimités. Mais c'est encore une chose de se tromper.

Cependant, un rapide google pour "Haskell graph" m'a conduit à http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , ce qui ressemble à une lecture intéressante.

44
Ben

Dans la réponse de shang, vous pouvez voir comment représenter un graphique en utilisant la paresse. Le problème avec ces représentations est qu'elles sont très difficiles à changer. L'astuce de nouage n'est utile que si vous allez créer un graphique une fois, et ensuite il ne change jamais.

En pratique, si je veux réellement faire quelque chose avec mon graphique, j'utilise les représentations les plus piétonnes:

  • Liste des bords
  • Liste d'adjacence
  • Donnez une étiquette unique à chaque nœud, utilisez l'étiquette au lieu d'un pointeur et conservez une carte finie des étiquettes aux nœuds

Si vous allez changer ou éditer le graphique fréquemment, je recommande d'utiliser une représentation basée sur la fermeture éclair de Huet. Il s'agit de la représentation utilisée en interne dans GHC pour les graphiques de flux de contrôle. Vous pouvez lire à ce sujet ici:

57
Norman Ramsey

Comme Ben l'a mentionné, les données cycliques dans Haskell sont construites par un mécanisme appelé "attacher le nœud". En pratique, cela signifie que nous écrivons des déclarations mutuellement récursives en utilisant des clauses let ou where, ce qui fonctionne car les parties mutuellement récursives sont évaluées paresseusement.

Voici un exemple de type de graphique:

import Data.Maybe (fromJust)

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

data Graph a = Graph [Node a]

Comme vous pouvez le voir, nous utilisons des références réelles Node au lieu d'indirection. Voici comment implémenter une fonction qui construit le graphique à partir d'une liste d'associations d'étiquettes.

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

Nous prenons une liste de paires (nodeLabel, [adjacentLabel]) Et construisons les valeurs réelles de Node via une liste de recherche intermédiaire (qui fait le nœud réel). L'astuce est que nodeLookupList (qui a le type [(a, Node a)]) est construit en utilisant mkNode, qui à son tour se réfère à nodeLookupList pour trouver le adjacent nœuds.

34
shang

C'est vrai, les graphiques ne sont pas algébriques. Pour faire face à ce problème, vous avez deux options:

  1. Au lieu de graphiques, considérez les arbres infinis. Représentez les cycles dans le graphique comme leurs dépliages infinis. Dans certains cas, vous pouvez utiliser l'astuce connue sous le nom de "nouer le nœud" (bien expliqué dans certaines des autres réponses ici) pour même représenter ces arbres infinis dans un espace fini en créant un cycle dans le tas; cependant, vous ne pourrez pas observer ou détecter ces cycles depuis Haskell, ce qui rend une variété d'opérations graphiques difficiles ou impossibles.
  2. Il existe une variété d'algèbres graphiques disponibles dans la littérature. Celui qui me vient à l'esprit en premier est la collection de constructeurs de graphes décrite dans la section deux de Transformations graphiques bidirectionnelles . La propriété habituelle garantie par ces algèbres est que tout graphe peut être représenté algébriquement; cependant, de manière critique, de nombreux graphiques n'auront pas de représentation canonique . Il ne suffit donc pas de vérifier structurellement l'égalité; le faire correctement se résume à trouver l'isomorphisme des graphes - connu pour être quelque chose d'un problème difficile.
  3. Abandonnez les types de données algébriques; représenter explicitement l'identité du nœud en leur donnant chacune des valeurs uniques (disons Ints) et en s'y référant indirectement plutôt qu'algébriquement. Cela peut être rendu beaucoup plus pratique en rendant le type abstrait et en fournissant une interface qui jongle avec l'indirection pour vous. C'est l'approche adoptée par exemple par fgl et d'autres bibliothèques de graphes pratiques sur Hackage.
  4. Trouvez une toute nouvelle approche qui correspond exactement à votre cas d'utilisation. C'est une chose très difficile à faire. =)

Il y a donc des avantages et des inconvénients à chacun des choix ci-dessus. Choisissez celui qui vous convient le mieux.

32
Daniel Wagner

Quelques autres ont brièvement mentionné fgl et Martin Erwig Inductive Graphs and Functional Graph Algorithms , mais cela vaut probablement la peine d'écrire une réponse qui donne en fait une idée des types de données derrière la représentation inductive approche.

Dans son article, Erwig présente les types suivants:

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 représentation dans fgl est légèrement différente et fait bon usage des classes de caractères - mais l'idée est essentiellement la même.)

Erwig décrit un multigraphe dans lequel les nœuds et les bords ont des étiquettes et dans lequel tous les bords sont dirigés. Un Node a une étiquette d'un certain type a; un Edge a une étiquette d'un certain type b. Un Context est simplement (1) une liste de bords étiquetés pointant vers un nœud particulier, (2) le nœud en question, ( 3) l'étiquette du nœud, et (4) la liste des bords étiquetés pointant depuis le nœud. Un Graph peut alors être conçu par induction comme Empty, ou comme Context fusionné (avec &) dans un Graph existant.

Comme le note Erwig, nous ne pouvons pas générer librement un Graph avec Empty et &, car nous pourrions générer une liste avec les constructeurs Cons et Nil, ou Tree avec Leaf et Branch. De plus, contrairement aux listes (comme d'autres l'ont mentionné), il n'y aura pas de représentation canonique d'un Graph. Ce sont des différences cruciales.

Néanmoins, ce qui rend cette représentation si puissante et si similaire aux représentations Haskell typiques des listes et des arbres, c'est que le type de données Graph ici est défini inductivement . Le fait qu'une liste soit définie de manière inductive est ce qui nous permet de reproduire de manière succincte des motifs sur elle, de traiter un seul élément et de traiter récursivement le reste de la liste; de même, la représentation inductive d'Erwig nous permet de traiter récursivement un graphe un Context à la fois. Cette représentation d'un graphique se prête à une définition simple d'un moyen de mapper sur un graphique (gmap), ainsi qu'à un moyen d'effectuer des replis non ordonnés sur des graphiques (ufold).

Les autres commentaires sur cette page sont super. La principale raison pour laquelle j'ai écrit cette réponse, cependant, est que lorsque je lis des expressions telles que "les graphiques ne sont pas algébriques", je crains que certains lecteurs ne s'en tirent inévitablement avec l'impression (erronée) que personne n'a trouvé une belle façon de représenter les graphiques dans Haskell d'une manière qui permet la mise en correspondance de modèles sur eux, le mappage sur eux, leur pliage, ou généralement le genre de trucs sympas et fonctionnels que nous sommes habitués à faire avec des listes et des arbres.

14
liminalisht

J'ai toujours aimé l'approche de Martin Erwig dans "Graphes inductifs et algorithmes de graphes fonctionnels", que vous pouvez lire ici . FWIW, j'ai écrit une fois une implémentation Scala également, voir https://github.com/nicolast/scalagraphs .

14
Nicolas Trangez

Toute discussion sur la représentation de graphiques dans Haskell nécessite une mention d'Andy Gill bibliothèque data-reify (voici l'article ).

La représentation de style "nouer le nœud" peut être utilisée pour créer des DSL très élégants (voir l'exemple ci-dessous). Cependant, la structure des données est d'une utilité limitée. La bibliothèque de Gill vous offre le meilleur des deux mondes. Vous pouvez utiliser un DSL "tying the knot", mais ensuite convertir le graphe basé sur un pointeur en un graphe basé sur une étiquette afin de pouvoir y exécuter les algorithmes de votre choix.

Voici un exemple simple:

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

Pour exécuter le code ci-dessus, vous aurez besoin des définitions suivantes:

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

Je tiens à souligner qu'il s'agit d'un DSL simpliste, mais le ciel est la limite! J'ai conçu un DSL très fonctionnel, y compris une belle syntaxe arborescente pour avoir un nœud diffusait une valeur initiale à certains de ses enfants et de nombreuses fonctions pratiques pour construire des types de nœuds spécifiques. Bien sûr, les définitions de type de données Node et mapDeRef étaient beaucoup plus impliquées.

3
Artelius

J'aime cette implémentation d'un graphique tiré de ici

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