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


265

Ú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.



1
Mi humilde opinión personal es que es como la fuerza y ​​el dinero. Se aplica la ley de rendimientos decrecientes. Si eres bastante fuerte, puede haber pocos incentivos para fortalecerte un poco, pero no hace daño trabajar en ello (y algunas personas lo hacen con pasión). Lo mismo se aplica al estado mutable global. Es mi preferencia personal aceptar que a medida que avanza mi habilidad de codificación, es bueno limitar la cantidad de estado mutable global en mi código. Puede que nunca sea perfecto, pero es bueno trabajar para minimizar el estado mutable global.
AturSams

Al igual que con el dinero, se llegará a un punto en el que invertir más tiempo en él, ya no es muy útil y otras prioridades llegarán a la cima. Si, por ejemplo, alcanza la mayor cantidad de fuerza posible (según mi metáfora), puede que no sirva para ningún propósito útil e incluso podría convertirse en una carga. Pero aún es bueno esforzarse por alcanzar ese objetivo posiblemente inalcanzable e invertir recursos moderados en él.
AturSams

77
Brevemente, en FP, las funciones nunca modifican el estado. Finalmente, devolverán algo que reemplace el estado actual. Pero el estado nunca se modifica (muta) en el lugar.
jinglesthula

Hay formas de obtener la capacidad de estado sin mutación (usando la pila por lo que entiendo), pero esta pregunta es, en cierto sentido, irrelevante (a pesar de que es excelente). Es difícil hablar de manera sucinta, pero aquí hay una publicación que con suerte responde a su pregunta medium.com/@jbmilgrom/… . El TLDR es que la semántica de incluso un programa funcional con estado es inmutable, sin embargo, se manejan las ejecuciones de comunicación en blanco y negro de la función del programa.
jbmilgrom

Respuestas:


166

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, 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:

// 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 por completo y simplemente abstraerlo:

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

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:

// 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 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.

Casi todas las aplicaciones de usuario que se me ocurren implican al estado como un concepto central.

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:

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 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 #:

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 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.


77
Me gusta el ejemplo de Pacman. Pero eso podría resolver un problema solo para generar otro: ¿Qué pasa si otra cosa tiene una referencia al objeto Pacman existente? Entonces no será recolectado y reemplazado; en su lugar, terminas con dos copias del objeto, una de las cuales no es válida. ¿Cómo manejas este problema?
Mason Wheeler

9
Obviamente, necesitas crear un nuevo "algo más" con el nuevo objeto Pacman;) Por supuesto, si tomamos esa ruta demasiado lejos, terminamos recreando el gráfico de objetos para todo nuestro mundo cada vez que algo cambia. Aquí se describe un mejor enfoque ( prog21.dadgum.com/26.html ): en lugar de que los objetos se actualicen a sí mismos y todas sus dependencias, es mucho más fácil hacer que pasen mensajes sobre su estado a un bucle de eventos que maneja todos los actualización Esto hace que sea mucho más fácil decidir qué objetos en el gráfico deben actualizarse y cuáles no.
Juliet

66
@Juliet, tengo una duda: en mi mentalidad totalmente imperativa, la recursión debe terminar en algún momento, de lo contrario eventualmente producirá un desbordamiento de pila. En el ejemplo recursivo de pacman, ¿cómo se mantiene a raya la pila? ¿El objeto aparece explícitamente al comienzo de la función?
BlueStrat

9
@BlueStrat - buena pregunta ... si es una "llamada de cola" ... es decir, la llamada recursiva es lo último en la función ... entonces el sistema no necesita generar un nuevo marco de pila ... puede solo reutiliza el anterior. Esta es una optimización común para lenguajes de programación funcionales. en.wikipedia.org/wiki/Tail_call
reteptilian

44
@MichaelOsofsky, cuando interactúa con bases de datos y API, siempre hay un 'mundo exterior' con el que se puede comunicar. En este caso, no puede ir 100% funcional. Es importante mantener este código 'no funcional' aislado y abstraído para que solo haya una entrada y una salida al mundo exterior. De esta manera, puede mantener funcional el resto de su código.
Chielt

76

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:

double x = 2 * x

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:

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

pero también podría hacer

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

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.


2
<< readFile ("myFile.txt") necesitaría devolver el mismo valor de cadena cada vez. No es demasiado útil. >> Supongo que es útil siempre que oculte el sistema de archivos global. Si lo considera como un segundo parámetro y deja que otros procesos devuelvan una nueva referencia al sistema de archivos cada vez que lo modifican con filesystem2 = write (filesystem1, fd, pos, "string"), y deja que todos los procesos intercambien su referencia al sistema de archivos , podríamos tener una imagen mucho más limpia del sistema operativo.
anguila ghEEz

@eelghEEz, este es el mismo enfoque que Datomic adopta para las bases de datos.
Jason

1
+1 para la comparación clara y concisa entre paradigmas. Una sugerencia es int double(x){ return x * (++y); }que la actual seguirá siendo 4, aunque seguirá teniendo un efecto secundario no anunciado, mientras ++yque devolverá 6.
BrainFRZ

@eelghEEz No estoy seguro de una alternativa, realmente, ¿hay alguien más? Para introducir información en un contexto FP (puro), "toma una medida", por ejemplo, "en la marca de tiempo X, la temperatura es Y". Si alguien pregunta por la temperatura, puede significar implícitamente X = ahora, pero no puede pedir la temperatura como una función universal del tiempo, ¿verdad? FP trata con el estado inmutable, y debe crear un estado inmutable, a partir de fuentes internas y externas, a partir de uno mutable. Los índices, marcas de tiempo, etc. son útiles, pero ortogonales a la mutabilidad, como VCS lo son para el control de versiones.
John P

