En la programación funcional, ¿las variables mutables locales sin efectos secundarios todavía se consideran "mala práctica"?

23

¿Tener variables locales mutables en una función que solo se usan internamente (por ejemplo, la función no tiene efectos secundarios, al menos no intencionalmente) todavía se considera "no funcional"?

Por ejemplo, en la verificación de estilo del curso "Programación funcional con Scala" se considera que cualquier varuso es malo

Mi pregunta, si la función no tiene efectos secundarios, ¿todavía se desaconseja escribir código de estilo imperativo?

Por ejemplo, en lugar de utilizar la recursión de cola con el patrón del acumulador, ¿qué hay de malo en hacer un bucle local for y crear un mutable localListBuffer y agregarlo, siempre que la entrada no cambie?

Si la respuesta es "sí, siempre se desaniman, incluso si no hay efectos secundarios", ¿cuál es la razón?

Eran Medan
fuente
3
Todos los consejos, exhortaciones, etc. sobre el tema que he escuchado alguna vez se refieren al estado mutable compartido como la fuente de complejidad. ¿Ese curso está destinado a ser consumido solo por principiantes? Entonces es probablemente una simplificación excesiva deliberada bien intencionada.
Kilian Foth
3
@KilianFoth: El estado mutable compartido es un problema en contextos multiproceso, pero el estado mutable no compartido puede hacer que los programas también sean difíciles de razonar.
Michael Shaw
1
Creo que usar una variable mutable local no es necesariamente una mala práctica, pero no es un "estilo funcional": creo que el propósito del curso Scala (que tomé durante el otoño pasado) es enseñarle a programar en un estilo funcional. Una vez que pueda distinguir claramente entre el estilo funcional y el imperativo, puede decidir cuándo usar cuál (en caso de que su lenguaje de programación permita ambos). varsiempre es no funcional. Scala tiene vals perezosos y optimización de recursión de cola, lo que permite evitar los vars por completo.
Giorgio

Respuestas:

17

Lo único que es inequívocamente una mala práctica aquí es afirmar que algo es una función pura cuando no lo es.

Si las variables mutables se usan de una manera que es verdaderamente y completamente autónoma, la función es externamente pura y todos están felices. De hecho, Haskell respalda esto explícitamente , con el sistema de tipos incluso asegurando que las referencias mutables no puedan usarse fuera de la función que las crea.

Dicho esto, creo que hablar de "efectos secundarios" no es la mejor manera de verlo (y es por eso que dije "puro" arriba). Cualquier cosa que cree una dependencia entre la función y el estado externo hace que las cosas sean más difíciles de razonar, y eso incluye cosas como saber la hora actual o usar el estado mutable oculto de una manera no segura para subprocesos.

CA McCann
fuente
16

El problema no es la mutabilidad per se, es una falta de transparencia referencial.

Una cosa referencialmente transparente y una referencia a ella siempre deben ser iguales, por lo que una función referencialmente transparente siempre devolverá los mismos resultados para un conjunto dado de entradas y una "variable" referencialmente transparente es realmente un valor en lugar de una variable, ya que no puede cambiar Puede hacer una función referencialmente transparente que tenga una variable mutable dentro; Eso no es un problema. Sin embargo, puede ser más difícil garantizar que la función sea referencialmente transparente, dependiendo de lo que esté haciendo.

Se me ocurre una instancia en la que la mutabilidad tiene que usarse para hacer algo que es muy funcional: la memorización. Memoization es almacenar en caché los valores de una función, por lo que no es necesario volver a calcularlos; es referencialmente transparente, pero usa mutación.

Pero, en general, la transparencia referencial y la inmutabilidad van juntas, aparte de una variable mutable local en una función y una memoria referencialmente transparentes, no estoy seguro de que haya otros ejemplos en los que este no sea el caso.

