Es bastante fácil representar un árbol o una lista en Haskell utilizando tipos de datos algebraicos. Pero, ¿cómo harías para representar tipográficamente un gráfico? Parece que necesitas tener punteros. Supongo que podrías tener algo como
type Nodetag = String
type Neighbours = [Nodetag]
data Node a = Node a Nodetag Neighbours
Y eso sería viable. Sin embargo, se siente un poco desacoplado; Los enlaces entre diferentes nodos en la estructura realmente no "se sienten" tan sólidos como los enlaces entre los elementos actuales anteriores y siguientes en una lista, o los padres e hijos de un nodo en un árbol. Tengo el presentimiento de que realizar manipulaciones algebraicas en el gráfico, tal como lo definí, se vería obstaculizado por el nivel de indirección introducido a través del sistema de etiquetas.
Es principalmente este sentimiento de duda y percepción de inelegancia lo que me hace hacer esta pregunta. ¿Existe una forma mejor / más matemáticamente elegante de definir gráficos en Haskell? ¿O me he topado con algo inherentemente duro / fundamental? Las estructuras de datos recursivas son dulces, pero esto parece ser otra cosa. Una estructura de datos autorreferencial en un sentido diferente a cómo los árboles y las listas son autorreferenciales. Es como si las listas y los árboles fueran autorreferenciales a nivel de tipo, pero los gráficos son autorreferenciales a nivel de valor.
Entonces, ¿qué está pasando realmente?
fuente
fgl
paquete se desarrolló a partir de esto.Respuestas:
También me resulta incómodo tratar de representar estructuras de datos con ciclos en un lenguaje puro. Son los ciclos los que realmente son el problema; porque los valores se pueden compartir, cualquier ADT que pueda contener un miembro del tipo (incluidas listas y árboles) es realmente un DAG (Gráfico Acíclico Dirigido). El problema fundamental es que si tiene valores A y B, con A que contiene B y B que contiene A, entonces ninguno puede crearse antes de que exista el otro. Debido a que Haskell es perezoso, puedes usar un truco conocido como Tying the Knot para evitar esto, pero eso hace que me duela el cerebro (porque todavía no lo he hecho mucho). He hecho más de mi programación sustancial en Mercurio que Haskell hasta ahora, y Mercurio es estricto, por lo que atar nudos no ayuda.
Por lo general, cuando me he encontrado con esto antes, he recurrido a la indirección adicional, como estás sugiriendo; a menudo mediante el uso de un mapa de identificadores a los elementos reales, y tener elementos que contienen referencias a los identificadores en lugar de a otros elementos. Lo principal que no me gustó de hacer eso (aparte de la ineficiencia obvia) es que se sintió más frágil, introduciendo los posibles errores de buscar una identificación que no existe o tratar de asignar la misma identificación a más de uno elemento. Puede escribir código para que estos errores no ocurran, por supuesto, e incluso ocultarlo detrás de abstracciones para que los únicos lugares donde tales errores puedan ocurrir estén limitados. Pero todavía es una cosa más equivocarse.
Sin embargo, un rápido google para el "gráfico de Haskell" me llevó a http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling , que parece una lectura que vale la pena.
fuente
En la respuesta de shang puedes ver cómo representar un gráfico usando la pereza. El problema con estas representaciones es que son muy difíciles de cambiar. El truco de atar nudos es útil solo si vas a construir un gráfico una vez, y luego nunca cambia.
En la práctica, si realmente quisiera hacer algo con mi gráfico, utilizo las representaciones más peatonales:
Si va a cambiar o editar el gráfico con frecuencia, le recomiendo usar una representación basada en la cremallera de Huet. Esta es la representación utilizada internamente en GHC para gráficos de flujo de control. Usted puede leer sobre ello aquí:
Un gráfico de control de flujo aplicativo basado en la cremallera de Huet
Hoopl: una biblioteca modular y reutilizable para el análisis y la transformación del flujo de datos
fuente
Como mencionó Ben, los datos cíclicos en Haskell se construyen mediante un mecanismo llamado "atar el nudo". En la práctica, significa que escribimos declaraciones recursivas recíprocas usando
let
owhere
cláusulas, lo que funciona porque las partes recursivas recíprocas se evalúan perezosamente.Aquí hay un ejemplo de tipo de gráfico:
Como puede ver, usamos
Node
referencias reales en lugar de indirección. Aquí se explica cómo implementar una función que construya el gráfico a partir de una lista de asociaciones de etiquetas.Tomamos una lista de
(nodeLabel, [adjacentLabel])
pares y construimos losNode
valores reales a través de una lista de búsqueda intermedia (que hace el nudo real). El truco es quenodeLookupList
(que tiene el tipo[(a, Node a)]
) se construye utilizandomkNode
, lo que a su vez se refierenodeLookupList
al encontrar los nodos adyacentes.fuente
Es cierto, los gráficos no son algebraicos. Para solucionar este problema, tiene un par de opciones:
Int
s) y refiriéndose a ellos indirectamente en lugar de algebraicamente. Esto se puede hacer significativamente más conveniente haciendo que el tipo sea abstracto y proporcionando una interfaz que haga malabarismos con la indirección para usted. Este es el enfoque adoptado por, por ejemplo, fgl y otras bibliotecas gráficas prácticas en Hackage.Por lo tanto, hay ventajas y desventajas para cada una de las opciones anteriores. Elige el que te parezca mejor.
fuente
Algunos otros mencionaron brevemente
fgl
los gráficos inductivos y los algoritmos de gráficos funcionales de Martin Erwig , pero probablemente valga la pena escribir una respuesta que realmente dé una idea de los tipos de datos detrás del enfoque de representación inductiva.En su artículo, Erwig presenta los siguientes tipos:
(La representación en
fgl
es ligeramente diferente y hace un buen uso de las clases de tipos, pero la idea es esencialmente la misma).Erwig está describiendo un multigrafo en el que los nodos y los bordes tienen etiquetas, y en el que todos los bordes están dirigidos. A
Node
tiene una etiqueta de algún tipoa
; un borde tiene una etiqueta de algún tipob
. AContext
es simplemente (1) una lista de bordes etiquetados que apuntan a un nodo en particular, (2) el nodo en cuestión, (3) la etiqueta del nodo y (4) la lista de bordes etiquetados que apuntan desde el nodo. AGraph
puede entonces concebirse inductivamente como cualquieraEmpty
, o como unaContext
fusión (con&
) en una existenteGraph
.Como señala Erwig, no podemos generar libremente un
Graph
conEmpty
y&
, como podríamos generar una lista con los constructoresCons
yNil
, o unTree
conLeaf
yBranch
. Además, a diferencia de las listas (como han mencionado otros), no habrá ninguna representación canónica de aGraph
. Estas son diferencias cruciales.No obstante, lo que hace que esta representación sea tan poderosa y tan similar a las representaciones típicas de Haskell de listas y árboles, es que el
Graph
tipo de datos aquí se define inductivamente . El hecho de que una lista se defina inductivamente es lo que nos permite hacer una concordancia de patrones tan sucintamente en ella, procesar un solo elemento y procesar recursivamente el resto de la lista; igualmente, la representación inductiva de Erwig nos permite procesar recursivamente un gráfico unoContext
a la vez. Esta representación de un gráfico se presta a una definición simple de una forma de mapear sobre un gráfico (gmap
), así como a una forma de realizar pliegues desordenados sobre gráficos (ufold
).Los otros comentarios en esta página son geniales. Sin embargo, la razón principal por la que escribí esta respuesta es que cuando leo frases como "los gráficos no son algebraicos", me temo que algunos lectores inevitablemente saldrán con la impresión (errónea) de que nadie encontró una buena manera de representar gráficos en Haskell de una manera que permite la coincidencia de patrones en ellos, mapearlos, doblarlos o, en general, hacer el tipo de cosas geniales y funcionales que estamos acostumbrados a hacer con listas y árboles.
fuente
Siempre me gustó el enfoque de Martin Erwig en "Gráficos inductivos y algoritmos de gráficos funcionales", que puedes leer aquí . FWIW, una vez escribí una implementación de Scala también, vea https://github.com/nicolast/scalagraphs .
fuente
Cualquier discusión sobre la representación de gráficos en Haskell necesita una mención de la biblioteca de datos de Andy Gill (aquí está el artículo ).
La representación de estilo "atar el nudo" se puede utilizar para hacer DSL muy elegantes (ver ejemplo a continuación). Sin embargo, la estructura de datos es de uso limitado. La biblioteca de Gill te permite lo mejor de ambos mundos. Puede usar un DSL "atar el nudo", pero luego convertir el gráfico basado en puntero en un gráfico basado en etiquetas para que pueda ejecutar sus algoritmos de elección en él.
Aquí hay un ejemplo simple:
Para ejecutar el código anterior, necesitará las siguientes definiciones:
Quiero enfatizar que este es un DSL simplista, ¡pero el cielo es el límite! Diseñé un DSL muy funcional, que incluye una buena sintaxis similar a un árbol para que un nodo transmita un valor inicial a algunos de sus hijos, y muchas funciones convenientes para construir tipos de nodo específicos. Por supuesto, el tipo de datos de nodo y las definiciones mapDeRef estuvieron mucho más involucradas.
fuente
Me gusta esta implementación de un gráfico tomado de aquí
fuente