Programmation asynchrone dans les langages fonctionnels

31

Je suis principalement un programmeur C / C ++, ce qui signifie que la majorité de mon expérience concerne les paradigmes procéduraux et orientés objet. Cependant, comme de nombreux programmeurs C ++ le savent, C ++ s'est déplacé au fil des ans vers un style fonctionnel, aboutissant finalement à l'ajout de lambdas et de fermetures en C ++ 0x.

Quoi qu'il en soit, même si j'ai une expérience considérable dans le codage dans un style fonctionnel utilisant C ++, je n'ai que très peu d'expérience avec les langages fonctionnels réels tels que Lisp, Haskell, etc.

J'ai récemment commencé à étudier ces langages, car l'idée de "pas d'effets secondaires" dans les langages purement fonctionnels m'a toujours intrigué, notamment en ce qui concerne ses applications à la concurrence et à l'informatique distribuée.

Cependant, venant d'un arrière-plan C ++, je suis confus quant à la façon dont cette philsophie "sans effets secondaires" fonctionne avec la programmation asynchrone. Par programmation asynchrone, j'entends tout cadre / API / style de codage qui envoie des gestionnaires d'événements fournis par l'utilisateur pour gérer les événements qui se produisent de manière asynchrone (en dehors du flux du programme). Cela inclut les bibliothèques asynchrones telles que Boost.ASIO, ou même tout simplement le vieux C gestionnaires de signaux ou gestionnaires d'événements Java GUI.

La seule chose que tous ces éléments ont en commun est que la nature de la programmation asynchrone semble nécessiter la création d'effets secondaires (état), afin que le flux principal du programme prenne conscience qu'un gestionnaire d'événements asynchrone a été invoqué. En règle générale, dans un cadre comme Boost.ASIO, un gestionnaire d'événements modifie l'état d'un objet, de sorte que l'effet de l'événement se propage au-delà de la durée de vie de la fonction de gestionnaire d'événements. Vraiment, que peut faire d'autre un gestionnaire d'événements? Il ne peut pas "renvoyer" une valeur au point d'appel, car il n'y a pas de point d'appel. Le gestionnaire d'événements ne fait pas partie du flux principal du programme, donc la seule façon dont il peut avoir un effet sur le programme réel est de changer un état (ou bien longjmpun autre point d'exécution).

Il semble donc que la programmation asynchrone consiste à produire des effets secondaires de manière asynchrone. Cela semble totalement contraire aux objectifs de la programmation fonctionnelle. Comment ces deux paradigmes sont-ils conciliés (en pratique) dans les langages fonctionnels?

Charles Salvia
la source
3
Wow, j'étais sur le point d'écrire une question comme celle-ci et je ne savais pas comment la poser, puis j'ai vu cela dans les suggestions!
Amogh Talpallikar

Réponses:

11

Toute votre logique est solide, sauf que je pense que votre compréhension de la programmation fonctionnelle est un peu trop extrême. Dans la programmation fonctionnelle du monde réel, tout comme la programmation orientée objet ou impérative concerne l' état d' esprit et la façon dont vous abordez le problème. Vous pouvez toujours écrire des programmes dans l'esprit de la programmation fonctionnelle tout en modifiant l'état de l'application.

En fait, vous devez modifier l'état de l'application pour réellement faire quoi que ce soit. Les gars de Haskell vous diront que leurs programmes sont «purs» car ils encapsulent tous leurs changements d'état dans une monade. Cependant, leurs programmes interagissent toujours avec le monde extérieur. (Sinon à quoi ça sert!)

La programmation fonctionnelle met l'accent sur «aucun effet secondaire» quand cela a du sens. Cependant, pour faire de la programmation dans le monde réel, comme vous l'avez dit, vous devez modifier l'état du monde. (Par exemple, répondre à des événements, écrire sur le disque, etc.)

