En la programación funcional, ¿tener la mayoría de las estructuras de datos inmutables requiere más uso de memoria?

63

En la programación funcional, dado que casi toda la estructura de datos es inmutable, cuando el estado tiene que cambiar, se crea una nueva estructura. ¿Esto significa mucho más uso de memoria? Conozco bien el paradigma de programación orientado a objetos, ahora estoy tratando de aprender sobre el paradigma de programación funcional. El concepto de que todo sea inmutable me confunde. Parece que un programa que usa estructuras inmutables requeriría mucha más memoria que un programa con estructuras mutables. ¿Estoy incluso mirando esto de la manera correcta?

Jbemmz
fuente
77
Se puede decir que, pero las estructuras de datos más inmutables reutilizar los datos subyacentes de los cambios. Eric Lippert tiene una gran serie de blogs sobre la inmutabilidad en C #
Oded
3
Me gustaría echar un vistazo a las estructuras de datos puramente funcional, Es un gran libro escrito por el mismo tipo que escribió la mayor parte de la biblioteca de contenedores de Haskell (aunque el libro es principalmente SML)
jozefg
1
Esta respuesta, relacionada con el tiempo de ejecución en lugar del consumo de memoria, también puede ser interesante para usted: stackoverflow.com/questions/1990464/…
9000
1
Puede encontrar esto interesante: en.wikipedia.org/wiki/Static_single_assignment_form
Sean McSomething

Respuestas:

35

La única respuesta correcta a esto es "a veces". Hay muchos trucos que los lenguajes funcionales pueden usar para evitar el desperdicio de memoria. La inmutabilidad hace que sea más fácil compartir datos entre funciones, e incluso entre estructuras de datos, ya que el compilador puede garantizar que los datos no se modificarán. Los lenguajes funcionales tienden a fomentar el uso de estructuras de datos que se pueden usar de manera eficiente como estructuras inmutables (por ejemplo, árboles en lugar de tablas hash). Si agrega pereza a la mezcla, como lo hacen muchos lenguajes funcionales, eso agrega nuevas formas de ahorrar memoria (también agrega nuevas formas de desperdiciar memoria, pero no voy a entrar en eso).

Dirk Holsopple
fuente
24

En la programación funcional, dado que casi toda la estructura de datos es inmutable, cuando el estado tiene que cambiar, se crea una nueva estructura. ¿Esto significa mucho más uso de memoria?

Eso depende de la estructura de datos, los cambios exactos que realizó y, en algunos casos, el optimizador. Como un ejemplo, consideremos anteponer a una lista:

list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
                           // by the elements of list1. list1 is unchanged

Aquí el requisito de memoria adicional es constante, al igual que el costo de tiempo de ejecución de las llamadas prepend. ¿Por qué? Porque prependsimplemente crea una nueva celda que tiene 42como cabeza y list1como cola. No tiene que copiar o iterar list2para lograr esto. Es decir, a excepción de la memoria requerida para almacenar 42, list2reutiliza la misma memoria utilizada por list1. Como ambas listas son inmutables, este intercambio es perfectamente seguro.

De manera similar, cuando se trabaja con estructuras de árbol equilibradas, la mayoría de las operaciones requieren solo una cantidad logarítmica de espacio adicional porque todo, pero una ruta del árbol puede ser compartida.

Para las matrices, la situación es un poco diferente. Es por eso que, en muchos lenguajes FP, las matrices no se usan con tanta frecuencia. Sin embargo, si hace algo así arr2 = map(f, arr1)y arr1nunca se vuelve a usar después de esta línea, un optimizador inteligente puede crear código que mute en arr1lugar de crear una nueva matriz (sin afectar el comportamiento del programa). En ese caso, el rendimiento será como en un lenguaje imperativo, por supuesto.

sepp2k
fuente
1
Por interés, ¿qué implementación de qué idiomas reutiliza el espacio como describió cerca del final?
@delnan En mi universidad había un lenguaje de investigación llamado Qube, que hizo eso. Sin embargo, no sé si hay algún lenguaje usado en la naturaleza que haga esto. Sin embargo, la fusión de Haskell puede lograr el mismo efecto en muchos casos.
sepp2k
7

