functional-programming state

functional programming - ¿Cómo puedes hacer algo útil sin estado mutable?



functional-programming state (17)

He estado leyendo muchas cosas sobre programación funcional últimamente, 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 al eliminar el tablero de instrumentos: el producto terminado puede ser más simple, pero buena suerte al interactuar con los usuarios finales.

Casi todas las aplicaciones de usuario en las que puedo pensar involucran el estado como un concepto central. Si escribe un documento (o una publicación SO), el estado cambia con cada nueva entrada. 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 trata este tema, está escrito en un funcional muy técnico que asume un fondo de FP pesado que no tengo. ¿Alguien sabe una manera de explicarle esto a alguien con una buena y sólida comprensión de la codificación imperativa pero quién es un n00b completo en el lado funcional?

EDITAR: Un montón de respuestas parecen estar tratando de convencerme de las ventajas de los valores inmutables. Consigo esa parte. Tiene perfecto sentido. Lo que no entiendo es cómo se puede hacer un seguimiento de los valores que tienen que cambiar y cambiar constantemente, sin variables mutables.


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?

Si estás interesado, here''s una serie de artículos que describen la programación de juegos con Erlang.

Probablemente no le gustará esta respuesta, pero no obtendrá un programa funcional hasta que lo use. Puedo publicar ejemplos de código y decir "Aquí, ¿no ves? ", Pero si no entiendes la sintaxis y los principios subyacentes, entonces tus ojos simplemente se ponen vidriosos. 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, 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 for-loop, por ejemplo, se convierte en recursión:

// Imperative let printTo x = for a in 1 .. x do printfn "%i" a // Recursive let printTo x = let rec loop a = if a <= x then printfn "%i" a; loop (a + 1) loop 1

No es muy bonito, pero obtuvimos el mismo efecto sin mutación. Por supuesto, siempre que sea posible, nos gusta evitar los bucles y abstraerlos:

// Preferred let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

El método Seq.iter enumera a través de la colección e invoca 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 fotograma es una instantánea sin estado del juego, donde cada fotograma simplemente crea un objeto nuevo con los cambios deseados de cualquier objeto sin estado que deba actualizarse. El pseudocódigo para esto podría ser:

// imperative version pacman = new pacman(0, 0) while true if key = UP then pacman.y++ elif key = DOWN then pacman.y-- elif key = LEFT then pacman.x-- elif key = UP then pacman.x++ render(pacman) // functional version let rec loop pacman = render(pacman) let x, y = switch(key) case LEFT: pacman.x - 1, pacman.y case RIGHT: pacman.x + 1, pacman.y case UP: pacman.x, pacman.y - 1 case DOWN: pacman.x, pacman.y + 1 loop(new pacman(x, y))

Las versiones imperativas y funcionales son idénticas, pero la versión funcional claramente no usa 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 se amplía 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.

Casi todas las aplicaciones de usuario en las que puedo pensar involucran el estado como un concepto central.

En los lenguajes funcionales, en lugar de mutar el estado de los objetos, simplemente devolvemos un nuevo objeto con los cambios que deseamos. 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 notablemente fáciles de implementar:

using System; namespace ConsoleApplication1 { static class Stack { public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); } public static Stack<T> Append<T>(Stack<T> x, Stack<T> y) { return x == null ? y : Cons(x.Head, Append(x.Tail, y)); } public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } } } class Stack<T> { public readonly T Head; public readonly Stack<T> Tail; public Stack(T hd, Stack<T> tl) { this.Head = hd; this.Tail = tl; } } class Program { static void Main(string[] args) { Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null)))); Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null)))); Stack<int> z = Stack.Append(x, y); Stack.Iter(z, a => Console.WriteLine(a)); Console.ReadKey(true); } } }

El código anterior construye dos listas inmutables, las agrega 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 #:

type ''a stack = | Cons of ''a * ''a stack | Nil let rec append x y = match x with | Cons(hd, tl) -> Cons(hd, append tl y) | Nil -> y let rec iter f = function | Cons(hd, tl) -> f(hd); iter f tl | Nil -> () let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil)))) let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil)))) let z = append x y iter (fun a -> printfn "%i" a) z

