web-dev-qa-db-fra.com

Idées fausses sur les langages purement fonctionnels?

Je rencontre souvent les déclarations/arguments suivants:

  1. Les langages de programmation fonctionnelle pure ne permettent pas d'effets secondaires (et sont donc peu utilisés dans la pratique car tout programme utile a des effets secondaires, par exemple lorsqu'il interagit avec le monde extérieur).
  2. Les langages de programmation fonctionnelle pure ne permettent pas d'écrire un programme qui maintient l'état (qui rend la programmation très maladroite car dans de nombreuses applications que vous avez besoin d'état).

Je ne suis pas un expert en langues fonctionnelles, mais voici ce que j'ai compris de ces sujets jusqu'à présent.

En ce qui concerne le point 1, vous pouvez interagir avec l'environnement dans des langages purement fonctionnels, mais vous devez expliquer explicitement le code (fonctions) qui introduit des effets secondaires (par exemple dans HASKELL au moyen de types monadiques). De plus, autant que je sache l'informatique par des effets secondaires (la mise à jour de manière destructive des données) devrait également être possible (en utilisant des types monadiques?) Même si ce n'est pas la façon de travailler préférée.

En ce qui concerne le point 2, autant que je sache, vous pouvez représenter l'État en enfilant des valeurs via plusieurs étapes de calcul (à Haskell, à nouveau, à l'aide de types monadiques), mais je n'ai aucune expérience pratique et ma compréhension est plutôt vague.

Donc, les deux déclarations ci-dessus sont-elles correctes en quelque sorte ou sont-elles simplement des idées fausses sur les langues purement fonctionnelles? S'ils sont des idées fausses, comment sont-ils venus? Pourriez-vous écrire une extrait de code (éventuellement petit) illustrant la voie idiomatique Haskell à (1) implémenter des effets secondaires et (2) mettre en œuvre un calcul avec l'état?

39
Giorgio

Aux fins de cette réponse, je définis "une langue purement fonctionnelle" pour signifier une langue fonctionnelle dans laquelle les fonctions sont transparentes de manière référentielle, c'est-à-dire appeler la même fonction plusieurs fois avec les mêmes arguments produira toujours les mêmes résultats. Je pense que c'est la définition habituelle d'une langue purement fonctionnelle.

Les langages de programmation fonctionnelle pure ne permettent pas d'effets secondaires (et sont donc peu utilisés dans la pratique car tout programme utile a des effets secondaires, par exemple lorsqu'il interagit avec le monde extérieur).

Le moyen le plus simple de parvenir à une transparence référentielle serait effectivement d'interdire les effets secondaires et il existe en effet des langues dans lesquelles c'est le cas (principalement des domaines spécifiques). Cependant, ce n'est certainement pas le seul moyen et la plupart des langues générales purement fonctionnelles (HASKELLL, CLEAN, ...) Permettent un effet secondaire.

En outre, disant également qu'un langage de programmation sans effets secondaires est peu utilisé dans la pratique n'est pas vraiment juste que je pense - certainement pas pour les langues spécifiques du domaine, mais même pour les langues générales, j'imagine qu'une langue peut être très utile sans fournir d'effets secondaires sans fournir des effets secondaires . Peut-être pas pour les applications de console, mais je pense que les applications d'interface graphique peuvent être bien implémentées sans effets secondaires. Dites, le paradigme réactif fonctionnel.

En ce qui concerne le point 1, vous pouvez interagir avec l'environnement dans des langages purement fonctionnels, mais vous devez expliquer explicitement le code (fonctions) qui les introduit (par exemple dans HASKELL au moyen de types monadiques).

