Si les langages de programmation fonctionnels ne peuvent enregistrer aucun état, comment font-ils des trucs simples comme lire les entrées d'un utilisateur (je veux dire comment les «stockent»), ou stocker des données d'ailleurs?
Comme vous l'avez compris, la programmation fonctionnelle n'a pas d'état, mais cela ne signifie pas qu'elle ne peut pas stocker de données. La différence est que si j'écris une déclaration (Haskell) dans le sens de
let x = func value 3.14 20 "random"
in ...
Je suis assuré que la valeur de x
est toujours la même dans le ...
: rien ne peut la changer. De même, si j'ai une fonction f :: String -> Integer
(une fonction prenant une chaîne et renvoyant un entier), je peux être sûr que f
cela ne modifiera pas son argument, ni ne changera aucune variable globale, ni n'écrira de données dans un fichier, etc. Comme sepp2k l'a dit dans un commentaire ci-dessus, cette non-mutabilité est vraiment utile pour raisonner sur les programmes: vous écrivez des fonctions qui plient, fusionnent et mutilent vos données, renvoyant de nouvelles copies afin de pouvoir les enchaîner, et vous pouvez être sûr qu'aucune de ces appels de fonction peuvent faire quelque chose de «nuisible». Vous savez que x
c'est toujours le cas x
et vous n'avez pas à vous inquiéter que quelqu'un ait écrit x := foo bar
quelque part entre la déclaration dex
et son utilisation, car c'est impossible.
Maintenant, que faire si je veux lire l'entrée d'un utilisateur? Comme l'a dit KennyTM, l'idée est qu'une fonction impure est une fonction pure qui passe le monde entier en argument et renvoie à la fois son résultat et le monde. Bien sûr, vous ne voulez pas faire ça: d'une part, c'est horriblement maladroit, et d'autre part, que se passe-t-il si je réutilise le même objet du monde? Donc, cela est abstrait d'une manière ou d'une autre. Haskell le gère avec le type IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Cela nous indique qu'il main
s'agit d'une action IO qui ne renvoie rien; exécuter cette action est ce que signifie exécuter un programme Haskell. La règle est que les types d'E / S ne peuvent jamais échapper à une action d'E / S; dans ce contexte, nous introduisons cette action en utilisant do
. Ainsi, getLine
renvoie un IO String
, qui peut être considéré de deux manières: d'abord, comme une action qui, lorsqu'elle est exécutée, produit une chaîne; deuxièmement, comme une chaîne "entachée" par IO puisqu'elle a été obtenue impur. Le premier est plus correct, mais le second peut être plus utile. Le <-
prend le String
hors du IO String
et le stocke dans str
- mais puisque nous sommes dans une action IO, nous devrons l'envelopper de nouveau, donc il ne peut pas «s'échapper». La ligne suivante tente de lire un entier ( reads
) et saisit la première correspondance réussie (fst . head
); tout cela est pur (pas d'IO), nous lui donnons donc un nom avec let no = ...
. Nous pouvons alors utiliser à la fois no
et str
dans le ...
. Nous avons ainsi stocké des données impures (de getLine
dedans str
) et des données pures ( let no = ...
).
Ce mécanisme pour travailler avec IO est très puissant: il vous permet de séparer la partie pure et algorithmique de votre programme du côté impur, de l'interaction utilisateur, et de l'appliquer au niveau du type. Votre minimumSpanningTree
fonction ne peut pas changer quelque chose ailleurs dans votre code, ou écrire un message à votre utilisateur, etc. C'est sur.
C'est tout ce que vous devez savoir pour utiliser IO dans Haskell; si c'est tout ce que vous voulez, vous pouvez vous arrêter ici. Mais si vous voulez comprendre pourquoi cela fonctionne, continuez à lire. (Et notez que ce truc sera spécifique à Haskell - d'autres langages peuvent choisir une implémentation différente.)
Donc, cela semblait probablement être un peu une triche, ajoutant en quelque sorte de l'impureté à Haskell pur. Mais ce n'est pas le cas - il s'avère que nous pouvons implémenter le type IO entièrement dans Haskell pur (tant qu'on nous donne le RealWorld
). L'idée est la suivante: une action IO IO type
est la même qu'une fonction RealWorld -> (type, RealWorld)
, qui prend le monde réel et renvoie à la fois un objet de type type
et le modifié RealWorld
. Nous définissons ensuite quelques fonctions afin de pouvoir utiliser ce type sans devenir fou:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
La première permet de parler d'actions IO qui ne font rien: return 3
c'est une action IO qui n'interroge pas le monde réel et qui ne fait que revenir 3
. L' >>=
opérateur, prononcé "bind", nous permet d'exécuter des actions IO. Il extrait la valeur de l'action IO, la transmet ainsi que le monde réel via la fonction et renvoie l'action IO résultante. Notez que cela >>=
applique notre règle selon laquelle les résultats des actions d'E / S ne peuvent jamais s'échapper.
Nous pouvons ensuite transformer ce qui précède main
en l'ensemble d'applications de fonction ordinaire suivant:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Le démarrage main
de l' exécution Haskell avec l'initiale RealWorld
, et nous sommes prêts! Tout est pur, il a juste une syntaxe sophistiquée.
[ Edit: Comme le souligne @Conal , ce n'est pas vraiment ce que Haskell utilise pour faire des E / S. Ce modèle casse si vous ajoutez la concurrence, ou en fait une manière quelconque pour que le monde change au milieu d'une action IO, il serait donc impossible pour Haskell d'utiliser ce modèle. Il n'est précis que pour le calcul séquentiel. Ainsi, il se peut que l'IO de Haskell soit un peu une esquive; même si ce n'est pas le cas, ce n'est certainement pas aussi élégant. Selon l'observation de @ Conal, voyez ce que dit Simon Peyton-Jones dans Tackling the Awkward Squad [pdf] , section 3.1; il présente ce qui pourrait constituer un modèle alternatif dans ce sens, mais le laisse tomber pour sa complexité et adopte une approche différente.]
Encore une fois, cela explique (à peu près) comment IO, et la mutabilité en général, fonctionne dans Haskell; si c'est tout ce que vous voulez savoir, vous pouvez arrêter de lire ici. Si vous voulez une dernière dose de théorie, continuez à lire - mais rappelez-vous, à ce stade, nous sommes allés très loin de votre question!
Donc une dernière chose: il s'avère que cette structure - un type paramétrique avec return
et >>=
- est très générale; cela s'appelle une monade, et la do
notation return
, et >>=
fonctionne avec n'importe lequel d'entre eux. Comme vous l'avez vu ici, les monades ne sont pas magiques; tout ce qui est magique, c'est que les do
blocs se transforment en appels de fonction. Le RealWorld
type est le seul endroit où nous voyons de la magie. Des types comme []
, le constructeur de liste, sont également des monades, et ils n'ont rien à voir avec du code impur.
Vous savez maintenant (presque) tout sur le concept de monade (sauf quelques lois qui doivent être satisfaites et la définition mathématique formelle), mais vous manquez d'intuition. Il existe un nombre ridicule de tutoriels monades en ligne; J'aime celui-ci , mais vous avez des options. Cependant, cela ne vous aidera probablement pas ; le seul véritable moyen d'obtenir l'intuition consiste à combiner leur utilisation et la lecture de quelques tutoriels au bon moment.
Cependant, vous n'avez pas besoin de cette intuition pour comprendre IO . Comprendre les monades en général est la cerise sur le gâteau, mais vous pouvez utiliser IO dès maintenant. Vous pouvez l'utiliser après que je vous ai montré la première main
fonction. Vous pouvez même traiter le code IO comme s'il était dans un langage impur! Mais rappelez-vous qu'il existe une représentation fonctionnelle sous-jacente: personne ne triche.
(PS: Désolé pour la durée. Je suis allé un peu loin.)