Las implementaciones ingenuas realmente expondrían este problema: cuando crea una nueva estructura de datos en lugar de actualizar una existente en el lugar, debe tener algunos gastos generales.

Los diferentes idiomas tienen diferentes formas de lidiar con esto, y hay algunos trucos que la mayoría usa.

Una estrategia es la recolección de basura . En el momento en que se crea la nueva estructura, o poco después, las referencias a la estructura anterior quedan fuera de alcance, y el recolector de basura la recogerá al instante o lo suficientemente pronto, dependiendo del algoritmo GC. Esto significa que si bien todavía hay una sobrecarga, solo es temporal y no crecerá linealmente con la cantidad de datos.

Otro es elegir diferentes tipos de estructuras de datos. Cuando las matrices son la estructura de datos de la lista de acceso en lenguajes imperativos (generalmente envueltos en algún tipo de contenedor de reasignación dinámica como std::vectoren C ++), los lenguajes funcionales a menudo prefieren listas vinculadas. Con una lista vinculada, una operación de anteponer ('contras') puede reutilizar la lista existente como la cola de la nueva lista, por lo que todo lo que realmente se asigna es el nuevo encabezado de la lista. Existen estrategias similares para otros tipos de estructuras de datos: conjuntos, árboles, lo que sea.

Y luego está la evaluación perezosa, a la Haskell. La idea es que las estructuras de datos que cree no se creen completamente de inmediato; en su lugar, se almacenan como "troncos" (puede pensar en ellos como recetas para construir el valor cuando sea necesario). Solo cuando se necesita el valor, el thunk se expande a un valor real. Esto significa que la asignación de memoria se puede diferir hasta que sea necesaria una evaluación, y en ese punto, se pueden combinar varios thunks en una asignación de memoria.

tdammers
fuente
Wow, una pequeña respuesta y mucha información / visión. Gracias :)
Gerry
3

Solo sé un poco sobre Clojure y sus estructuras de datos inmutables .

Clojure proporciona un conjunto de listas inmutables, vectores, conjuntos y mapas. Como no se pueden cambiar, 'agregar' o 'eliminar' algo de una colección inmutable significa crear una nueva colección como la anterior pero con el cambio necesario. Persistencia es un término utilizado para describir la propiedad en la que la versión anterior de la colección todavía está disponible después del 'cambio', y que la colección mantiene sus garantías de rendimiento para la mayoría de las operaciones. Específicamente, esto significa que la nueva versión no se puede crear usando una copia completa, ya que eso requeriría tiempo lineal. Inevitablemente, las colecciones persistentes se implementan utilizando estructuras de datos vinculadas, de modo que las nuevas versiones pueden compartir la estructura con la versión anterior.

Gráficamente, podemos representar algo como esto:

(def my-list '(1 2 3))

    +---+      +---+      +---+
    | 1 | ---> | 2 | ---> | 3 |
    +---+      +---+      +---+

(def new-list (conj my-list 0))

              +-----------------------------+
    +---+     | +---+      +---+      +---+ |
    | 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
    +---+     | +---+      +---+      +---+ |
              +-----------------------------+
Arturo Herrero
fuente
2

Además de lo que se ha dicho en otras respuestas, me gustaría mencionar el lenguaje de programación Clean, que admite los llamados tipos únicos . No conozco este lenguaje, pero supongo que los tipos únicos admiten algún tipo de "actualización destructiva".

En otras palabras, si bien la semántica de actualizar un estado es que crea un nuevo valor a partir de uno antiguo mediante la aplicación de una función, la restricción de unicidad puede permitir que el compilador reutilice objetos de datos internamente porque sabe que el valor antiguo no será referenciado más en el programa después de que se haya producido el nuevo valor.

Para obtener más detalles, consulte, por ejemplo, la página de inicio de Clean y este artículo de Wikipedia.

Giorgio
fuente