C'est un peu plus la simplifiant. Il suffit de disposer d'un système où des fonctions essentielles doivent être marquées comme telles (semblables à la constance de C++, mais avec des effets secondaires généraux) ne suffisent pas pour assurer une transparence référentielle. Vous devez vous assurer qu'un programme ne puisse jamais appeler une fonction plusieurs fois avec les mêmes arguments et obtenir des résultats différents. Vous pourriez soit faire cela en faisant des choses comme readLine que ce n'est pas une fonction (c'est ce que fait Haskell avec le IO monade) ou que vous pourriez rendre impossible à apposer des fonctions essentielles à plusieurs reprises avec le même argument (c'est ce qui est propre. Dans ce dernier cas, le compilateur s'assurerait que chaque fois que vous appelez une fonction latérale, vous le faites avec un nouvel argument et que vous rejeteriez tout programme où vous transmettez deux fois le même argument à une fonction latérale à deux reprises.

Les langages de programmation fonctionnelle pure ne permettent pas d'écrire un programme qui maintient l'état (qui rend la programmation très maladroite car dans de nombreuses applications que vous avez besoin d'état).

Encore une fois, une langue purement fonctionnelle pourrait très bien interdire l'état mutable, mais il est certainement possible d'être pur et d'avoir un état mutable, si vous le mettez en œuvre de la même manière que je décris avec des effets secondaires ci-dessus. L'état vraiment mutable n'est qu'une autre forme d'effets secondaires.

Cela dit, les langages de programmation fonctionnelle découragent définitivement les langages d'état mutable, en particulier. Et je ne pense pas que cela rend la programmation maladroite - tout à fait le contraire. Parfois (mais pas tout cela souvent), l'état mutable ne peut être évité sans perdre de la performance ni de la clarté (c'est pourquoi des langues comme Haskell ont des installations d'état mutable), mais le plus souvent, cela peut.

S'ils sont des idées fausses, comment sont-ils venus?

Je pense que beaucoup de gens lisent simplement "une fonction doit produire le même résultat lorsqu'ils ont appelé avec les mêmes arguments" et concluent de cela qu'il n'est pas possible de mettre en œuvre quelque chose comme readLine ou code qui maintient l'état mutable. Donc, ils ne sont tout simplement pas au courant des "tricheurs" que les langues purement fonctionnelles peuvent utiliser pour introduire ces choses sans casser la transparence référentielle.

L'état mutable est également lourdement décourage dans les langages fonctionnels, ce n'est donc pas tout autant d'un saut de supposer que cela n'est pas autorisé du tout dans ceux purement fonctionnels.

Pourriez-vous écrire une extrait de code (éventuellement petit) illustrant la voie idiomatique Haskell à (1) implémenter des effets secondaires et (2) mettre en œuvre un calcul avec l'état?

Voici une application à Pseudo-Haskell qui demande à l'utilisateur un nom et la salue. Pseudo-Haskell est une langue que je viens d'inventer, qui possède le système de Haskell's IO, mais utilise plus de syntaxe conventionnelle, plus de noms de fonction descriptifs et n'a pas de do - notation (comme cela distrait que exactement les œuvres de IO monad):

greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)

L'indice ici est que readLine est une valeur de type IO<String> Et composeMonad est une fonction qui prend un argument de type IO<T> (Pour certains types T) et un autre argument qui est une fonction qui prend un argument de type T et renvoie une valeur de type IO<U> (Pour certains types U). print est une fonction qui prend une chaîne et renvoie une valeur de type IO<void>.

Une valeur de type IO<A> Est une valeur qui "code" une action donnée qui produit une valeur de type A. composeMonad(m, f) Produit une nouvelle valeur IO qui code l'action de m suivie de l'action de f(x), où x est le la valeur produit en effectuant l'action de m.

L'état mutable ressemblerait à ceci:

counter = mutableVariable(0)
increaseCounter(cnt) =
    setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
    composeMonad(getValue(cnt), setIncreasedValue)

printCounter(cnt) = composeMonad( getValue(cnt), print )

main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )

Ici mutableVariable est une fonction qui prend une valeur de tout type T et produit un MutableVariable<T>. La fonction getValue prend MutableVariable et renvoie un IO<T> Qui produit sa valeur actuelle. setValue prend un MutableVariable<T> Et un T et renvoie un IO<void> Qui définit la valeur. composeVoidMonad est identique à composeMonad sauf que le premier argument est un IO qui ne produit pas de valeur raisonnable et que le deuxième argument est une autre monade, pas une fonction qui retourne un Monad.

