Solución alternativa para implementar operaciones en estructuras de datos doblemente vinculadas o circulares en idiomas con datos inmutables

11

Me gustaría aprender a hacer gráficos y realizar algunas operaciones locales en Haskell, pero la pregunta no es específica de Haskell, y en lugar de gráficos podemos considerar listas doblemente vinculadas.

Pregunta: ¿Cuál sería una forma idiomática o recomendada de implementar una lista doblemente enlazada (u otra estructura de datos doblemente enlazada o circular) y operaciones en un lenguaje que apoye y defienda principalmente estructuras de datos inmutables (Haskell, Clojure, etc.) ? En particular, ¿cómo usar las actualizaciones en el lugar, que están formalmente prohibidas por el idioma?

Puedo imaginar fácilmente que si se realiza alguna operación local en una lista doblemente vinculada (si se inserta un elemento, por ejemplo), puede que no sea necesario copiar toda la lista inmediatamente debido a la flojera del idioma. Sin embargo, dado que la lista está doblemente vinculada, si se modifica en un lugar, ninguno de los nodos antiguos se puede usar en la nueva versión de la lista, y necesitarían ser marcados, copiados, recolectados de basura de alguna manera tarde o temprano. . Obviamente, estas son operaciones redundantes si solo se utilizara la copia actualizada de la lista, pero agregarían una "sobrecarga" proporcional al tamaño de la lista.

¿Significa esto que para esas tareas los datos inmutables son simplemente inapropiados y que los lenguajes declarativos funcionales sin soporte "nativo" para datos mutables no son tan buenos como los imperativos? O, ¿hay alguna solución difícil?

PD: He encontrado algunos artículos y presentaciones sobre este tema en Internet, pero tuve dificultades para seguirlos, aunque creo que la respuesta a esta pregunta no debería tomar más de un párrafo y tal vez un diagrama ... Quiero decir, si hay no hay una solución "funcional" para este problema, la respuesta probablemente sea "usar C". Si hay uno, entonces, ¿qué tan complicado puede ser?


Preguntas relacionadas


Citas relevantes

Los lenguajes de programación puramente funcionales permiten que muchos algoritmos se expresen de manera muy concisa, pero hay algunos algoritmos en los que el estado actualizable en el lugar parece desempeñar un papel crucial. Para estos algoritmos, los lenguajes puramente funcionales, que carecen de estado actualizable, parecen ser inherentemente ineficientes ( [Ponder, McGeer y Ng, 1988] ).

- John Launchbury y Simon Peyton Jones, Hilos de estado funcional perezoso (1994), también John Launchbury y Simon Peyton Jones, Estado en Haskell (1995). Estos documentos introducen el STconstructor de tipo monádico en Haskell.

Alexey
fuente
44
Recomendado: Okasaki
Robert Harvey
2
Gracias por la referencia He encontrado su tesis .
Alexey
Este artículo parece prometedor: algoritmos de búsqueda lineal y de profundidad lineal de Haskell (1994), de David King y John Launchbury.
Alexey
Parece que un problema similar con las matrices es abordado por el paquete diffarray que implementa el DiffArraytipo. Mirando la fuente del paquete diffarray , veo 91 ocurrencias de unsafePerformIO. Parece que la respuesta a mi pregunta es "sí, no, los lenguajes puramente funcionales con datos inmutables no son apropiados para implementar algoritmos que normalmente dependen de actualizaciones en el lugar".
Alexey
Mi solución actual (en Haskell) es el uso de un diccionario ( Map, IntMapo HashMap) como un almacenamiento y para hacer nodos contienen identificadores de los nodos vinculados. "Todos los problemas en informática pueden resolverse mediante otro nivel de indirección".
Alexey

Respuestas:

6

Podría haber otras estructuras de datos inmutables eficientes que se ajusten a su tarea particular, pero que no sean tan generales como una lista doblemente vinculada (que desafortunadamente es propensa a errores de modificación concurrentes debido a su mutabilidad). Si especifica su problema de manera más específica, tal estructura probablemente se pueda encontrar.

La respuesta general para el recorrido (relativamente) económico de estructuras inmutables es lentes. La idea es que puede mantener la información suficiente para reconstruir una estructura inmutable modificada a partir de sus partes no modificadas y la pieza actualmente modificada, y navegar sobre ella a un nodo vecino.

Otra estructura útil es una cremallera . (La parte divertida es que la firma de tipo para una cremallera de lente es una derivada matemática escolar de una firma de tipo de la estructura).

Aqui hay unos cuantos enlaces.

9000
fuente
1
dependiendo de lo que se necesite, una cremallera también puede ser útil
jk.
Para especificar mi problema más estrictamente, supongamos que quiero programar un sistema de reescritura de gráficos, por ejemplo, un evaluador de cálculo lambda basado en la reescritura de gráficos.
Alexey
1
@Alexey: ¿Conoces el trabajo de Clean people en la reescritura de gráficos? wiki.clean.cs.ru.nl/…
Giorgio
1
@Alexey: No que yo sepa: Clean es un primo de Haskell que se desarrolló por sí solo. También tiene un mecanismo diferente para tratar los efectos secundarios (AFAIK se llama tipos únicos). Por otro lado, los desarrolladores han trabajado mucho con la reescritura de gráficos. Por lo tanto, podrían estar entre las mejores personas que conocen tanto la reescritura de gráficos como la programación funcional.
Giorgio
1
Estoy de acuerdo en que una cremallera parece resolver el problema con una lista doblemente vinculada o un árbol si quiero navegar y modificar en el lugar en el que estoy actualmente, pero no está claro qué hacer si quiero centrarme en varios puntos simultáneamente y, por ejemplo, intercambiar dos elementos en dos lugares muy separados. Es aún menos claro si se puede usar con estructuras "circulares".
Alexey
2

Haskell no impide el uso de estructuras de datos mutables. Se desaconsejan y se hacen más difíciles de usar debido al hecho de que las partes del código que los usan deben devolver una acción IO (que finalmente debe estar vinculada a la acción IO que devuelve la función principal), pero eso no hacer que sea imposible usar tales estructuras si realmente las necesita.

Sugeriría investigar el uso de la memoria transaccional de software como un camino a seguir. Además de proporcionar una forma eficiente de implementar estructuras mutables, también ofrece garantías muy útiles para la seguridad de los hilos. Consulte la descripción del módulo en https://hackage.haskell.org/package/stm y la descripción general de la wiki en https://wiki.haskell.org/Software_transactional_memory .

Jules
fuente
Gracias, intentaré aprender sobre STM. Parece que hay más métodos en Haskell para tener la mutabilidad y el estado (he tropieza MVar, State, ST), por lo que voy a tener que averiguar sus diferencias y usos previstos.
Alexey
@Alexey: Buen punto con respecto a STIMO, debería mencionarse en la respuesta porque permite ejecutar un cálculo con estado, luego descartar el estado y extraer el resultado como un valor puro.
Giorgio
@Giorgio, ¿es posible usar Haskell's STcon STM para tener concurrencia y estado desechable?
Alexey
Solo una sugerencia terminológica más: la acción de E / S principal compuesta no es " devuelta por la función principal" sino que se asigna a la mainvariable. :) ( mainni siquiera tiene una función.)
Alexey
Entiendo su punto de vista, pero aún "variable" tiene una connotación en la mente de la mayoría de las personas como un valor simple, en lugar de un proceso que produce un valor, y main es claramente mejor considerado como el último en lugar del primero. El cambio que sugiere, aunque es técnicamente correcto, puede confundir a quienes no están familiarizados con el tema.
Jules