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
Réponses:
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):
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:
É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 affectationsq := Queue.add(…)
et leq := 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.
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.
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.
la source
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
la source
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 transmettentPost
leurs 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.map
les recopier et rassembler les résultats à l'aide d'une sérieArray.reduce
. Vous pouvez également utiliser des fonctions duPSeq
module 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.
la source
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.
la source