Programmation fonctionnelle - Immuabilité


12

J'essaie de comprendre le traitement des données immuables en FP (en particulier en F #, mais les autres FP sont également corrects) et de briser la vieille habitude de la pensée pleine d'état (style OOP). Une partie de la réponse choisie à la question ici a réitéré ma recherche de toutes les écritures autour des problèmes qui sont résolus par des représentations avec état dans OOP avec immuables dans FP (par exemple: une file d'attente avec les producteurs et les consommateurs). Des pensées ou des liens sont les bienvenus? Merci d'avance.

Edit : Pour clarifier un peu plus la question, comment les structures immuables (ex: file d'attente) seraient-elles partagées simultanément entre plusieurs threads (ex: producteur et consommateur) dans FP


Une façon de gérer les problèmes de simultanéité consiste à faire des copies de la file d'attente à chaque fois (un peu cher, mais fonctionne).
Job

infoq.com/presentations/Functional-Data-Structures-in-Scala Vous pouvez trouver cet discours perspicace.
deadalnix

Réponses:


19

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, qest lui-même une variable; les affectations q := Queue.add(…)et le q := tailfaire 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.


2
+1 Réponse beaucoup plus complète, même si je suppose que l'on pourrait chipoter qu'Erlang n'est pas un langage FP pur.
Rein Henrichs

1
@Rein Henrichs: En effet. En fait, de tous les langages courants existants, Erlang est celui qui met le plus fidèlement en œuvre l'Orientation-Orientation.
Jörg W Mittag

2
@ Jörg d'accord. Bien que, encore une fois, on puisse chicaner que FP et OO purs sont orthogonaux.
Rein Henrichs

Ainsi, pour implémenter une file d'attente immuable dans un logiciel simultané, les messages doivent être envoyés et reçus entre les nœuds. Où sont stockés les messages en attente?
mouviciel

Les éléments @mouviciel Queue sont stockés dans la file d'attente de messages entrants du nœud. Cette fonction de file d'attente de messages est une fonctionnalité de base d'une infrastructure distribuée. Une autre conception d'infrastructure qui fonctionne bien pour la concurrence locale mais pas avec les systèmes distribués consiste à bloquer l'expéditeur jusqu'à ce que le récepteur soit prêt. Je me rends compte que cela n'explique pas tout, il faudrait un chapitre ou deux d'un livre sur la programmation simultanée pour l'expliquer complètement.
Gilles 'SO- arrête d'être méchant'

4

Le comportement avec état dans une jauge FP est implémenté comme une transformation d'un état antérieur vers un nouvel état. Par exemple, la mise en file d'attente serait une transformation d'une file d'attente et d'une valeur vers une nouvelle file d'attente avec la valeur mise en file d'attente. La file d'attente serait une transformation d'une file d'attente en une valeur et une nouvelle file d'attente avec la valeur supprimée. Des constructions comme des monades ont été conçues pour abstraire cette transformation d'état (et d'autres résultats de calcul) de manière utile


3
S'il s'agit d'une nouvelle file d'attente pour chaque opération d'ajout / suppression, comment deux (ou plusieurs) opérations asynchrones (threads) partageraient-elles la file d'attente? Est-ce un modèle d'abstraire le nouveau contenu de la file d'attente?
venkram

La concurrence est une question complètement différente. Je ne peux pas fournir une réponse suffisante dans un commentaire.
Rein Henrichs

2
@Rein Henrichs: "ne peut pas fournir une réponse suffisante dans un commentaire". Cela signifie généralement que vous devez mettre à jour la réponse pour résoudre les problèmes liés aux commentaires.
S.Lott

La concurrence peut également être monadique, voir haskells Control.Concurrency.STM.
alternative

1
@ S.Lott dans ce cas, cela signifie que l'OP doit poser une nouvelle question. La concurrence est OT à cette question, qui concerne les structures de données immuables.
Rein Henrichs

2

... problèmes qui sont résolus par des représentations avec état dans OOP avec immuables dans FP (par exemple: une file d'attente avec les producteurs et les consommateurs)

Votre question est ce qu'on appelle un "problème XY". Plus précisément, le concept que vous citez (faire la queue avec les producteurs et les consommateurs) est en fait une solution et non un "problème" comme vous le décrivez. Cela introduit une difficulté car vous demandez une implémentation purement fonctionnelle de quelque chose qui est intrinsèquement impur. Ma réponse commence donc par une question: quel est le problème que vous essayez de résoudre?

Il existe de nombreuses façons pour plusieurs producteurs d'envoyer leurs résultats à un seul consommateur partagé. La solution la plus évidente en F # est peut-être de faire du consommateur un agent (aka MailboxProcessor) et de faire en sorte que les producteurs transmettent Postleurs résultats à l'agent de consommation. Cela utilise une file d'attente en interne et ce n'est pas pur (l'envoi de messages en F # est un effet secondaire non contrôlé, une impureté).

Cependant, il est fort probable que le problème sous-jacent ressemble davantage au modèle de diffusion-collecte de la programmation parallèle. Pour résoudre ce problème, vous pouvez créer un tableau de valeurs d'entrée, puis Array.Parallel.maples recopier et rassembler les résultats à l'aide d'une série Array.reduce. Vous pouvez également utiliser des fonctions du PSeqmodule pour traiter les éléments des séquences en parallèle.

Je dois également souligner qu'il n'y a rien de mal à la pensée dynamique. La pureté a des avantages mais ce n'est certainement pas une panacée et vous devez également vous rendre compte de ses défauts. En effet, c'est précisément pourquoi F # n'est pas un langage fonctionnel pur: vous pouvez donc utiliser les impuretés quand elles sont préférables.


1

Clojure a un concept d'État et d'identité très bien pensé, qui est étroitement lié à la concurrence. L'immuabilité joue un rôle important, toutes les valeurs de Clojure sont immuables et sont accessibles via des références. Les références sont plus que de simples pointeurs. Ils gèrent l'accès à la valeur, et il en existe plusieurs types avec une sémantique différente. Une référence peut être modifiée pour pointer vers une nouvelle valeur (immuable), et un tel changement est garanti d'être atomique. Cependant, après la modification, tous les autres threads fonctionnent toujours sur la valeur d'origine, au moins jusqu'à ce qu'ils accèdent à nouveau à la référence.

Je vous recommande fortement de lire un excellent article sur l'état et l'identité dans Clojure , il explique les détails beaucoup mieux que moi.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.