Programación Funcional - Inmutabilidad

12

Estoy tratando de entender el manejo de datos inmutables en FP (específicamente en F #, pero otros FP también están bien) y romper el viejo hábito del pensamiento completo (estilo OOP). Una parte de la respuesta seleccionada a la pregunta aquí reiteró mi búsqueda de cualquier crítica sobre problemas que se resuelven mediante representaciones con estado en OOP con inmutables en FP (por ejemplo: una cola con productores y consumidores). ¿Alguna idea o enlaces son bienvenidos? Gracias por adelantado.

Editar : para aclarar un poco más la pregunta, ¿cómo se compartirían las estructuras inmutables (por ejemplo, cola) simultáneamente en varios subprocesos (por ejemplo, productor y consumidor) en FP

Venkram
fuente
Una forma de manejar los problemas de concurrencia es hacer copias de la cola cada vez (algo costoso, pero funciona).
Trabajo
infoq.com/presentations/Functional-Data-Structures-in-Scala Puede encontrar este discurso perspicaz.
deadalnix

Respuestas:

19

Aunque a veces se expresa de esa manera, la programación funcional¹ no impide los cálculos con estado. Lo que hace es forzar al programador a hacer explícito el estado.

Por ejemplo, tomemos la estructura básica de algún programa usando una cola imperativa (en algunos pseudolenguaje):

q := Queue.new();
while (true) {
    if (Queue.is_empty(q)) {
        Queue.add(q, producer());
    } else {
        consumer(Queue.take(q));
    }
}

La estructura correspondiente con una estructura de datos de cola funcional (todavía en un lenguaje imperativo, para abordar una diferencia a la vez) se vería así:

q := Queue.empty;
while (true) {
    if (q = Queue.empty) {
        q := Queue.add(q, producer());
    } else {
        (tail, element) := Queue.take(q);
        consumer(element);
        q := tail;
    }
}

Como la cola ahora es inmutable, el objeto en sí no cambia. En este pseudocódigo, en qsí mismo es una variable; las asignaciones q := Queue.add(…)y q := tailhacer que apunte a un objeto diferente. La interfaz de las funciones de la cola ha cambiado: cada una debe devolver el nuevo objeto de la cola que resulta de la operación.

En un lenguaje puramente funcional, es decir, en un lenguaje sin efectos secundarios, debe hacer explícito todo el estado. Dado que el productor y el consumidor presumiblemente están haciendo algo, su estado también debe estar en la interfaz de su interlocutor.

main_loop(q, other_state) {
    if (q = Queue.empty) {
        let (new_state, element) = producer(other_state);
        main_loop(Queue.add(q, element), new_state);
    } else {
        let (tail, element) = Queue.take(q);
        let new_state = consumer(other_state, element);
        main_loop(tail, new_state);
    }
}
main_loop(Queue.empty, initial_state)

Observe cómo ahora cada parte del estado se administra explícitamente. Las funciones de manipulación de colas toman una cola como entrada y producen una nueva cola como salida. El productor y el consumidor también pasan su estado.

La programación concurrente no encaja muy bien dentro de la programación funcional, pero se adapta muy bien a la programación funcional. La idea es ejecutar un grupo de nodos de computación separados y dejarlos intercambiar mensajes. Cada nodo ejecuta un programa funcional y su estado cambia a medida que envía y recibe mensajes.

Continuando con el ejemplo, dado que hay una sola cola, es administrada por un nodo en particular. Los consumidores envían a ese nodo un mensaje para obtener un elemento. Los productores envían a ese nodo un mensaje para agregar un elemento.

main_loop(q) =
    consumer->consume(q->take()) || q->add(producer->produce());
    main_loop(q)

El único lenguaje "industrializado" que acerta la concurrencia³ es Erlang . Aprender Erlang es definitivamente el camino hacia la iluminación⁴ sobre la programación concurrente.

¡Todos cambien a idiomas libres de efectos secundarios ahora!

¹ Este término tiene varios significados; aquí creo que lo estás usando para significar programación sin efectos secundarios, y ese es el significado que también estoy usando.
² La programación con estado implícito es una programación imperativa ; La orientación a objetos es una preocupación completamente ortogonal.
³ Inflamatorio, lo sé, pero lo digo en serio. Los subprocesos con memoria compartida son el lenguaje ensamblador de la programación concurrente. La transmisión de mensajes es mucho más fácil de entender, y la falta de efectos secundarios realmente brilla tan pronto como se introduce la concurrencia.
Y esto viene de alguien que no es fanático de Erlang, pero por otras razones.

Gilles 'SO- deja de ser malvado'
fuente
2
+1 Respuesta mucho más completa, aunque supongo que uno podría objetar que Erlang no es un lenguaje FP puro.
Rein Henrichs
1
@Rein Henrichs: De hecho. De hecho, de todos los lenguajes dominantes actualmente existentes, Erlang es el que implementa más fielmente la Orientación a objetos.
Jörg W Mittag
2
@ Jörg De acuerdo. Aunque, de nuevo, uno podría objetar que FP y OO puros son ortogonales.
Rein Henrichs
Entonces, para implementar una cola inmutable en un software concurrente, los mensajes deben enviarse y recibirse entre nodos. ¿Dónde se almacenan los mensajes pendientes?
Mouviciel
@mouviciel Los elementos de la cola se almacenan en la cola de mensajes entrantes del nodo. Esta función de cola de mensajes es una característica básica de una infraestructura distribuida. Un diseño de infraestructura alternativa que funciona bien para la concurrencia local pero no con sistemas distribuidos es bloquear al remitente hasta que el receptor esté listo. Me doy cuenta de que esto no explica todo, tomaría uno o dos capítulos de un libro sobre programación concurrente para explicar esto completamente.
Gilles 'SO- deja de ser malvado'
4

El comportamiento con estado en un lenguaje FP se implementa como una transformación de un estado anterior a un estado nuevo. Por ejemplo, en cola sería una transformación de una cola y un valor a una nueva cola con el valor en cola. Dequeue sería una transformación de una cola a un valor y una nueva cola con el valor eliminado. Se han ideado construcciones como mónadas para abstraer esta transformación de estado (y otros resultados de cálculo) de maneras útiles

Rein Henrichs
fuente
3
Si se trata de una nueva cola para cada operación de agregar / quitar, ¿cómo compartirían la cola dos (o más) operaciones asincrónicas (hilos)? ¿Es un patrón para abstraer las novedades de la cola?
venkram
La concurrencia es una pregunta completamente diferente. No puedo proporcionar una respuesta suficiente en un comentario.
Rein Henrichs
2
@Rein Henrichs: "no se puede proporcionar una respuesta suficiente en un comentario". Eso generalmente significa que debe actualizar la respuesta para abordar los problemas relacionados con los comentarios.
S.Lott
La simultaneidad también puede ser monádica, consulte Control Haskells. Concurrencia.
alternativa
1
@ S.Lott en este caso significa que el OP debe hacer una nueva pregunta. La concurrencia es OT a esta pregunta, que trata sobre estructuras de datos inmutables.
Rein Henrichs
2

... problemas que se resuelven mediante representaciones con estado en OOP con inmutables en FP (por ejemplo: una cola con productores y consumidores)

Su pregunta es lo que se llama un "problema XY". Específicamente, el concepto que cita (hacer cola con productores y consumidores) es en realidad una solución y no un "problema" como usted describe. Esto presenta una dificultad porque está solicitando una implementación puramente funcional de algo que es inherentemente impuro. Entonces mi respuesta comienza con una pregunta: ¿cuál es el problema que estás tratando de resolver?

Hay muchas formas para que múltiples productores envíen sus resultados a un solo consumidor compartido. Quizás la solución más obvia en F # es hacer del consumidor un agente (también conocido como MailboxProcessor) y hacer que los productores den Postsus resultados al agente de consumo. Esto utiliza una cola internamente y no es pura (enviar mensajes en F # es un efecto secundario no controlado, una impureza).

Sin embargo, es bastante probable que el problema subyacente sea algo más parecido al patrón de dispersión-reunión de la programación paralela. Para resolver este problema, puede crear una matriz de valores de entrada y luego Array.Parallel.mapsobre ellos y recopilar los resultados utilizando una serie Array.reduce. Alternativamente, puede usar funciones del PSeqmódulo para procesar los elementos de secuencias en paralelo.

También debo enfatizar que no hay nada malo con el pensamiento con estado. La pureza tiene ventajas, pero ciertamente no es una panacea y usted también debe darse cuenta de sus defectos. De hecho, esta es precisamente la razón por la que F # no es un lenguaje funcional puro: por lo tanto, puede usar impurezas cuando sea preferible.

Jon Harrop
fuente
1

Clojure tiene un concepto muy bien pensado de estado e identidad, que está estrechamente relacionado con la concurrencia. La inmutabilidad juega un papel importante, todos los valores en Clojure son inmutables y se puede acceder a ellos a través de referencias. Las referencias son más que simples punteros. Administran el acceso al valor, y hay varios tipos de ellos con diferentes semánticas. Se puede modificar una referencia para apuntar a un nuevo valor (inmutable), y se garantiza que dicho cambio será atómico. Sin embargo, después de la modificación, todos los otros subprocesos siguen funcionando en el valor original, al menos hasta que vuelvan a acceder a la referencia.

Le recomiendo que lea un excelente artículo sobre el estado y la identidad en Clojure , explica los detalles mucho mejor de lo que podría.

Adam Byrtek
fuente