Pour plus d'informations sur la programmation asynchrone dans les langages fonctionnels, je vous invite fortement à vous pencher sur le modèle de programmation des flux de travail asynchrones de F # . Il vous permet d'écrire des programmes fonctionnels tout en cachant tous les détails désordonnés de la transition des threads dans une bibliothèque. (D'une manière très similaire aux monades de style Haskell.)

Si le «corps» du thread calcule simplement une valeur, la génération de plusieurs threads et le fait de calculer des valeurs en parallèle font toujours partie du paradigme fonctionnel.

Chris Smith
la source
5
Aussi: regarder Erlang aide. La langue est très simple, pur (toutes les données sont immuables), et est tout sur le traitement asynchrone.
9000
Fondamentalement, après avoir compris les avantages de l'approche fonctionnelle et ne changer d'état que lorsque cela est important, vous vous assurerez automatiquement que même si vous travaillez dans quelque chose comme Java, vous savez quand modifier l'état et comment garder ces choses sous contrôle.
Amogh Talpallikar
Pas d'accord - le fait qu'un programme soit composé de fonctions `` pures '' ne signifie pas qu'il n'itère pas avec le monde extérieur, c'est que chaque fonction d'un programme pour un ensemble d'arguments retournera toujours le même résultat, et ceci est (pureté) un gros problème, car, d'un point de vue pratique - un tel programme sera moins bogué, plus «testable», l'exécution réussie des fonctions pourrait être prouvée mathématiquement.
Gill Bates
8

C'est une question fascinante. À mon avis, l'approche la plus intéressante est l'approche adoptée dans Clojure et expliquée dans cette vidéo:

http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

Fondamentalement, la "solution" proposée est la suivante:

  • Vous écrivez la plupart de votre code en tant que fonctions "pures" classiques avec des structures de données immuables et sans effets secondaires
  • Les effets secondaires sont isolés via l'utilisation de références gérées qui contrôlent le changement en fonction des règles de mémoire transactionnelle du logiciel (c'est-à-dire que toutes vos mises à jour à l'état mutable ont lieu dans une transaction isolée appropriée)
  • Si vous adoptez cette vision du monde, vous pouvez voir des "événements" asynchrones comme déclencheurs d'une mise à jour transactionnelle d'état mutable où la mise à jour est elle-même une fonction pure.

Je n'ai probablement pas exprimé l'idée aussi clairement que d'autres l'ont fait, mais j'espère que cela donne l'idée générale - fondamentalement, il utilise un système STM simultané pour fournir le "pont" entre la programmation fonctionnelle pure et la gestion des événements asynchrones.

mikera
la source
6

Une remarque: un langage fonctionnel est pur, mais son runtime ne l'est pas.

Par exemple, les runtimes Haskell impliquent des files d'attente, le multiplexage de threads, le garbage collection, etc ... tout cela n'est pas pur.

Un bon exemple est la paresse. Haskell prend en charge l'évaluation paresseuse (c'est la valeur par défaut, en fait). Vous créez une valeur paresseuse en préparant une opération, vous pouvez ensuite créer plusieurs copies de cette valeur, et elle est toujours "paresseuse" tant qu'elle n'est pas requise. Lorsque le résultat est nécessaire, ou si le runtime trouve du temps, la valeur est réellement calculée et l'état de l'objet paresseux change pour refléter qu'il n'est plus nécessaire d'effectuer le calcul (une fois de plus) pour obtenir son résultat. Il est désormais disponible dans toutes les références, donc l'état de l'objet a changé, même s'il s'agit d'un langage pur.

Matthieu M.
la source
2

Je suis confus quant à la façon dont cette philsophie «sans effets secondaires» fonctionne avec la programmation asynchrone. Par programmation asynchrone, je veux dire ...

Ce serait le point, alors.

Un style sonore, sans effet secondaire est incompatible avec les cadres qui dépendent de l'état. Trouvez un nouveau cadre.

La norme WSGI de Python, par exemple, nous permet de créer des applications sans effet secondaire.

L'idée est que les différents "changements d'état" se reflètent dans un environnement de valeurs qui peut être construit de manière incrémentielle. Chaque demande est un pipeline de transformations.

S.Lott
la source
"ne permet de créer aucune application à effets secondaires" Je pense qu'il y a quelque chose qui manque quelque part.
Christopher Mahan
1

Ayant appris l'encapsulation de Borland C ++ après avoir appris C, lorsque Borland C ++ manquait de modèles qui permettaient les génériques, le paradigme d'orientation d'objet m'a mis mal à l'aise. Un moyen un peu plus naturel de calculer semblait filtrer les données via des tuyaux. Le flux sortant avait une identité distincte et indépendante du flux d'entrée immuable entrant, plutôt que d'être considéré comme un effet secondaire, c'est-à-dire que chaque source de données (ou filtre) était autonome par rapport aux autres. La pression de touche (un exemple d'événement) a contraint les combinaisons d'entrées utilisateur asynchrones aux codes clés disponibles. Les fonctions fonctionnent sur les arguments des paramètres d'entrée, et l'état encapsulé par la classe est juste un raccourci pour éviter de passer explicitement des arguments répétitifs entre un petit sous-ensemble de fonctions, en plus d'être prudent avec le contexte lié empêchant d'abuser ces arguments de toute fonction arbitraire.

L'adhésion rigide à un paradigme particulier entraîne des désagréments pour traiter les abstractions qui fuient, par exemple. les runtimes commerciaux tels que JRE, DirectX, les promoteurs orientés objet cible .net avant tout. Pour limiter les inconvénients, les langues optent soit pour des monades savantes sophistiquées comme le fait Haskell, soit pour un support multi-paradigme flexible comme F #. À moins que l'encapsulation ne soit utile dans certains cas d'utilisation de l'héritage multiple, l'approche multi-paradigme peut être une alternative supérieure à certains modèles de programmation spécifiques au paradigme, parfois complexes.

Chawathe Vipul
la source