Bien qu'elle soit parfois exprimée de cette façon, la programmation fonctionnelle¹ n'empêche pas les calculs avec état. Cela oblige le programmeur à rendre l'état explicite.
Par exemple, prenons la structure de base d'un programme utilisant une file d'attente impérative (dans certains pseudolangage):
q := Queue.new();
while (true) {
if (Queue.is_empty(q)) {
Queue.add(q, producer());
} else {
consumer(Queue.take(q));
}
}
La structure correspondante avec une structure de données de file d'attente fonctionnelle (toujours dans un langage impératif, afin de s'attaquer à une différence à la fois) ressemblerait à ceci:
q := Queue.empty;
while (true) {
if (q = Queue.empty) {
q := Queue.add(q, producer());
} else {
(tail, element) := Queue.take(q);
consumer(element);
q := tail;
}
}
Étant donné que la file d'attente est désormais immuable, l'objet lui-même ne change pas. Dans ce pseudo-code, q
est lui-même une variable; les affectations q := Queue.add(…)
et le q := tail
faire pointer vers un autre objet. L'interface des fonctions de file d'attente a changé: chacune doit renvoyer le nouvel objet de file d'attente qui résulte de l'opération.
Dans un langage purement fonctionnel, c'est-à-dire dans un langage sans effet secondaire, vous devez rendre tous les états explicites. Étant donné que le producteur et le consommateur font probablement quelque chose, leur état doit également être dans l'interface de leur appelant.
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)
Notez comment maintenant chaque morceau d'état est géré explicitement. Les fonctions de manipulation de file d'attente prennent une file d'attente en entrée et produisent une nouvelle file d'attente en sortie. Le producteur et le consommateur passent également leur état.
La programmation simultanée ne s'intègre pas si bien dans la programmation fonctionnelle, mais elle s'intègre très bien dans la programmation fonctionnelle. L'idée est d'exécuter un tas de nœuds de calcul séparés et de les laisser échanger des messages. Chaque nœud exécute un programme fonctionnel et son état change à mesure qu'il envoie et reçoit des messages.
Poursuivant l'exemple, puisqu'il n'y a qu'une seule file d'attente, elle est gérée par un nœud particulier. Les consommateurs envoient à ce nœud un message pour obtenir un élément. Les producteurs envoient à ce nœud un message pour ajouter un élément.
main_loop(q) =
consumer->consume(q->take()) || q->add(producer->produce());
main_loop(q)
Le seul langage «industrialisé» qui obtient le droit d'accès simultané³ est Erlang . Apprendre Erlang est certainement le chemin vers l'illumination⁴ sur la programmation simultanée.
Tout le monde passe maintenant à des langues sans effets secondaires!
¹ Ce terme a plusieurs significations; ici je pense que vous l'utilisez pour signifier une programmation sans effets secondaires, et c'est le sens que j'utilise également.
² La programmation avec état implicite est une programmation impérative ; l'orientation de l'objet est une préoccupation complètement orthogonale.
³ Inflammatoire, je sais, mais je le pense. Les threads avec mémoire partagée sont le langage d'assemblage de la programmation simultanée. La transmission de messages est beaucoup plus facile à comprendre et le manque d'effets secondaires brille vraiment dès que vous introduisez la concurrence.
⁴ Et cela vient de quelqu'un qui n'est pas fan d'Erlang, mais pour d'autres raisons.