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?
functional-programming
Jbemmz
fuente
fuente
Respuestas:
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).
fuente
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:
Aquí el requisito de memoria adicional es constante, al igual que el costo de tiempo de ejecución de las llamadas
prepend
. ¿Por qué? Porqueprepend
simplemente crea una nueva celda que tiene42
como cabeza ylist1
como cola. No tiene que copiar o iterarlist2
para lograr esto. Es decir, a excepción de la memoria requerida para almacenar42
,list2
reutiliza la misma memoria utilizada porlist1
. 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)
yarr1
nunca se vuelve a usar después de esta línea, un optimizador inteligente puede crear código que mute enarr1
lugar de crear una nueva matriz (sin afectar el comportamiento del programa). En ese caso, el rendimiento será como en un lenguaje imperativo, por supuesto.fuente
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::vector
en 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.
fuente
Solo sé un poco sobre Clojure y sus estructuras de datos inmutables .
Gráficamente, podemos representar algo como esto:
fuente
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.
fuente