Michael Shaw
fuente
44
Su punto sobre la memorización es muy bueno. Tenga en cuenta que Haskell enfatiza fuertemente la transparencia referencial para la programación, pero el comportamiento similar a la memorización de la evaluación perezosa implica una cantidad asombrosa de mutación que está haciendo el tiempo de ejecución del lenguaje detrás de escena.
CA McCann
@CA McCann: Creo que lo que dices es muy importante: en un lenguaje funcional, el tiempo de ejecución puede usar la mutación para optimizar el cálculo, pero no hay una construcción en el lenguaje que permita al programador usar la mutación. Otro ejemplo es un ciclo while con una variable de ciclo: en Haskell puede escribir una función recursiva de cola que puede implementarse con una variable mutable (para evitar usar la pila), pero lo que ve el programador son argumentos de función inmutable que se pasan de uno llama al siguiente.
Giorgio
@Michael Shaw: +1 para "El problema no es la mutabilidad per se, es una falta de transparencia referencial". Tal vez pueda citar el lenguaje limpio en el que tiene tipos de singularidad: estos permiten la mutabilidad pero aún garantizan la transparencia referencial.
Giorgio
@Giorgio: Realmente no sé nada sobre Clean, aunque lo he oído mencionar de vez en cuando. Tal vez debería investigarlo.
Michael Shaw
@ Michael Shaw: No sé mucho sobre Clean, pero sé que usa tipos únicos para garantizar la transparencia referencial. Básicamente, puede modificar un objeto de datos siempre que después de la modificación no tenga referencias al valor anterior. En mi opinión, esto ilustra su punto: la transparencia referencial es el punto más importante, y la inmutabilidad es solo una forma posible de garantizarlo.
Giorgio
8

No es realmente bueno reducir esto a "buenas prácticas" versus "malas prácticas". Scala admite valores mutables porque resuelven ciertos problemas mucho mejor que los valores inmutables, es decir, aquellos que son de naturaleza iterativa.

En perspectiva, estoy bastante seguro de que a través de CanBuildFromcasi todas las estructuras inmutables proporcionadas por scala, se realiza algún tipo de mutación internamente. El punto es que lo que exponen es inmutable. Mantener tantos valores inmutables como sea posible ayuda a que el programa sea ​​más fácil de razonar y menos propenso a errores .

Esto no significa que necesariamente deba evitar estructuras y valores mutables internamente cuando tenga un problema que se adapte mejor a la mutabilidad.

Con eso en mente, muchos problemas que generalmente requieren variables mutables (como el bucle) se pueden resolver mejor con muchas de las funciones de orden superior que proporcionan lenguajes como Scala (mapa / filtro / pliegue). Sé consciente de eso.

KChaloux
fuente
2
Sí, casi nunca necesito un bucle for cuando uso las colecciones de Scala. map, filter, foldLeftY forEach hacer el truco mayor parte del tiempo, pero no cuando lo hacen, ser capaz de sentir que soy "OK" para volver a la fuerza bruta código imperativo es agradable. (siempre y cuando no haya efectos secundarios, por supuesto)
Eran Medan
3

Además de los posibles problemas con la seguridad de los hilos, también suele perder mucha seguridad de tipo. Los bucles imperativos tienen un tipo de retorno Unity pueden tomar casi cualquier expresión para las entradas. Las funciones de orden superior e incluso la recursividad tienen una semántica y tipos mucho más precisos.

También tiene muchas más opciones para el procesamiento funcional de contenedores que con los bucles imperativos. Con imperativo, básicamente tienes for, whiley pequeñas variaciones en esos dos como do...whiley foreach.

En funcional, tiene agregado, conteo, filtro, búsqueda, flatMap, fold, groupBy, lastIndexWhere, map, maxBy, minBy, partición, scan, sortBy, sortWith, span y takeWhile, solo para nombrar algunos más comunes de Scala's biblioteca estándar Cuando te acostumbras a tenerlos disponibles, los forbucles imperativos parecen demasiado básicos en comparación.

La única razón real para usar la mutabilidad local es muy ocasionalmente para el rendimiento.

Karl Bielefeldt
fuente
2

Yo diría que en su mayoría está bien. Además, generar estructuras de esta manera podría ser una buena manera de mejorar el rendimiento en algunos casos. Clojure ha abordado este problema proporcionando estructuras de datos transitorias .

La idea básica es permitir mutaciones locales en un alcance limitado y luego congelar la estructura antes de devolverla. De esta manera, su usuario aún puede razonar sobre su código como si fuera puro, pero puede realizar transformaciones en el lugar cuando lo necesite.

Como dice el enlace:

Si un árbol cae en el bosque, ¿emite un sonido? Si una función pura muta algunos datos locales para producir un valor de retorno inmutable, ¿está bien?

Simon Bergot
fuente
2

No tener variables locales mutables tiene una ventaja: hace que la función sea más amigable con los hilos.

Me quemé con una variable local (no en mi código, ni tenía la fuente) causando una corrupción de datos de baja probabilidad. La seguridad de subprocesos no se mencionó de una manera u otra, no hubo un estado que persistiera en las llamadas y no hubo efectos secundarios. No se me ocurrió que podría no ser seguro para subprocesos, perseguir una corrupción de datos aleatorios de 1 en 100,000 es un dolor real.

Loren Pechtel
fuente