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.
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).
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.
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 (les fonctions) qui les introduit (par exemple en Haskell au moyen de types monadiques).
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 commereadLine
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.
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).
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.
Si ce sont des idées fausses, comment sont-elles apparues?
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.
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?
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):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
L'indice ici est que readLine
c'est une valeur de type IO<String>
et composeMonad
une fonction qui prend un argument de type IO<T>
(pour un type T
) et un autre argument qui est une fonction qui prend un argument de type T
et retourne une valeur de type IO<U>
(pour un type U
). print
est une fonction qui prend une chaîne et retourne 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 IO
valeur qui code l'action de m
suivie de l'action de f(x)
, où x
est la valeur produite 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) )
Voici mutableVariable
une fonction qui prend la valeur de tout type T
et produit un MutableVariable<T>
. La fonction getValue
prend MutableVariable
et retourne un IO<T>
qui produit sa valeur actuelle. setValue
prend un MutableVariable<T>
et a T
et retourne un IO<void>
qui définit la valeur. composeVoidMonad
est le même que composeMonad
sauf que le premier argument est un IO
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.