Il est assez facile de représenter un arbre ou une liste dans haskell en utilisant des 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 vraiment aussi solides que les liens entre les éléments précédents et suivants actuels d'une liste, ou les parents et enfants d'un nœud dans un arbre. 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 principalement ce sentiment de doute et cette perception d'inélégance qui m'amènent à poser cette question. Existe-t-il un moyen meilleur / plus élégant mathématiquement de définir des graphes dans Haskell? Ou suis-je tombé sur quelque chose de fondamentalement dur / 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 des valeurs.
Alors qu'est-ce qui se passe vraiment?
la source
fgl
package s'est développé à partir de cela.Réponses:
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; car les valeurs peuvent être partagées, tout ADT qui peut 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 les valeurs A et B, avec A contenant B et B contenant A, aucune ne peut être créée avant que l'autre n'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 (car je n'en 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 nouer des nœuds n'aide pas.
Habituellement, lorsque j'ai rencontré cela avant, je viens de recourir à une indirection supplémentaire, comme vous le suggérez; souvent en utilisant une carte des identifiants aux éléments réels, et en faisant en sorte que les éléments contiennent des références aux identifiants plutôt qu'à d'autres éléments. La principale chose que je n'aimais pas faire cela (mis à part l'inefficacité évidente) est que cela semblait plus fragile, introduisant les erreurs possibles de recherche d'un identifiant qui n'existe pas ou d'essayer d'attribuer le même identifiant à plus d'un. élément. Vous pouvez écrire du code pour que ces erreurs ne se produisent pas, bien sûr, et même le cacher derrière des abstractions afin que les seuls endroits où de telles erreurs pourraient se produire soient 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 , qui semble être une lecture intéressante.
la source
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 par la suite, 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:
Si vous comptez changer ou éditer fréquemment le graphique, je vous 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:
Un graphique de contrôle-flux applicatif basé sur la fermeture à glissière de Huet
Hoopl: une bibliothèque modulaire et réutilisable pour l'analyse et la transformation des flux de données
la source
Comme Ben l'a mentionné, les données cycliques dans Haskell sont construites par un mécanisme appelé "nouer le nœud". En pratique, cela signifie que nous écrivons des déclarations mutuellement récursives à l'aide de clauses
let
ouwhere
, ce qui fonctionne car les parties mutuellement récursives sont évaluées paresseusement.Voici un exemple de type de graphique:
Comme vous pouvez le voir, nous utilisons des
Node
références réelles au lieu de l'indirection. Voici comment implémenter une fonction qui construit le graphique à partir d'une liste d'associations d'étiquettes.Nous prenons une liste de
(nodeLabel, [adjacentLabel])
paires et construisons lesNode
valeurs réelles via une liste de recherche intermédiaire (qui fait le nœud réel). L'astuce est quenodeLookupList
(qui a le type[(a, Node a)]
) est construit en utilisantmkNode
, qui à son tour se réfère aunodeLookupList
pour trouver les nœuds adjacents.la source
C'est vrai, les graphiques ne sont pas algébriques. Pour résoudre ce problème, vous avez plusieurs options:
Int
s) et en y faisant référence indirectement plutôt que 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.Il y a donc des avantages et des inconvénients à chacun des choix ci-dessus. Choisissez celui qui vous convient le mieux.
la source
Quelques autres ont brièvement mentionné
fgl
les graphes inductifs et les algorithmes de graphes fonctionnels de Martin Erwig , mais il vaut probablement la peine d'écrire une réponse qui donne réellement une idée des types de données derrière l'approche de représentation inductive.Dans son article, Erwig présente les types suivants:
(La représentation dans
fgl
est légèrement différente et fait bon usage des classes de types - mais l'idée est essentiellement la même.)Erwig décrit un multigraphe dans lequel les nœuds et les arêtes ont des étiquettes, et dans lequel toutes les arêtes sont dirigées. A
Node
a une étiquette d'un certain typea
; un bord a une étiquette d'un certain typeb
. AContext
est simplement (1) une liste d'arêtes étiquetées pointant vers un nœud particulier, (2) le nœud en question, (3) l'étiquette du nœud, et (4) la liste des arêtes étiquetées pointant à partir du nœud. AGraph
peut alors être conçu de manière inductive comme l'unEmpty
ou l' autre , ou commeContext
fusionné (avec&
) dans un existantGraph
.Comme le note Erwig, nous ne pouvons pas générer librement un
Graph
avecEmpty
et&
, comme nous pourrions générer une liste avec les constructeursCons
etNil
, ouTree
avecLeaf
etBranch
. De plus, contrairement aux listes (comme d'autres l'ont mentionné), il n'y aura pas de représentation canonique d'unGraph
. 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
Graph
type de données est ici défini de manière inductive . Le fait qu'une liste soit définie de manière inductive est ce qui nous permet de faire une correspondance de modèle si succincte dessus, 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 de manière récursive un graphe unContext
par un. Cette représentation d'un graphe se prête à une définition simple d'un moyen de mapper sur un graphe (gmap
), ainsi qu'à un moyen d'effectuer des plis non ordonnés sur des graphes (ufold
).Les autres commentaires sur cette page sont excellents. La principale raison pour laquelle j'ai écrit cette réponse, cependant, est que lorsque je lis des phrases telles que "les graphiques ne sont pas algébriques", je crains que certains lecteurs en reviennent inévitablement avec l'impression (erronée) que personne n'a trouvé une bonne façon de représenter les graphiques en Haskell d'une manière qui permet la correspondance de motifs sur eux, les mapper sur eux, les plier, ou généralement faire le genre de choses cool et fonctionnelles que nous avons l'habitude de faire avec des listes et des arbres.
la source
J'ai toujours aimé l'approche de Martin Erwig dans «Graphiques inductifs et algorithmes de graphes fonctionnels», que vous pouvez lire ici . FWIW, j'ai également écrit une implémentation Scala, voir https://github.com/nicolast/scalagraphs .
la source
Toute discussion sur la représentation des graphiques dans Haskell nécessite une mention de la bibliothèque data-reify d'Andy Gill (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 une DSL «nouant le nœud», mais ensuite convertir le graphique basé sur des pointeurs en un graphique basé sur des étiquettes afin que vous puissiez y exécuter les algorithmes de votre choix.
Voici un exemple simple:
Pour exécuter le code ci-dessus, vous aurez besoin des définitions suivantes:
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, comprenant une belle syntaxe arborescente pour qu'un nœud diffuse une valeur initiale à certains de ses enfants, et de nombreuses fonctions pratiques pour construire des types de nœuds spécifiques. Bien sûr, le type de données Node et les définitions de mapDeRef étaient beaucoup plus impliqués.
la source
J'aime cette implémentation d'un graphe tiré d' ici
la source