29

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.


2
OK, cambié el título. Sin embargo, su respuesta parece conducir a un problema aún peor. Si tengo que recrear cada objeto cada vez que algo cambia en su estado, pasaré todo el tiempo de mi CPU haciendo nada más que construir objetos. Estoy pensando en la programación de juegos aquí, donde tienes muchas cosas moviéndose en la pantalla (y fuera de la pantalla) a la vez, que deben ser capaces de interactuar entre sí. Todo el motor tiene una velocidad de cuadro establecida: todo lo que vas a hacer, debes hacerlo en X número de milisegundos. ¿Seguramente hay una mejor manera que constantemente reciclar objetos enteros?
Mason Wheeler

44
Lo bueno de esto es que la imutabilidad está en el lenguaje, no en la implementación. Con algunos trucos, puede tener un estado inmutable en el idioma mientras que la implementación está cambiando el estado en el lugar. Ver, por ejemplo, la mónada ST de Haskell.
CesarB

44
@Mason: El punto es que el compilador puede decidir mucho mejor dónde es (hilo) seguro cambiar el estado en el lugar que usted.
jerryjvl

Creo que para los juegos debes evitar la inmutación de cualquier parte donde la velocidad no importe. Si bien un lenguaje inmutable podría optimizarlo para usted, nada será más rápido que modificar la memoria que las CPU son rápidas. Entonces, si resulta que hay 10 o 20 lugares en los que necesitas un imperativo, creo que deberías evitar el inmutable por completo a menos que puedas modularizarlo para áreas muy separadas como los menús de juegos. Y la lógica del juego en particular podría ser un buen lugar para usar inmutable porque siento que es genial para modelar complejos sistemas puros como las reglas de negocios.
LegendLength

@LegendLength te estás contradiciendo a ti mismo.
Ixx

18

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:

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

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:

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.


¿Es algo así como una mónada?
CMCDragonkai

Consideraría esto: A es declarativo en el nivel 1 B es declarativo en el nivel 2, considera que A es imperativo. C es declarativa en el nivel 3, considera que B es imprescindible. A medida que aumentamos la capa de abstracción, siempre considera que los lenguajes inferiores en la capa de abstracción son más imperativos que ellos mismos.
CMCDragonkai

14

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:

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 tipo esquema):

(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 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 .


13

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.


10

Llegué tarde a la discusión, pero quería agregar algunos puntos para 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 posteriores . Aquí hay un ejemplo muy simple de viajar por una recta numérica. Tu estado es tu 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 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.

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

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 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.

  1. 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.

  2. 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.


8

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". Si ses 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:

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

Al traducir de estas declaraciones tipo Haskell, readRefrecibe 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. writeRefes similar, pero cambia el valor señalado en su lugar.

Si llama readRefy luego pasa el estado falso devuelto por writeRef(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. Puede writeRefvolver 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.Statedesde el mtl, y al ST sy IO(también conocidos como ST RealWorldmó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 STmónada en Haskell es la que normalmente se usa para este truco; el State#que mencioné anteriormente es, de hecho, GHC State# s, utilizado por su implementación de la STyIO mónadas).


7

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.


Cuando era más antifuncional, me preguntaba cómo podría ser bueno cuando algo como un disco duro es mutable. Todas mis clases de c # tenían un estado mutable y podían simular lógicamente un disco duro o cualquier otro dispositivo. Mientras que con funcional había una falta de coincidencia entre los modelos y las máquinas reales que estaban modelando. Después de profundizar más en lo funcional, me he dado cuenta de que los beneficios que obtienes son mayores que ese problema. Y si fuera físicamente posible inventar un disco duro que hiciera una copia de sí mismo, en realidad sería útil (como ya lo hace el diario).
LegendLength

5

Además de las excelentes respuestas que otros están dando, piense en las clases Integery Stringen 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 a Map, la clave no se puede cambiar. Compare esto con la Dateclase 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 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.


2
Entiendo eso, pero no responde mi pregunta. Teniendo en cuenta que un programa de computadora es un modelo de algún evento o proceso del mundo real, si no puede cambiar sus valores, ¿cómo modela algo que cambia?
Mason Wheeler

Bueno, ciertamente puedes hacer cosas útiles con las clases Integer y String. No es que su inmutabilidad signifique que no puedes tener un estado mutable.
Eddie

@Mason Wheeler: al comprender que una cosa y su estado son dos "cosas" diferentes. Lo que es pacman no cambia de un momento a otro B. Donde está pacman sí cambia. Cuando pasas del tiempo A al tiempo B, obtienes una nueva combinación de pacman + state ... que es el mismo pacman, un estado diferente. Estado no cambiado ... estado diferente.
RHSeeger el

4

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 .


4

3

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!


2
No estoy de acuerdo con que esto sea algo que puedas fijar en OO. Los lenguajes anteriores a OO fomentaron el estado de acoplamiento y los algoritmos. OO acaba de proporcionar una mejor manera de administrarlo.
Jason Baker

"Animado" - tal vez. OO lo convierte en una parte explícita del lenguaje. Puedes encapsular e ocultar información en C, pero diría que los lenguajes OO lo hacen mucho más fácil.
duffymo

2

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.


El hecho de que dos idiomas estén completos no significa que puedan realizar las mismas tareas. Lo que significa es que pueden realizar el mismo cálculo. Brainfuck está Turing completo, pero estoy bastante seguro de que no puede comunicarse a través de una pila TCP.
RHSeeger el

2
Claro que si. Con el mismo acceso al hardware que dice C, puede hacerlo. Eso no significa que sea práctico, pero existe la posibilidad.
Jason Baker

2

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.



-3

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:

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()
    )
);

John, ¿qué idioma es este?
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.