No es necesario mutar para crear y manipular listas. Casi todas las estructuras de datos se pueden convertir fácilmente en sus equivalentes funcionales. Escribí una página here que proporciona implementaciones inmutables de pilas, colas, montones de izquierdas, árboles rojo-negros, listas perezosas. Ni un solo 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 de SQL que es totalmente sin estado (o al menos mi código es sin estado, no sé si la biblioteca de lexing subyacente es sin 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 y comenzar a pensar sin estado. Por supuesto, "programación sin estado cuando es posible, programación con estado donde sea necesario" parece ser el lema de la mayoría de los lenguajes funcionales impuros. No hay daño en recurrir a los mutables cuando el enfoque funcional no es tan limpio o eficiente.


Además de las excelentes respuestas que otros están dando, piense en las clases Integer y String en Java. Las instancias de estas clases son inmutables, pero eso no hace que las clases sean inútiles simplemente porque sus instancias no se pueden cambiar. La inmutabilidad te da algo de seguridad. Sabe que si utiliza una instancia de String o Integer como la clave para un Map , la clave no se puede cambiar. Compare esto con la clase de Date en Java:

Date date = new Date(); mymap.put(date, date.toString()); // Some time later: date.setTime(new Date().getTime());

¡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 qué efectos secundarios ocurren, ¡ninguno! Esto significa que es más fácil para el programador y también más fácil para el optimizador.


Creo que hay un pequeño malentendido. Los programas puramente funcionales 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 luego pasando el estado a través de una secuencia de funciones puras.

Incluso el estado mutable global puede modelarse de esta manera. En Haskell, por ejemplo, un programa es una función de un mundo a un mundo. Es decir, se pasa en todo el universo y el programa devuelve un nuevo universo. Sin embargo, en la práctica, solo necesitas pasar a las partes del universo en las que tu programa está realmente interesado. Y los programas realmente 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.

Considere este código:

int x = 1; int y = x + 1; x = x + y; return x;

Código imperativo bastante bog estándar. No hace nada interesante, pero eso está bien para la ilustración. Creo que estarás de acuerdo en que hay un estado involucrado aquí. El valor de la variable x cambia con el tiempo. Ahora, cambiemos un poco la notación inventando una nueva sintaxis:

let x = 1 in let y = x + 1 in let z = x + y in z

Ponga paréntesis para aclarar lo que esto significa:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

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.


De hecho, es bastante fácil tener algo que se parece al estado mutable incluso en idiomas sin estado mutable.

Considere una función con el tipo s -> (a, s) . Al traducir desde la sintaxis de Haskell, significa una función que toma un parámetro de tipo " s " y devuelve un par de valores, de tipos " a " y " s ". Si s 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" aka () , que es equivalente a " void " en C / C ++, como el tipo " a "). 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). ).

Podría ser más fácil de entender si imagina el estado mutable como el "espacio" donde se ejecuta su programa y luego piensa en la dimensión temporal. En el instante t1, el "espacio" está en una cierta condición (por ejemplo, algunas ubicaciones de memoria tienen valor 5). En un instante posterior t2, está en una condición diferente (por ejemplo, la ubicación de la memoria ahora tiene 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, pasó del espacio-tiempo completo con una flecha de tiempo (su estado mutable) a un conjunto de segmentos de espacio-tiempo (varios estados inmutables), y su programa simplemente trata cada segmento como un valor y calcula cada uno. De ellas como función aplicada a la anterior.

OK, tal vez eso no fue más fácil de entender :-)

Puede parecer ineficiente representar explícitamente el estado del programa completo como un valor, que debe crearse solo para descartarlo en el 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, puedes usar un estado falso que no es más que un marcador (llamemos al tipo de este estado falso estado State# ). Este estado falso existe desde el punto de vista del lenguaje y se pasa como cualquier otro valor, pero el compilador lo omite completamente al generar el código de la máquina. Solo sirve para marcar la secuencia de ejecución.

Como ejemplo, supongamos que el compilador nos da las siguientes funciones:

readRef :: Ref a -> State# -> (a, State#) writeRef :: Ref a -> a -> State# -> (a, State#)

Al traducir de estas declaraciones similares a Haskell, readRef recibe algo que se parece 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 al que apunta.

Si llama a readRef y luego le pasa el estado falso devuelto por writeRef (quizás con otras llamadas a funciones no relacionadas en el medio; estos valores de estado crean una "cadena" de llamadas de función), devolverá el valor escrito. Puede writeRef llamar a writeRef con el mismo puntero / identificador y escribirá en la misma ubicación de memoria, pero, como conceptualmente está devolviendo un nuevo estado (falso), el estado (falso) sigue siendo imutable (uno nuevo ha sido " creado"). 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.

(Aquellos que conocen Haskell notarán que simplifiqué mucho las cosas y puse varios detalles importantes. Para aquellos que quieran ver más detalles, eche un vistazo a Control.Monad.State desde el mtl , y a los ST s y IO (también conocido como ST RealWorld ).

Podría preguntarse 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 reified el estado de su programa. Lo que antes era implícito (su estado de programa era global, permitiendo cosas como acciones a distancia ) ahora es explícito. Las funciones que no reciben y devuelven el estado no pueden modificarlo o ser influenciados por él; son "puros". Aún mejor, puedes tener hilos de estado separados, y con un poco de magia tipo, se pueden usar para incrustar un cálculo imperativo dentro de uno puro, sin hacerlo impuro (la mónada ST en Haskell es la que normalmente se usa para este truco ; el State# que mencioné anteriormente es, de hecho, el State# s GHC, utilizado por su implementación de las mónadas ST y IO ).


Esa es la forma en que FORTRAN funcionaría sin bloques COMUNES: escribiría métodos que tuvieran los valores que pasó y las variables locales. Eso es.

La programación orientada a objetos nos unió al estado y al comportamiento, pero fue una idea nueva cuando la encontré por primera vez en C ++ en 1994.

Caray, yo era un programador funcional cuando era ingeniero mecánico y no lo sabía.


Esto es muy simple.Puede usar tantas variables como desee en la programación funcional ... pero solo si son variables locales (contenidas dentro de las funciones). Así que simplemente ajuste su código en funciones, pase valores de ida y vuelta entre esas funciones (como parámetros pasados ​​y valores devueltos) ... ¡y eso es todo!

Aquí hay un ejemplo:

function ReadDataFromKeyboard() { $input_values = $_POST[]; return $input_values; } function ProcessInformation($input_values) { if ($input_values[''a''] > 10) return ($input_values[''a''] + $input_values[''b''] + 3); else if ($input_values[''a''] > 5) return ($input_values[''b''] * 3); else return ($input_values[''b''] - $input_values[''a''] - 7); } function DisplayToPage($data) { print "Based your input, the answer is: "; print $data; print "/n"; } /* begin: */ DisplayToPage ( ProcessInformation ( GetDataFromKeyboard() ) );


La programación funcional evita el estado y enfatiza la funcionalidad. Nunca existe un estado sin estado, aunque en realidad podría ser algo inmutable o incorporado a la arquitectura de lo que estás trabajando. Considere la diferencia entre un servidor web estático que simplemente carga archivos del sistema de archivos en lugar de 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 ruta de acceso de archivo en una respuesta del contenido de ese archivo. Prácticamente no se necesita ningún estado más allá de un poquito de 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.


Llego tarde a la discusión, pero quería agregar algunos puntos a las personas que tienen dificultades con la programación funcional.

  1. Los lenguajes funcionales mantienen exactamente las mismas actualizaciones de estado que los lenguajes imperativos, pero lo hacen pasando el estado actualizado a las llamadas de función subsiguientes . Aquí hay un ejemplo muy simple de viajar por una recta numérica. Su estado es su ubicación actual.

Primero la forma imperativa (en pseudocódigo)

moveTo(dest, cur): while (cur != dest): if (cur < dest): cur += 1 else: cur -= 1 return cur

Ahora la forma funcional (en pseudocódigo). Me estoy apoyando mucho en el operador ternario porque quiero que las personas de orígenes imperativos puedan leer este código. Entonces, si no usas mucho el operador ternario (siempre lo evité en mis días imperativos), así es como funciona.

predicate ? if-true-expression : if-false-expression

Puede encadenar la expresión ternaria colocando una nueva expresión ternaria en lugar de la expresión falsa

predicate1 ? if-true1-expression : predicate2 ? if-true2-expression : else-expression

Así que con eso en mente, aquí está la versión funcional.

moveTo(dest, cur): return ( cur == dest ? return cur : cur < dest ? moveTo(dest, cur + 1) : moveTo(dest, cur - 1) )

Este es un ejemplo trivial. Si esto moviera a las personas en un mundo de juego, 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 la velocidad con que se mueve el objeto. Pero todavía no necesitarías un estado mutable.

  1. La lección es que los lenguajes funcionales "mutan" el estado 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 realizar una programación funcional.

  2. Aprender a pensar recursivamente no es difícil, pero requiere práctica y un conjunto de herramientas. Esa pequeña sección de ese libro "Learn Java" donde usaron la recursión para calcular factorial no lo corta. Necesita un conjunto de herramientas de habilidades como hacer procesos iterativos fuera de la recursión (es por eso que la recursión de la 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. Para la programación funcional.

Mi recomendación es hacer el Little Schemer (tenga en cuenta que digo "hacer" y no "leer") y luego hacer todos los ejercicios en SICP. Cuando hayas terminado, tendrás un cerebro diferente al de cuando empezaste.


Para aplicaciones altamente interactivas, como los juegos, la Programación reactiva funcional es su amiga: si puede formular las propiedades del mundo de su juego como valores variables en el tiempo (y / o flujos de eventos), ¡está listo! Estas fórmulas a veces serán incluso más naturales y reveladoras de intenciones 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 se 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 de la secuencia de eventos que consiste en las colisiones de la pelota. Para consideraciones de diseño más concretas, vea Cómo hacer juegos en Elm .


Respuesta corta: no puedes.

Entonces, ¿cuál es el escándalo de la inmutabilidad entonces?

Si estás bien versado en lenguaje imperativo, entonces sabes que "los globales son malos". ¿Por qué? Porque introducen (o tienen el potencial de introducir) algunas dependencias muy difíciles de desentrañar 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 te lleva al santo grial de la modularidad: no tiene efectos secundarios. Solo tienes tu f (x) = y. Poner x dentro, sacar y salir. No hay cambios en x ni nada más. FP te hace dejar de pensar en el estado y empezar 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 de que la introducción de una nueva parte del programa va a interferir y bloquear una parte existente que funcione.

En segundo lugar, esto hace que el programa sea trivialmente paralelizable (la paralelización eficiente es otra cuestión).

En tercer lugar, hay algunas posibles ventajas de rendimiento. Digamos que tienes una función:

double x = 2 * x

Ahora, pones un valor de 3 en, y obtienes un valor de 6 en. Cada vez. Pero también puedes hacerlo en imperativo, ¿verdad? Sí. Pero el problema es que en imperativo, puedes hacer aún más . Puedo hacer:

int y = 2; int double(x){ return x * y; }

pero tambien podria hacer

int y = 2; int double(x){ return x * (y++); }

El imperativo compilador no sabe si voy a tener efectos secundarios o no, lo que dificulta la optimización (es decir, el doble 2 no tiene que ser 4 cada vez). El funcional sabe que no lo haré, por lo tanto, puede optimizar cada vez que vea "doble 2".

Ahora, aunque crear nuevos valores cada vez parezca increíblemente inútil para tipos complejos de valores en términos de memoria de computadora, no tiene que ser así. Porque, si tiene f (x) = y, y los valores x e y son "casi iguales" (p. Ej., Árboles que se diferencian solo en unas pocas hojas), x e y pueden compartir partes de la memoria, ya que ninguno de ellos mutará .

Entonces, si esta cosa inmutable es tan grande, ¿por qué respondí que no se puede hacer nada útil sin un estado mutable? Bueno, sin mutabilidad, su programa completo sería una función f (x) = y gigante. Y lo mismo sucedería con todas las partes de su programa: solo funciona, y funciona en el sentido "puro" en ese sentido. Como dije, esto significa f (x) = y cada vez. Entonces, por ejemplo, readFile ("myFile.txt") tendría que devolver el mismo valor de cadena cada vez. No es demasiado útil.

Por lo tanto, cada FP proporciona algún medio de estado de mutación. Los lenguajes funcionales "puros" (por ejemplo, Haskell) hacen esto utilizando conceptos un tanto aterradores como las mónadas, mientras que los "impuros" (por ejemplo, ML) lo permiten directamente.

Y, por supuesto, los lenguajes funcionales vienen con una serie de otras funciones que hacen que la programación sea más eficiente, como las funciones de primera clase, etc.


Son solo formas diferentes 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 agregándole 5, luego agregando 10 a ese "3", y luego emitiendo el valor actual de " 3 "(18). Esto parece evidentemente ridículo, pero en esencia es la forma en que se realiza la programación imperativa basada en el estado. De hecho, puedes tener muchos "3" diferentes que tienen el valor 3, pero son diferentes. Todo esto parece extraño, porque hemos estado tan arraigados con la idea, enormemente sensata, de que los números son inmutables.

Ahora piense en agregar 3, 5 y 10 cuando considere que los valores son inmutables. Agrega 3 y 5 para producir otro valor, 8, luego agrega 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 formas diferentes. En uno, la información existe como estado y en las reglas para cambiar el estado. En el otro, la información existe en datos inmutables y definiciones funcionales.


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 que son manipulados; simplemente no se pueden cambiar in situ (por ejemplo, debe crear nuevos valores a partir de los valores antiguos).

Esto es una simplificación excesiva, pero imagina que tenías 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. Aún puede realizar casi cualquier cálculo si los métodos toman objetos que contienen todos los valores que necesitan para sus cálculos y luego devuelven objetos nuevos con el resultado (tal vez 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 en el código. Sin embargo, como efecto secundario, 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 deben cambiar)
Se almacenarían en una estructura de datos inmutables, por supuesto ...

Esta no es una "solución" sugerida, pero la forma más fácil de ver que esto siempre funcionará es que usted podría almacenar estos valores inmutables en una estructura similar a un mapa (tabla / tabla de hash), codificada por un "nombre de variable".

Obviamente, en las soluciones prácticas usaría un enfoque más sano, pero esto muestra que, en el peor de los casos, si no hubiera otra cosa que funcionara, podría "simular" un estado mutable con un mapa que lleve a través de su árbol de invocación.



Así es como escribe el código sin estado mutable : en lugar de poner el estado cambiante en variables mutables, lo pone en los parámetros de las funciones. Y en lugar de escribir bucles, escribes funciones recursivas. Así por ejemplo este código imperativo:

f_imperative(y) { local x; x := e; while p(x, y) do x := g(x, y) return h(x, y) }

se convierte en este código funcional (sintaxis Scheme-like):

(define (f-functional y) (letrec ( (f-helper (lambda (x y) (if (p x y) (f-helper (g x y) y) (h x y))))) (f-helper e y)))

o este código Haskellish

f_fun y = h x_final y where x_initial = e x_final = loop x_initial loop x = if p x y then loop (g x y) else x

En cuanto a por qué a los programadores funcionales les gusta hacer esto (lo que usted no preguntó), cuantas más partes de su programa sean apátridas, más formas hay de juntar las piezas sin que se rompa nada . El poder del paradigma sin estado no radica en la apatridia (o pureza) per se , sino en la capacidad que tiene para escribir funciones potentes y reutilizables y combinarlas.

Puede encontrar un buen tutorial con muchos ejemplos en el artículo de Por qué funciona la programación funcional de John Hughes.



No puedes 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. Es posible que el ejemplo de juego que dio no sea la mejor manera de usar un lenguaje funcional, al menos la pantalla tendrá un estado mutable sobre el que no puede hacer nada con FP. La forma en que piense en un problema y el tipo de problemas que resuelva con FP será diferente de los que está acostumbrado con la programación imperativa.


Ten en cuenta: los lenguajes funcionales son Turing completos. Por lo tanto, cualquier tarea útil que realizaría en un lenguaje imperitivo puede realizarse en un lenguaje funcional. Sin embargo, al final del día, creo que hay algo que decir acerca de un enfoque híbrido. Los idiomas como F # y Clojure (y estoy seguro de que otros) fomentan el diseño sin estado, pero permiten la mutabilidad cuando es necesario.