À Haskell, il y a du sucre syntaxique, qui rend toute l'épreuve moins douloureuse, mais il est toujours évident que l'état mutable est quelque chose que la langue ne veut pas vraiment que vous fassiez.

26
sepp2k

Je suis confus car il y a une différence entre une langue pure et une fonction pure . Commençons par la fonction. Une fonction est pure si elle (étant donné la même entrée) renvoie toujours la même valeur et ne provoque aucun effet secondaire observable. Les exemples typiques sont des fonctions mathématiques telles que f(x) = x * x. Envisagez maintenant une implémentation de cette fonction. Il serait pur dans la plupart des langues, même ceux qui ne sont généralement pas considérés comme des langues fonctionnelles pure par exemple. Ml. Même une méthode Java ou C++ avec ce comportement peut être considérée comme pure.

Alors qu'est-ce qu'une langue pure? Strictement parlant, on pourrait s'attendre à ce qu'une langue pure ne vous permet pas d'exprimer des fonctions qui ne sont pas pures. Appelons ceci the définition idéaliste d'une langue pure. Un tel comportement est hautement souhaitable. Pourquoi? Eh bien, la bonne chose à propos d'un programme composé uniquement de fonctions pures est que vous pouvez remplacer l'application de fonction avec sa valeur sans changer la signification du programme. Cela facilite la motivation des programmes, car une fois que vous connaissez le résultat, vous pouvez oublier la façon dont il a été calculé. La pureté peut également permettre au compilateur d'effectuer certaines optimisations agressives.

Alors, que si vous avez besoin d'un état interne? Vous pouvez imiter l'état dans une langue pure simplement en ajoutant l'état avant le calcul en tant que paramètre d'entrée et l'état après le calcul dans le résultat. Au lieu de Int -> Bool Vous obtenez quelque chose comme Int -> State -> (Bool, State). Vous faites simplement la dépendance explicite (qui est considérée comme une bonne pratique dans n'importe quel paradigme de programmation). BTW Il y a une monade qui constitue un moyen particulièrement élégant de combiner de telles fonctions d'État-mimiquant dans des fonctions plus grandes à imitrage de l'état. De cette façon, vous pouvez définitivement "maintenir l'état" dans une langue pure. Mais vous devez le rendre explicite.

Cela signifie-t-il donc que je peux interagir avec l'extérieur? Après tout, un programme utile doit interagir avec le monde réel afin d'être utile. Mais l'entrée et la sortie ne sont évidemment pas pure. Écrire un octet spécifique à un fichier spécifique pourrait aller pour la première fois. Mais exécutant exactement la même opération une seconde fois peut renvoyer une erreur car le disque est plein. Clairement, il n'existe aucune langue pure (dans la signification idéaliste) qui puisse écrire dans un fichier.

Nous sommes donc confrontés à un dilemme. Nous voulons surtout des fonctions pures, mais certains effets secondaires sont absolument nécessaires et ceux qui ne sont pas purs. Maintenant A Définition réaliste d'une langue pure serait qu'il doit exister des moyens de séparer les parties pures des autres parties. Le mécanisme doit s'assurer qu'aucune opération impure ne se fraye un chemin dans les parties pures.

Dans HASKELL, cela se fait avec le type IO. Vous ne pouvez pas détruire un résultat IO (sans mécanismes dangereux). Ainsi, vous ne pouvez traiter que IO résultats avec des fonctions définies dans le module IO eux-mêmes. Heureusement, il existe un ensemble de combinaisons très flexibles qui vous permettent de prendre un résultat IO et de le traiter dans une fonction tant que cette fonction renvoie un autre résultat IO. Cette combinaison est appelée liaison (ou >>=) Et a le type IO a -> (a -> IO b) -> IO b. Si vous généralisez ce concept, vous arrivez à la classe MONAAD et IO arrive à en être un exemple.

16
scarfridge