Je rencontre souvent les déclarations / arguments suivants:
- Les langages de programmation purement fonctionnels ne permettent pas les effets secondaires (et sont donc peu utiles dans la pratique car tout programme utile a des effets secondaires, par exemple lorsqu’il interagit avec le monde extérieur).
- Les langages de programmation purement fonctionnels ne permettent pas d’écrire un programme qui maintient l’état (ce qui rend la programmation très délicate, car dans de nombreuses applications, vous avez besoin d’état).
Je ne suis pas un expert en langages fonctionnels 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 marquer explicitement le code (fonctions) introduisant des effets secondaires (par exemple, en Haskell au moyen de types monadiques). Aussi, autant que je sache, l'informatique par effets secondaires (mise à jour destructive des données) devrait également être possible (en utilisant des types monadiques?) Même si ce n'est pas la méthode de travail préférée.
En ce qui concerne le point 2, autant que je sache, vous pouvez représenter un état en enchaînant les valeurs à travers plusieurs étapes de calcul (en Haskell, là encore, en utilisant des types monadiques), mais je n’ai aucune expérience pratique dans ce domaine et ma compréhension est plutôt vague.
Alors, les deux affirmations précédentes sont-elles correctes ou sont-elles simplement des idées fausses sur des langages purement fonctionnels? Si ce sont des idées fausses, comment sont-elles apparues? Pourriez-vous écrire un extrait de code (éventuellement petit) illustrant la méthode idiomatique de Haskell pour (1) implémenter des effets secondaires et (2) implémenter un calcul avec state?
la source
Réponses:
Dans le cadre de cette réponse, je définis «langage purement fonctionnel» comme un langage fonctionnel dans lequel les fonctions sont transparentes de manière référentielle, c'est-à-dire que l'appel d'une même fonction plusieurs fois avec les mêmes arguments produira toujours les mêmes résultats. C'est, je crois, la définition habituelle d'un langage purement fonctionnel.
Le moyen le plus simple d’atteindre la transparence référentielle serait en effet d’annuler les effets secondaires et c’est effectivement le cas dans certaines langues (principalement dans des domaines spécifiques). Cependant, ce n'est certainement pas le seul moyen et la plupart des langages purement fonctionnels (Haskell, Clean, ...) permettent des effets secondaires.
Dire également qu’un langage de programmation sans effets secondaires est peu utilisé en pratique n’est pas vraiment juste, à mon avis - certainement pas pour les langages spécifiques à un domaine, mais même pour les langages à usage général, j’imagine qu’un langage peut être très utile sans générer d’effets secondaires . Peut-être pas pour les applications en console, mais je pense que les applications à interface graphique peuvent être bien implémentées sans effets secondaires, par exemple dans le paradigme réactif fonctionnel.
C'est un peu trop simplifier. Le simple fait de disposer d'un système dans lequel les fonctions affectant les effets secondaires doivent être marquées comme telle (similaire à la correction de const en C ++, mais avec des effets secondaires généraux) ne suffit pas à assurer la transparence référentielle. Vous devez vous assurer qu'un programme ne peut jamais appeler une fonction plusieurs fois avec les mêmes arguments et obtenir des résultats différents. Vous pouvez soit le faire en faisant des choses comme
readLine
soit quelque chose qui ne soit pas une fonction (c'est ce que Haskell fait avec la monade IO) ou vous pouvez rendre impossible l'appel de fonctions ayant des effets secondaires plusieurs fois avec le même argument (c'est ce que fait Clean). Dans ce dernier cas, le compilateur s'assurera que chaque fois que vous appelez une fonction à effet secondaire, vous le ferez avec un nouvel argument et rejettera tout programme dans lequel vous passerez le même argument deux fois à une fonction à effet secondaire.Encore une fois, un langage purement fonctionnel peut 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 celle que j’ai décrite avec les effets secondaires ci-dessus. L’état réellement mutable n’est qu’une autre forme d’effets secondaires.
Cela dit, les langages de programmation fonctionnels découragent définitivement les états mutables - les purs, en particulier. Et je ne pense pas que cela rende la programmation difficile, bien au contraire. Parfois (mais pas très souvent), l'état mutable ne peut pas être évité sans perte de performance ou de clarté (c'est pourquoi des langues telles que Haskell disposent de fonctionnalités pour l'état mutable), mais le plus souvent.
Je pense que beaucoup de gens lisent simplement "une fonction doit produire le même résultat quand ils sont appelés avec les mêmes arguments" et en concluent qu'il n'est pas possible d'implémenter quelque chose comme
readLine
un code qui maintient l'état mutable. Donc, ils ne sont tout simplement pas au courant des "astuces" que des langages purement fonctionnels peuvent utiliser pour introduire ces choses sans casser la transparence référentielle.De plus, l'état mutable est fortement découragé dans les langages fonctionnels, il n'est donc pas si difficile de supposer que ce n'est pas permis dans les langages purement fonctionnels.
Voici une application dans Pseudo-Haskell qui demande à l'utilisateur un nom et le salue. Le pseudo-Haskell est un langage que je viens d'inventer, qui possède le système IO de Haskell, mais utilise une syntaxe plus conventionnelle, des noms de fonction plus descriptifs et ne comporte pas de
do
-notation (car cela détournerait du fonctionnement exact de la monade IO):L'indice ici est que
readLine
c'est une valeur de typeIO<String>
etcomposeMonad
une fonction qui prend un argument de typeIO<T>
(pour un typeT
) et un autre argument qui est une fonction qui prend un argument de typeT
et retourne une valeur de typeIO<U>
(pour un typeU
).print
est une fonction qui prend une chaîne et retourne une valeur de typeIO<void>
.Une valeur de type
IO<A>
est une valeur qui "code" une action donnée qui produit une valeur de typeA
.composeMonad(m, f)
produit une nouvelleIO
valeur qui code l'action dem
suivie de l'action def(x)
, oùx
est la valeur produite en effectuant l'action dem
.L'état Mutable ressemblerait à ceci:
Voici
mutableVariable
une fonction qui prend la valeur de tout typeT
et produit unMutableVariable<T>
. La fonctiongetValue
prendMutableVariable
et retourne unIO<T>
qui produit sa valeur actuelle.setValue
prend unMutableVariable<T>
et aT
et retourne unIO<void>
qui définit la valeur.composeVoidMonad
est le même quecomposeMonad
sauf que le premier argument est unIO
qui ne produit pas de valeur sensible et que le second argument est un autre monade, pas une fonction qui retourne un monade.En Haskell, il existe un sucre syntaxique qui rend toute cette épreuve moins pénible, mais il est toujours évident que l'état mutable est quelque chose que le langage ne veut pas vraiment que vous fassiez.
la source
counter
, c'estincreaseCounter(counter)
-à- dire ?main
sera celle qui sera réellement exécutée. Autre que le retour d'un E / S depuismain
il n'y a aucun moyen d'exécuter desIO
actions (sans utiliser des fonctions horriblement mauvaises qui ontunsafe
pour nom).IO
valeurs destructrices . Je ne comprenais pas s'il faisait référence à la correspondance de modèle, c'est-à-dire au fait que vous pouvez déconstruire les valeurs d'un type de données algébrique, mais vous ne pouvez pas utiliser la correspondance de modèle pour le faire avec desIO
valeurs.IMHO vous êtes confus parce qu'il y a une différence entre un langage pur et une fonction pure . Commençons par la fonction. Une fonction est pure si elle retourne toujours la même valeur (avec la même entrée) et ne provoque aucun effet secondaire observable. Des exemples typiques sont des fonctions mathématiques telles que f (x) = x * x. Considérons maintenant une implémentation de cette fonction. Il serait pur dans la plupart des langues, même dans celles qui ne sont généralement pas considérées comme des langages fonctionnels purs, par exemple ML. Même une méthode Java ou C ++ avec ce comportement peut être considérée comme pure.
Alors, quelle est une langue pure? À proprement parler, on pourrait s'attendre à ce qu'un langage pur ne vous laisse pas exprimer des fonctions qui ne sont pas pures. Appelons cela la définition idéaliste d'un langage pur. Un tel comportement est hautement souhaitable. Pourquoi? Eh bien, l’avantage d’un programme ne contenant que des fonctions pures est que vous pouvez remplacer l’application de fonction par sa valeur sans changer la signification du programme. Cela rend très facile de raisonner sur les programmes car une fois que vous connaissez le résultat, vous pouvez oublier la façon dont il a été calculé. Purity pourrait également permettre au compilateur d’effectuer certaines optimisations agressives.
Alors que faire si vous avez besoin d'un état interne? Vous pouvez imiter l'état dans un langage pur en ajoutant simplement l'état avant le calcul en tant que paramètre d'entrée et l'état après le calcul en tant que partie du résultat. Au lieu de
Int -> Bool
vous obtenir quelque chose commeInt -> State -> (Bool, State)
. Vous expliquez simplement la dépendance (ce qui est considéré comme une bonne pratique dans tout paradigme de programmation). BTW est une monade qui est un moyen particulièrement élégant de combiner de telles fonctions imitant l’état à des fonctions plus grandes imitant l’état. De cette façon, vous pouvez certainement "maintenir l'état" dans un langage pur. Mais vous devez le rendre explicite.Cela signifie-t-il que je peux interagir avec l'extérieur? Après tout, un programme utile doit interagir avec le monde réel pour être utile. Mais les entrées et les sorties ne sont évidemment pas pures. Écrire un octet spécifique dans un fichier spécifique peut convenir pour la première fois. Mais exécuter la même opération une seconde fois peut renvoyer une erreur car le disque est plein. Clairement, il n’existe pas de langage pur (au sens idéaliste) capable d’é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-ci ne sont pas purs. Maintenant, une définition réaliste d'un langage pur serait qu'il doit y avoir un moyen de séparer les parties pures des autres parties. Le mécanisme doit garantir qu'aucune opération impure ne s'infiltre dans les parties pures.
En Haskell, cela est fait avec le type IO. Vous ne pouvez pas détruire un résultat d'E / S (sans mécanismes non sécurisés). Ainsi, vous ne pouvez traiter que les résultats IO avec les fonctions définies dans le module IO eux-mêmes. Heureusement, il existe des combinateurs 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. Ce combinateur s'appelle bind (ou
>>=
) et a le typeIO a -> (a -> IO b) -> IO b
. Si vous généralisez ce concept, vous arrivez à la classe de la monade et IO en est un exemple.la source
unsafe
son nom) ne correspond pas à votre définition idéaliste. Il n'y a pas de fonctions impures dans Haskell (encore une fois ignorerunsafePerformIO
et co.).readFile
etwriteFile
retournera toujours la mêmeIO
valeur, avec les mêmes arguments. Ainsi , par exemple les deux extraits de codelet x = writeFile "foo.txt" "bar" in x >> x
etwriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"
fera la même chose.IO Something
? Si tel est le cas, il est parfaitement possible d'appeler deux fois une fonction IO avec le même argument:putStrLn "hello" >> putStrLn "hello"
- ici, les deux appels doiventputStrLn
avoir le même argument. Bien sûr, ce n'est pas un problème car, comme je l'ai dit précédemment, les deux appels donneront la même valeur d'E / S.writeFile "foo.txt" "bar"
ne peut pas causer d'erreur car l'évaluation de l'appel de fonction n'exécute pas l'action. Si vous dites que dans mon exemple précédent, la version aveclet
n'a qu'une seule occasion de provoquer une défaillance d'E / S alors que la version sans enlet
a deux, vous vous trompez. Les deux versions ont deux possibilités d'échec d'IO. Comme lalet
version évalue l'appelwriteFile
une seule fois alors que la version sans l'let
évalue deux fois, vous pouvez voir que la fréquence à laquelle la fonction est appelée n'a pas d'importance.putStrLn
fonction prend exactement un argument, qui est de typeString
. Si vous ne me croyez pas, regardez son type:String -> IO ()
. Cela ne prend certainement aucun argument de typeIO
- cela produit une valeur de ce type.