Últimamente he estado leyendo muchas cosas sobre programación funcional, y puedo entender la mayor parte, pero lo único que no puedo entender es la codificación sin estado. Me parece que simplificar la programación al eliminar el estado mutable es como "simplificar" un automóvil quitando el tablero de instrumentos: el producto terminado puede ser más simple, pero buena suerte para que interactúe con los usuarios finales.
Casi todas las aplicaciones de usuario que se me ocurren implican al estado como un concepto central. Si escribe un documento (o una publicación SO), el estado cambia con cada entrada nueva. O si juegas un videojuego, hay toneladas de variables de estado, comenzando con las posiciones de todos los personajes, que tienden a moverse constantemente. ¿Cómo puedes hacer algo útil sin hacer un seguimiento de los valores cambiantes?
Cada vez que encuentro algo que discute este problema, está escrito en un funcional funcional realmente técnico que asume un fondo pesado de FP que no tengo. ¿Alguien sabe una manera de explicar esto a alguien con una buena y sólida comprensión de la codificación imperativa, pero que es un n00b completo en el lado funcional?
EDITAR: Algunas de las respuestas hasta ahora parecen estar tratando de convencerme de las ventajas de los valores inmutables. Yo entiendo esa parte. Tiene mucho sentido Lo que no entiendo es cómo puede realizar un seguimiento de los valores que tienen que cambiar, y cambian constantemente, sin variables mutables.
fuente
Respuestas:
Si estás interesado, aquí hay una serie de artículos que describen la programación de juegos con Erlang.
Probablemente no le guste esta respuesta, pero no obtendrá un programa funcional hasta que lo use. Puedo publicar ejemplos de código y decir "Aquí, no lo ves ", pero si no entiendes la sintaxis y los principios subyacentes, entonces tus ojos simplemente se nublan. Desde su punto de vista, parece que estoy haciendo lo mismo que un lenguaje imperativo, pero simplemente estableciendo todo tipo de límites para dificultar la programación a propósito. Mi punto de vista, solo estás experimentando la paradoja de Blub .
Al principio era escéptico, pero me subí al tren de programación funcional hace unos años y me enamoré de él. El truco con la programación funcional es poder reconocer patrones, asignaciones de variables particulares y mover el estado imperativo a la pila. Un ciclo for, por ejemplo, se convierte en recursividad:
No es muy bonito, pero obtuvimos el mismo efecto sin mutación. Por supuesto, siempre que sea posible, nos gusta evitar los bucles por completo y simplemente abstraerlo:
El método Seq.iter enumerará a través de la colección e invocará la función anónima para cada elemento. Muy útil :)
Lo sé, imprimir números no es exactamente impresionante. Sin embargo, podemos usar el mismo enfoque con los juegos: mantener todo el estado en la pila y crear un nuevo objeto con nuestros cambios en la llamada recursiva. De esta manera, cada cuadro es una instantánea sin estado del juego, donde cada cuadro simplemente crea un nuevo objeto con los cambios deseados de los objetos sin estado que necesitan actualizarse. El pseudocódigo para esto podría ser:
Las versiones imperativas y funcionales son idénticas, pero la versión funcional claramente no utiliza un estado mutable. El código funcional mantiene todo el estado en la pila; lo bueno de este enfoque es que, si algo sale mal, la depuración es fácil, todo lo que necesita es un seguimiento de la pila.
Esto escala a cualquier número de objetos en el juego, porque todos los objetos (o colecciones de objetos relacionados) se pueden representar en su propio hilo.
En lenguajes funcionales, en lugar de mutar el estado de los objetos, simplemente devolvemos un nuevo objeto con los cambios que queremos. Es más eficiente de lo que parece. Las estructuras de datos, por ejemplo, son muy fáciles de representar como estructuras de datos inmutables. Las pilas, por ejemplo, son notoriamente fáciles de implementar:
El código anterior construye dos listas inmutables, las agrega juntas para hacer una nueva lista y agrega los resultados. No se utiliza ningún estado mutable en ninguna parte de la aplicación. Parece un poco voluminoso, pero eso es solo porque C # es un lenguaje detallado. Aquí está el programa equivalente en F #:
No se necesita mutable para crear y manipular listas. Casi todas las estructuras de datos se pueden convertir fácilmente en sus equivalentes funcionales. Escribí una página aquí que proporciona implementaciones inmutables de pilas, colas, montones izquierdistas, árboles rojo-negros, listas perezosas. Ningún fragmento de código contiene ningún estado mutable. Para "mutar" un árbol, creo uno nuevo con el nuevo nodo que quiero. Esto es muy eficiente porque no necesito hacer una copia de cada nodo en el árbol, puedo reutilizar los viejos en mi nuevo árbol.
Usando un ejemplo más significativo, también escribí este analizador SQL que no tiene estado (o al menos mi código no tiene estado, no sé si la biblioteca subyacente no tiene estado).
La programación sin estado es tan expresiva y poderosa como la programación con estado, solo requiere un poco de práctica para entrenarse para comenzar a pensar sin estado. Por supuesto, "programación sin estado cuando sea posible, programación con estado cuando sea necesario" parece ser el lema de los lenguajes funcionales más impuros. No hay daño en recurrir a las mutables cuando el enfoque funcional no es tan limpio o eficiente.
fuente
Respuesta corta: no puedes.
Entonces, ¿cuál es el alboroto sobre la inmutabilidad?
Si conoce bien el lenguaje imperativo, entonces sabe que "los globales son malos". ¿Por qué? Porque introducen (o tienen el potencial de introducir) algunas dependencias muy difíciles de desenredar en su código. Y las dependencias no son buenas; quieres que tu código sea modular . Las partes del programa no influyen en otras partes lo menos posible. Y FP que lleva al Santo Grial de la modularidad: no hay efectos secundarios en absoluto . Solo tienes tu f (x) = y. Pon x en, saca y. No hay cambios en x ni nada más. FP te hace dejar de pensar en el estado y comenzar a pensar en términos de valores. Todas sus funciones simplemente reciben valores y producen nuevos valores.
Esto tiene varias ventajas.
En primer lugar, sin efectos secundarios significa programas más simples, más fáciles de razonar. No se preocupe si la introducción de una nueva parte del programa va a interferir y bloquear una parte existente y funcional.
En segundo lugar, esto hace que el programa sea paralelizable trivialmente (la paralelización eficiente es otra cuestión).
En tercer lugar, hay algunas posibles ventajas de rendimiento. Digamos que tienes una función:
Ahora pones un valor de 3 y obtienes un valor de 6. Cada vez. Pero también puedes hacerlo imperativo, ¿verdad? Sí. Pero el problema es que, en imperativo, puedes hacer aún más . Puedo hacer:
pero también podría hacer
El compilador imperativo no sabe si voy a tener efectos secundarios o no, lo que hace que sea más difícil de optimizar (es decir, el doble 2 no tiene por qué ser 4 cada vez). El funcional sabe que no lo haré, por lo tanto, puede optimizar cada vez que ve "doble 2".
Ahora, aunque crear nuevos valores cada vez parece increíblemente derrochador para tipos complejos de valores en términos de memoria de la computadora, no tiene por qué ser así. Porque, si tiene f (x) = y, y los valores x e y son "casi iguales" (por ejemplo, árboles que difieren solo en unas pocas hojas), entonces x e y pueden compartir partes de la memoria, porque ninguno de ellos mutará .
Entonces, si esta cosa inmutable es tan genial, ¿por qué respondí que no puedes hacer nada útil sin un estado mutable? Bueno, sin mutabilidad, todo su programa sería una función gigante f (x) = y. Y lo mismo ocurriría con todas las partes de su programa: solo funciones y funciones en el sentido "puro". Como dije, esto significa f (x) = y cada vez. Entonces, por ejemplo, readFile ("myFile.txt") necesitaría devolver el mismo valor de cadena cada vez. No muy útil
Por lo tanto, cada FP proporciona algún medio de estado mutante. Los lenguajes funcionales "puros" (p. Ej., Haskell) hacen esto utilizando conceptos algo aterradores como las mónadas, mientras que los "impuros" (p. Ej., ML) lo permiten directamente.
Y, por supuesto, los lenguajes funcionales vienen con una serie de otras ventajas que hacen que la programación sea más eficiente, como funciones de primera clase, etc.
fuente
int double(x){ return x * (++y); }
que la actual seguirá siendo 4, aunque seguirá teniendo un efecto secundario no anunciado, mientras++y
que devolverá 6.Tenga en cuenta que decir que la programación funcional no tiene 'estado' es un poco engañoso y podría ser la causa de la confusión. Definitivamente no tiene un "estado mutable", pero aún puede tener valores manipulados; simplemente no se pueden cambiar en el lugar (por ejemplo, debe crear nuevos valores a partir de los valores anteriores).
Esta es una simplificación excesiva, pero imagine que tenía un lenguaje OO, donde todas las propiedades de las clases se establecen una sola vez en el constructor, todos los métodos son funciones estáticas. Todavía podría realizar casi cualquier cálculo haciendo que los métodos tomen objetos que contengan todos los valores que necesitan para sus cálculos y luego devuelvan nuevos objetos con el resultado (incluso una nueva instancia del mismo objeto).
Puede ser 'difícil' traducir el código existente a este paradigma, pero eso se debe a que realmente requiere una forma completamente diferente de pensar sobre el código. Como efecto secundario, aunque en la mayoría de los casos tienes muchas oportunidades de paralelismo gratis.
Anexo: (Con respecto a su edición de cómo realizar un seguimiento de los valores que necesitan cambiar)
Por supuesto, se almacenarían en una estructura de datos inmutable ...
Esta no es una 'solución' sugerida, pero la forma más fácil de ver que esto siempre funcionará es que podría almacenar estos valores inmutables en una estructura similar a un mapa (diccionario / tabla hash), con un 'nombre de variable'.
Obviamente, en soluciones prácticas usaría un enfoque más sensato, pero esto muestra que, en el peor de los casos, si nada más funcionara, podría 'simular' un estado mutable con un mapa que lleve a través de su árbol de invocación.
fuente
Creo que hay un ligero malentendido. Los programas funcionales puros tienen estado. La diferencia es cómo se modela ese estado. En la programación funcional pura, el estado es manipulado por funciones que toman algún estado y devuelven el siguiente estado. La secuenciación a través de estados se logra pasando el estado a través de una secuencia de funciones puras.
Incluso el estado mutable global se puede modelar de esta manera. En Haskell, por ejemplo, un programa es una función de un mundo a otro. Es decir, pasa en todo el universo , y el programa devuelve un nuevo universo. En la práctica, sin embargo, solo necesita pasar por las partes del universo en las que su programa está realmente interesado. Y los programas en realidad devuelven una secuencia de acciones que sirven como instrucciones para el entorno operativo en el que se ejecuta el programa.
Querías ver esto explicado en términos de programación imperativa. Bien, veamos una programación imperativa realmente simple en un lenguaje funcional.
Considera este código:
Código imperativo bastante pantanoso. No hace nada interesante, pero está bien como ilustración. Creo que estará de acuerdo en que hay un estado involucrado aquí. El valor de la variable x cambia con el tiempo. Ahora, cambiemos ligeramente la notación inventando una nueva sintaxis:
Ponga paréntesis para aclarar lo que esto significa:
Como puede ver, el estado se modela mediante una secuencia de expresiones puras que unen las variables libres de las siguientes expresiones.
Encontrará que este patrón puede modelar cualquier tipo de estado, incluso IO.
fuente
Así es como se escribe código sin estado mutable : en lugar de poner el estado cambiante en variables mutables, lo pones en los parámetros de las funciones. Y en lugar de escribir bucles, escribes funciones recursivas. Entonces, por ejemplo, este código imperativo:
se convierte en este código funcional (sintaxis tipo esquema):
o este código Haskellish
En cuanto a por qué a los programadores funcionales les gusta hacer esto (lo cual no preguntaste), mientras más partes de tu programa no tienen estado, más formas hay de juntar piezas sin que nada se rompa . El poder del paradigma sin estado no reside en la apatridia (o la pureza) per se , sino en la capacidad que le brinda para escribir funciones poderosas y reutilizables y combinarlas.
Puede encontrar un buen tutorial con muchos ejemplos en el artículo de John Hughes Por qué es importante la programación funcional .
fuente
Es solo una forma diferente de hacer lo mismo.
Considere un ejemplo simple como sumar los números 3, 5 y 10. Imagine pensar en hacerlo cambiando primero el valor de 3 agregando 5, luego agregando 10 a ese "3", luego generando el valor actual de " 3 "(18). Esto parece patentemente ridículo, pero en esencia es la forma en que a menudo se realiza la programación imperativa basada en estado. De hecho, puede tener muchos "3" diferentes que tienen el valor 3, pero son diferentes. Todo esto parece extraño, porque hemos estado muy arraigados con la idea, enormemente sensata, de que los números son inmutables.
Ahora piense en sumar 3, 5 y 10 cuando considere que los valores son inmutables. Sumas 3 y 5 para producir otro valor, 8, luego agregas 10 a ese valor para producir otro valor, 18.
Estas son formas equivalentes de hacer lo mismo. Toda la información necesaria existe en ambos métodos, pero en diferentes formas. En uno, la información existe como estado y en las reglas para cambiar de estado. En el otro, la información existe en datos inmutables y definiciones funcionales.
fuente
Llegué tarde a la discusión, pero quería agregar algunos puntos para las personas que tienen dificultades con la programación funcional.
Primero la forma imperativa (en pseudocódigo)
Ahora la forma funcional (en pseudocódigo). Me estoy apoyando mucho en el operador ternario porque quiero que las personas de entornos imperativos puedan leer este código. Entonces, si no usa mucho el operador ternario (siempre lo evité en mis días imperativos), así es como funciona.
Puede encadenar la expresión ternaria colocando una nueva expresión ternaria en lugar de la expresión falsa
Con eso en mente, aquí está la versión funcional.
Este es un ejemplo trivial. Si esto moviera a las personas en un mundo de juegos, tendrías que introducir efectos secundarios como dibujar la posición actual del objeto en la pantalla e introducir un poco de retraso en cada llamada en función de lo rápido que se mueve el objeto. Pero todavía no necesitarías un estado mutable.
La lección es que los lenguajes funcionales "mutan" al llamar a la función con diferentes parámetros. Obviamente, esto realmente no muta ninguna variable, pero así es como se obtiene un efecto similar. Esto significa que tendrá que acostumbrarse a pensar de forma recursiva si desea hacer una programación funcional.
Aprender a pensar de manera recursiva no es difícil, pero requiere práctica y herramientas. Esa pequeña sección en ese libro "Learn Java" donde usaron la recursión para calcular factorial no es suficiente. Necesita un conjunto de herramientas de habilidades como hacer procesos iterativos a partir de la recursividad (es por eso que la recursividad de cola es esencial para el lenguaje funcional), continuaciones, invariantes, etc. No haría la programación OO sin aprender sobre modificadores de acceso, interfaces, etc. Lo mismo para programación funcional.
Mi recomendación es hacer Little Schemer (tenga en cuenta que digo "hacer" y no "leer") y luego hacer todos los ejercicios en SICP. Cuando termines, tendrás un cerebro diferente al que comenzaste.
fuente
De hecho, es bastante fácil tener algo que parezca un estado mutable incluso en idiomas sin estado mutable.
Considere una función con tipo
s -> (a, s)
. Traduciendo de la sintaxis de Haskell, significa una función que toma un parámetro del tipo "s
" y devuelve un par de valores, de los tipos "a
" y "s
". Sis
es el tipo de nuestro estado, esta función toma un estado y devuelve un nuevo estado, y posiblemente un valor (siempre puede devolver "unidad"()
, que es equivalente a "void
" en C / C ++, como "a
" tipo). Si encadena varias llamadas de funciones con tipos como este (obtener el estado devuelto de una función y pasarlo a la siguiente), tiene un estado "mutable" (de hecho, está en cada función creando un nuevo estado y abandonando el anterior) )Puede ser más fácil de entender si imagina el estado mutable como el "espacio" donde se está ejecutando su programa, y luego piensa en la dimensión del tiempo. En el instante t1, el "espacio" está en una determinada condición (por ejemplo, alguna ubicación de memoria tiene el valor 5). En un instante posterior t2, está en una condición diferente (por ejemplo, esa ubicación de memoria ahora tiene el valor 10). Cada uno de estos "cortes" de tiempo es un estado y es inmutable (no puede retroceder en el tiempo para cambiarlos). Entonces, desde este punto de vista, pasaste del espacio-tiempo completo con una flecha de tiempo (tu estado mutable) a un conjunto de segmentos de espacio-tiempo (varios estados inmutables), y tu programa solo trata cada segmento como un valor y calcula cada uno de ellos como una función aplicada a la anterior.
OK, tal vez eso no fue más fácil de entender :-)
Puede parecer ineficaz representar explícitamente todo el estado del programa como un valor, que debe crearse solo para descartarse al siguiente instante (justo después de crear uno nuevo). Para algunos algoritmos puede ser natural, pero cuando no lo es, hay otro truco. En lugar de un estado real, puede usar un estado falso que no es más que un marcador (llamemos el tipo de este estado falso
State#
). Este estado falso existe desde el punto de vista del lenguaje, y se pasa como cualquier otro valor, pero el compilador lo omite por completo al generar el código de máquina. Solo sirve para marcar la secuencia de ejecución.Como ejemplo, supongamos que el compilador nos da las siguientes funciones:
Al traducir de estas declaraciones tipo Haskell,
readRef
recibe algo parecido a un puntero o un identificador a un valor de tipo "a
" y al estado falso, y devuelve el valor de tipo "a
" señalado por el primer parámetro y un nuevo estado falso.writeRef
es similar, pero cambia el valor señalado en su lugar.Si llama
readRef
y luego pasa el estado falso devuelto porwriteRef
(tal vez con otras llamadas a funciones no relacionadas en el medio; estos valores de estado crean una "cadena" de llamadas a funciones), devolverá el valor escrito. PuedewriteRef
volver a llamar con el mismo puntero / identificador y escribirá en la misma ubicación de memoria, pero, dado que conceptualmente devuelve un nuevo estado (falso), el estado (falso) sigue siendo inmutable (se ha creado uno nuevo) "). El compilador llamará a las funciones en el orden en que tendría que llamarlas si hubiera una variable de estado real que tuviera que calcularse, pero el único estado que existe es el estado completo (mutable) del hardware real.(Los que conocen Haskell notará he simplificado mucho las cosas y oitirán varios detalles importantes. Para aquellos que quieren ver más detalles, ver
Control.Monad.State
desde elmtl
, y alST s
yIO
(también conocidos comoST RealWorld
mónadas).)Quizás se pregunte por qué hacerlo de una manera tan indirecta (en lugar de simplemente tener un estado mutable en el idioma). La verdadera ventaja es que ha reificado el estado de su programa. Lo que antes era implícito (el estado de su programa era global, permitiendo cosas como la acción a distancia ) ahora es explícito. Las funciones que no reciben y devuelven el estado no pueden modificarlo ni ser influenciados por él; son "puros" Aún mejor, puede tener hilos de estado separados, y con un poco de magia tipográfica, se pueden usar para incrustar un cálculo imperativo dentro de uno puro, sin hacerlo impuro (la
ST
mónada en Haskell es la que normalmente se usa para este truco; elState#
que mencioné anteriormente es, de hecho, GHCState# s
, utilizado por su implementación de laST
yIO
mónadas).fuente
La programación funcional evita el estado y enfatizafuncionalidad Nunca hay tal cosa como ningún estado, aunque el estado en realidad podría ser algo inmutable o integrado en la arquitectura de lo que está trabajando. Considere la diferencia entre un servidor web estático que solo carga archivos del sistema de archivos versus un programa que implementa un cubo de Rubik. El primero se implementará en términos de funciones diseñadas para convertir una solicitud en una solicitud de ruta de archivo en una respuesta del contenido de ese archivo. Prácticamente no se necesita ningún estado más allá de una pequeña configuración (el 'estado' del sistema de archivos está realmente fuera del alcance del programa. El programa funciona de la misma manera independientemente del estado en que se encuentren los archivos). Sin embargo, en este último caso, debe modelar el cubo y la implementación de su programa de cómo las operaciones en ese cubo cambian su estado.
fuente
Además de las excelentes respuestas que otros están dando, piense en las clases
Integer
yString
en Java. Las instancias de estas clases son inmutables, pero eso no hace que las clases sean inútiles solo porque sus instancias no se pueden cambiar. La inmutabilidad te da cierta seguridad. Sabes que si usas una instancia de String o Integer como clave para aMap
, la clave no se puede cambiar. Compare esto con laDate
clase en Java:¡Has cambiado silenciosamente una clave en tu mapa! Trabajar con objetos inmutables, como en la programación funcional, es mucho más limpio. Es más fácil razonar sobre los efectos secundarios que se producen, ¡ninguno! Esto significa que es más fácil para el programador y también más fácil para el optimizador.
fuente
Para aplicaciones altamente interactivas como los juegos, la programación funcional reactiva es su amiga: si puede formular las propiedades del mundo de su juego como valores que varían en el tiempo (y / o transmisiones de eventos), ¡está listo! Estas fórmulas serán a veces incluso más naturales y reveladoras que mutar un estado, por ejemplo, para una bola en movimiento, puede usar directamente la conocida ley x = v * t . Y lo que es mejor, las reglas del juego escritas de esta manera componen mejor que las abstracciones orientadas a objetos. Por ejemplo, en este caso, la velocidad de la pelota también puede ser un valor variable en el tiempo, que depende del flujo del evento que consiste en las colisiones de la pelota. Para consideraciones de diseño más concretas, vea Hacer juegos en Elm .
fuente
Usando un poco de creatividad y coincidencia de patrones, se han creado juegos sin estado:
así como demos rodantes:
y visualizaciones:
fuente
Así es como FORTRAN funcionaría sin bloques COMUNES: escribirías métodos que tuvieran los valores que pasaste y las variables locales. Eso es.
La programación orientada a objetos nos unió estado y comportamiento, pero fue una idea nueva cuando lo encontré por primera vez en C ++ en 1994.
¡Dios, yo era un programador funcional cuando era ingeniero mecánico y no lo sabía!
fuente
Tenga en cuenta: los lenguajes funcionales son Turing completos. Por lo tanto, cualquier tarea útil que realice en un lenguaje imperitivo puede realizarse en un lenguaje funcional. Al final del día, creo que hay algo que decir sobre un enfoque híbrido. Lenguajes como F # y Clojure (y estoy seguro de que otros) fomentan el diseño sin estado, pero permiten la mutabilidad cuando sea necesario.
fuente
No puede tener un lenguaje funcional puro que sea útil. Siempre habrá un nivel de mutabilidad con el que tendrá que lidiar, IO es un ejemplo.
Piense en los lenguajes funcionales como una herramienta más que utiliza. Es bueno para ciertas cosas, pero no para otras. El ejemplo del juego que proporcionó podría no ser la mejor manera de usar un lenguaje funcional, al menos la pantalla tendrá un estado mutable que no puede hacer nada con FP. La forma en que piensa el problema y el tipo de problemas que resuelve con FP serán diferentes de los que está acostumbrado con la programación imperativa.
fuente
Mediante el uso de mucha recursividad.
Tic Tac Toe en F # (Un lenguaje funcional).
fuente
Esto es muy simple. Puede usar tantas variables como desee en la programación funcional ... pero solo si son variables locales (contenidas dentro de funciones). Así que simplemente envuelva su código en funciones, pase valores de un lado a otro entre esas funciones (como parámetros pasados y valores devueltos) ... ¡y eso es todo!
Aquí hay un ejemplo:
fuente