J'ai donc commencé à envelopper ma tête autour de Monades (utilisé en Haskell). Je suis curieux de ce que d'autres moyens IO ou de l'état peuvent être traitées dans un langage purement fonctionnel (à la fois dans la théorie ou de la réalité). Par exemple, il y a une logique de la langue appelée le "mercure", qui utilise l'effet du "groupage". Dans un programme comme haskell, comment effet-travaux de dactylographie? Comment d'autres systèmes de travail?
Réponses
Trop de publicités?Il y a plusieurs questions en cause ici.
Tout d'abord, IO
et State
sont des choses très différentes. State
est facile à faire
vous-même: il suffit de passer un argument supplémentaire pour chaque fonction et retourner un supplément
résultat, et vous avez une "dynamique de la fonction"; par exemple, tourner a -> b
en
a -> s -> (b,s)
.
Il n'y a pas quelque chose de magique ici: Control.Monad.State
fournit un wrapper qui
rend le travail avec les "actions de l'etat" de la forme s -> (a,s)
pratique, ainsi
comme un tas de fonctions d'aide, mais c'est tout.
I/O, de par sa nature, doit avoir un peu de magie dans sa mise en œuvre. Mais il y a beaucoup de façons d'exprimer des I/O en Haskell qui ne comportent pas le mot "monade". Si nous avions un IO-gratuit sous-ensemble de Haskell comme-est, et nous avons voulu inventer IO de zéro, sans rien connaître de monades, il y a beaucoup de choses qu'on peut n'.
Par exemple, si tout ce que nous voulons faire est d'imprimer sur la sortie standard, on pourrait dire:
type PrintOnlyIO = String
main :: PrintOnlyIO
main = "Hello world!"
Et puis avoir un RTS (runtime) qui évalue la chaîne et l'imprime. Ceci nous permet d'écrire tout Haskell programme dont les I/O se compose entièrement d'impression à stdout.
Ce n'est pas très utile, cependant, parce que nous voulons que l'interactivité! Donc, nous allons inventer un nouveau type de IO qui permet. La chose la plus simple qui vient à l'esprit est
type InteractIO = String -> String
main :: InteractIO
main = map toUpper
Cette approche IO nous permet d'écrire du code qui lit sur l'entrée standard et écrit à
stdout (le Prélude est livré avec une fonction interact :: InteractIO -> IO ()
qui le fait, par la voie).
C'est beaucoup mieux, car il nous permet d'écrire des programmes interactifs. Mais c'est encore très limitée par rapport à tous les IO que nous voulons faire, et également tout à fait sujettes à l'erreur (si nous avons accidentellement essayer de lire trop loin dans stdin, le programme va juste bloquer jusqu'à ce que l'utilisateur tape plus dans).
Nous voulons être en mesure de faire plus que de lire l'entrée standard stdin et écrire sur la sortie standard. Voici comment les premières versions de Haskell n'ai-je/O, environ:
data Request = PutStrLn String | GetLine | Exit | ...
data Response = Success | Str String | ...
type DialogueIO = [Response] -> [Request]
main :: DialogueIO
main resps1 =
PutStrLn "what's your name?"
: GetLine
: case resps1 of
Success : Str name : resps2 ->
PutStrLn ("hi " ++ name ++ "!")
: Exit
Lorsque nous écrivons, main
, nous obtenons un paresseux liste des arguments et retourne un paresseux liste
résultat. Le paresseux liste nous de retour a des valeurs comme l' PutStrLn s
et GetLine
;
après nous donner une (demande) valeur, nous pouvons examiner l'élément suivant de la
(réponse) de la liste, et la RTS sera nécessaire pour qu'elle soit la réponse à notre
demande.
Il y a des façons de faire le travail avec ce mécanisme de plus belle, mais comme vous pouvez le imaginez, l'approche est assez maladroit assez rapidement. Aussi, c'est le risque d'erreur de la même façon que la précédente.
Voici une autre approche qui est beaucoup moins sujette à erreur, et conceptuellement très à la façon dont Haskell IO se comporte effectivement:
data ContIO = Exit | PutStrLn String ContIO | GetLine (String -> ContIO) | ...
main :: ContIO
main =
PutStrLn "what's your name?" $
GetLine $ \name ->
PutStrLn ("hi " ++ name ++ "!") $
Exit
La clé, c'est qu'au lieu de prendre un "paresseux" liste de réponses comme une grande argument au début de la main, nous faisons des demandes individuelles qui acceptent un argument à la fois.
Notre programme est maintenant juste un type de données -- un peu comme une liste liée, à l'exception de
vous ne pouvez pas simplement le traverser normalement: Lors de la RTS interprète main
, parfois
il rencontre une valeur comme GetLine
qui détient une fonction; il doit alors obtenir
une chaîne de caractères à partir de stdin à l'aide de la RTS de la magie, et de transmettre cette chaîne à la fonction,
avant de pouvoir continuer. Exercice: Écrivez - interpret :: ContIO -> IO ()
.
Notez qu'aucune de ces implémentations impliquer "dans le monde, en passant".
"le monde de passage" n'est pas vraiment la façon dont I/O fonctionne en Haskell. Le réel
la mise en œuvre de l' IO
type dans GHC implique un type interne appelé
RealWorld
, mais ce n'est qu'un détail d'implémentation.
Réelle Haskell IO
ajoute un paramètre de type, donc on peut écrire les actions qui
"produire" des valeurs arbitraires -- de sorte qu'il ressemble plus à de la data IO a = Done a |
PutStr String (IO a) | GetLine (String -> IO a) | ...
. Cela nous donne plus de
flexibilité, car il peut créer de "IO
d'actions" qui produisent de l'arbitraire
des valeurs.
(Russell O'Connor souligne,
ce type est juste un gratuit monade. Nous pouvons écrire une Monad
exemple pour facilement.)
Où puis-monades venir en elle, alors? Il s'avère que nous n'avons pas besoin Monad
pour
I/O, et nous n'avons pas besoin d' Monad
pour l'état, alors pourquoi avons-nous besoin? L'
la réponse est que nous n'avons pas. Il n'y a rien de magique à propos de la classe de type Monad
.
Cependant, lorsque nous travaillons avec des IO
et State
(et les listes de fonctions et de
Maybe
et d'analyseurs et de continuation-passant du style et de la ...) pour assez longtemps, nous
finalement comprendre qu'ils se comportent de façon assez similaire à certains égards. Nous pourrions
écrire une fonction qui imprime chaque chaîne de caractères dans une liste, et une fonction qui s'exécute
chaque stateful de calcul dans une liste et les fils de l'état à travers, et ils vont
look très semblables les uns aux autres.
Puisque nous ne sommes pas comme la rédaction d'un lot de même l'avenir de code, nous avons besoin d'un moyen pour
résumé il; Monad
s'avère être d'une grande abstraction, parce qu'il nous permet de
résumé de nombreux types qui semblent très différentes, mais encore fournir beaucoup d'informations utiles
fonctionnalité (tout compris, en Control.Monad
).
Compte tenu de bindIO :: IO a -> (a -> IO b) -> IO b
et returnIO :: a -> IO a
, nous
pourrais écrire tout IO
programme en Haskell, sans jamais penser à les monades. Mais
nous serions probablement jusqu'à la fin de la réplication de beaucoup de fonctions en Control.Monad
,
comme mapM
et forever
et when
et (>=>)
.
Par la mise en œuvre de la commune, Monad
API, nous apprenons à utiliser le même code pour
travailler avec IO actions comme nous le faisons avec les analyseurs et les listes. C'est vraiment le seul
raison pour laquelle nous avons l' Monad
classe -- pour capturer les similitudes entre
différents types.
Une autre approche majeure est l'unicité de frappe, comme en Propre. La petite histoire c'est que les poignées de l'état (y compris le monde réel) ne peut être utilisé qu'une fois, et les fonctions que l'accès mutable état de retour une nouvelle poignée. Cela signifie que la sortie du premier appel est une entrée de seconde, de forcer l'ordre d'évaluation.
Effet saisissant est utilisé dans le Disciple Compilateur pour Haskell, mais à ma connaissance, il serait beaucoup compilateur de travail pour lui permettre, par exemple, de GHC. Je laisse la discussion des détails à ceux qui sont mieux informés que moi.
Bien, d'abord, qu'est-ce que l'état? Elle peut se manifester comme une variable mutable, qui vous n'avez pas en Haskell. Vous avez seulement les références de mémoire (IORef, MVar, Ptr, etc.) et IO/ST actions pour agir sur eux.
Cependant, l'état lui-même peut être pur bien. De reconnaître que l'examen de la "Stream" de type:
data Stream a = Stream a (Stream a)
C'est un flux de valeurs. Cependant une autre façon d'interpréter ce type est une évolution de la valeur:
stepStream :: Stream a -> (a, Stream a)
stepStream (Stream x xs) = (x, xs)
Cela devient intéressant lorsque vous autorisez les deux flux de communiquer. Vous obtenez alors l'automate catégorie Automatique:
newtype Auto a b = Auto (a -> (b, Auto a b))
C'est vraiment comme Stream
, sauf que maintenant, à chaque instant le flux obtient une valeur d'entrée de type un. Cela forme une catégorie, alors un instant d'un cours d'eau peut prendre sa valeur à l'instant d'un autre flux.
Encore une interprétation différente de ceci: Vous avez deux calculs qui changent au fil du temps et vous permettent de communiquer. Ainsi, chaque calcul est locales de l'état. Voici un type qui est isomorphe a' Auto
:
data LS a b =
forall s.
LS s ((a, s) -> (b, s))
Prendre un coup d'oeil à l'Histoire de Haskell: Être Paresseux Avec Classe. Il décrit deux approches différentes pour faire des I/O en Haskell, avant de monades ont été inventés: suites et des ruisseaux.
Il s'agit d'une approche Fonctionnelle Réactive de Programmation qui représente variant dans le temps, de valeurs et/ou des flux d'événements de première classe de l'abstraction. Un exemple récent qui me vient à l'esprit est de l'Orme qui est écrit en Haskell et a une syntaxe similaire à Haskell.