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 q
sí mismo es una variable; las asignaciones q := Queue.add(…)
y q := tail
hacer 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.