Exemple de transformateur Monad non trivial le plus simple pour "dummies", Io + peut-être
Quelqu'un pourrait-il donner un exemple de transformateur de monade super simple (quelques lignes), qui est non trivial (c'est - à-dire ne pas utiliser la Monade D'identité-que je comprends).
Par exemple, comment quelqu'un créerait-il une monade qui fait des E / S et peut gérer l'échec (peut-être)?
Quel serait l'exemple le plus simple qui le démontrerait?
J'ai parcouru quelques tutoriels de transformateur de monade et ils semblent tous utiliser la Monade D'État ou les analyseurs ou quelque chose de compliqué (pour un newbee). Je voudrais voir quelque chose de plus simple. Je pense que IO+serait peut-être simple, mais je ne sais pas vraiment comment le faire moi-même.
Comment pourrais-je utiliser une pile Io + Maybe monad? Ce serait au-dessus? Que serait sur le fond? Pourquoi?
Dans quel genre de cas d'utilisation voudrait-on utiliser la monade IO+Maybe ou la Monade Maybe+IO? Cela aurait-il du sens de créer une telle monade composite? Si oui, quand, et pourquoi?
3 réponses
Ceci est disponible ici en tant que .fichier lhs.
Le transformateur MaybeT
nous permettra de sortir d'un calcul de monade un peu comme lancer une exception.
Je vais d'abord passer rapidement en revue quelques préliminaires. Passez à en ajoutant peut-être des pouvoirs à IO pour un exemple travaillé.
D'abord quelques importations:
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
Règles de base:
Dans une pile de monades, IO est toujours en bas.
Les autres monades de type IO, en règle générale, apparaissent toujours sur le fond, par exemple le transformateur d'état monad ST
.
MaybeT m
est un nouveau type de monade qui ajoute la puissance de la Monade Maybe à la monadem
- par exempleMaybeT IO
.
Nous allons entrer dans ce que ce pouvoir est plus tard. Pour l'instant, habituez-vous à penser à MaybeT IO
comme la pile de monades maybe+IO.
Comme
IO Int
est une monade expression retournant uneInt
,MaybeT IO Int
est unMaybeT IO
expression retournant uneInt
.
Obtenir utilisé pour lire les signatures de type composé est la moitié de la bataille pour comprendre les transformateurs de monade.
Chaque expression d'un bloc
do
doit provenir de la même monade.
C'est-à-dire que cela fonctionne parce que chaque instruction est dans la monade IO:
greet :: IO () -- type:
greet = do putStr "What is your name? " -- IO ()
n <- getLine -- IO String
putStrLn $ "Hello, " ++ n -- IO ()
Cela ne fonctionnera pas car putStr
n'est pas dans le MaybeT IO
monade:
mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? " -- IO monad - need MaybeT IO here
...
Heureusement, il y a un moyen de résoudre ce problème.
Pour transformer une expression
IO
en une expressionMaybeT IO
, utilisezliftIO
.
liftIO
est polymorphe, mais dans notre cas il a le type:
liftIO :: IO a -> MaybeT IO a
mgreet :: MaybeT IO () -- types:
mgreet = do liftIO $ putStr "What is your name? " -- MaybeT IO ()
n <- liftIO getLine -- MaybeT IO String
liftIO $ putStrLn $ "Hello, " ++ n -- MaybeT IO ()
Maintenant, toutes les instructions de {[40] } proviennent de la monade MaybeT IO
.
Chaque transformateur monade a une fonction "run".
La fonction run "exécute" la couche la plus haute d'une pile de monades renvoyant une valeur du calque intérieur.
Pour MaybeT IO
, la fonction d'exécution est:
runMaybeT :: MaybeT IO a -> IO (Maybe a)
Exemple:
ghci> :t runMaybeT mgreet
mgreet :: IO (Maybe ())
ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()
Essayez aussi d'exécuter:
runMaybeT (forever mgreet)
Vous besoin d'utiliser Ctrl-C pour sortir de la boucle.
Jusqu'à présent mgreet
ne fait rien de plus que ce que nous pourrions faire dans IO.
Maintenant nous allons travailler sur un exemple qui démontre la puissance du mélange
la Monade peut-être avec IO.
Ajouter peut-être des pouvoirs à IO
Nous allons commencer par un programme qui pose quelques questions:
askfor :: String -> IO String
askfor prompt = do
putStr $ "What is your " ++ prompt ++ "? "
getLine
survey :: IO (String,String)
survey = do n <- askfor "name"
c <- askfor "favorite color"
return (n,c)
Maintenant, supposons que nous voulons donner à l'utilisateur la possibilité de mettre fin à l'enquête tôt en tapant fin en réponse à une question. On peut le manipuler ce façon:
askfor1 :: String -> IO (Maybe String)
askfor1 prompt = do
putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
r <- getLine
if r == "END"
then return Nothing
else return (Just r)
survey1 :: IO (Maybe (String, String))
survey1 = do
ma <- askfor1 "name"
case ma of
Nothing -> return Nothing
Just n -> do mc <- askfor1 "favorite color"
case mc of
Nothing -> return Nothing
Just c -> return (Just (n,c))
Le problème est que survey1
a le problème d'escalier familier qui
ne change pas si nous ajoutons plus de questions.
Nous pouvons utiliser le transformateur MaybeT monad pour nous aider ici.
askfor2 :: String -> MaybeT IO String
askfor2 prompt = do
liftIO $ putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
r <- liftIO getLine
if r == "END"
then MaybeT (return Nothing) -- has type: MaybeT IO String
else MaybeT (return (Just r)) -- has type: MaybeT IO String
Notez comment tous les statemens dans askfor2
ont le même type de monade.
, Nous avons utilisé une nouvelle fonction:
MaybeT :: IO (Maybe a) -> MaybeT IO a
Voici comment les types fonctionnent:
Nothing :: Maybe String
return Nothing :: IO (Maybe String)
MaybeT (return Nothing) :: MaybeT IO String
Just "foo" :: Maybe String
return (Just "foo") :: IO (Maybe String)
MaybeT (return (Just "foo")) :: MaybeT IO String
Ici return
provient de la Io-monade.
Maintenant, nous pouvons écrire notre fonction de sondage comme ce:
survey2 :: IO (Maybe (String,String))
survey2 =
runMaybeT $ do a <- askfor2 "name"
b <- askfor2 "favorite color"
return (a,b)
Essayez d'exécuter survey2
et de terminer les questions tôt en tapant fin comme réponse à l'une ou l'autre question.
Raccourcis
Je sais que je vais obtenir des commentaires des gens si Je ne mentionne pas les raccourcis suivants.
L'expression:
MaybeT (return (Just r)) -- return is from the IO monad
Peut aussi être écrit simplement comme:
return r -- return is from the MaybeT IO monad
Aussi, une autre manière d'écrire MaybeT (return Nothing)
est:
mzero
De plus, deux instructions liftIO
consécutives peuvent toujours être combinées en une seule liftIO
, par exemple:
do liftIO $ statement1
liftIO $ statement2
Est le même que:
liftIO $ do statement1
statement2
Avec ces changements, notre fonction askfor2
peut être écrite:
askfor2 prompt = do
r <- liftIO $ do
putStr $ "What is your " ++ prompt ++ " (type END to quit)?"
getLine
if r == "END"
then mzero -- break out of the monad
else return r -- continue, returning r
Dans un sens, mzero
devient un moyen de sortir de la monade - comme lancer une exception.
Un autre exemple
Considérez cette simple boucle de demande de mot de passe:
loop1 = do putStr "Password:"
p <- getLine
if p == "SECRET"
then return ()
else loop1
C'est une fonction récursive (queue) et fonctionne très bien.
Dans un langage conventionnel, nous pourrions écrire ceci comme une boucle while infinie avec une pause déclaration:
def loop():
while True:
p = raw_prompt("Password: ")
if p == "SECRET":
break
Avec MaybeT nous pouvons écrire la boucle de la même manière que le code Python:
loop2 :: IO (Maybe ())
loop2 = runMaybeT $
forever $
do liftIO $ putStr "Password: "
p <- liftIO $ getLine
if p == "SECRET"
then mzero -- break out of the loop
else return ()
Le Dernier return ()
continue l'exécution, et puisque nous sommes dans une boucle forever
, le contrôle revient en haut du bloc do. Notez que la seule valeur que loop2
peut renvoyer est Nothing
ce qui correspond à la sortie de la boucle.
Selon la situation, vous pourriez trouver plus facile d'écrire loop2
plutôt que le récursif loop1
.
Supposons que vous avez à travailler avec IO
valeurs "peut échouer" dans un certain sens, comme foo :: IO (Maybe a)
, func1 :: a -> IO (Maybe b)
et func2 :: b -> IO (Maybe c)
.
La vérification manuelle de la présence d'erreurs dans une chaîne de liaisons produit rapidement le redoutable "staircase of doom":
do
ma <- foo
case ma of
Nothing -> return Nothing
Just a -> do
mb <- func1 a
case mb of
Nothing -> return Nothing
Just b -> func2 b
Comment "automatiser" cela d'une certaine manière? Peut - être pourrions-nous concevoir un newtype autour de IO (Maybe a)
avec une fonction de liaison qui vérifie automatiquement si le premier argument est un Nothing
à l'intérieur de IO
, nous épargnant la peine de le vérifier nous-mêmes. Quelque comme
newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }
Avec la fonction bind:
betterBind :: MaybeOverIO a -> (a -> MaybeOverIO b) -> MaybeOverIO b
betterBind mia mf = MaybeOverIO $ do
ma <- runMaybeOverIO mia
case ma of
Nothing -> return Nothing
Just a -> runMaybeOverIO (mf a)
Ça marche! Et, en regardant de plus près, nous nous rendons compte que nous n'utilisons aucune fonction particulière exclusive à la monade IO
. Généralisant un peu le newtype, nous pourrions faire ce travail pour n'importe quelle monade sous-jacente!
newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }
Et c'est, en substance, comment le MaybeT
transformateur œuvres. J'ai laissé de côté quelques détails, comme comment implémenter return
pour le transformateur, et comment "soulever" les valeurs IO
dans MaybeOverM IO
valeurs.
Notez que MaybeOverIO
A kind * -> *
tandis que MaybeOverM
a kind (* -> *) -> * -> *
(parce que son premier "argument de type" est un constructeur de type monade, qui lui-même nécessite un "argument de type").
Bien sûr, le transformateur monade MaybeT
est:
newtype MaybeT m a = MaybeT {unMaybeT :: m (Maybe a)}
Nous pouvons implémenter son instance de monade comme ceci:
instance (Monad m) => Monad (MaybeT m) where
return a = MaybeT (return (Just a))
(MaybeT mmv) >>= f = MaybeT $ do
mv <- mmv
case mv of
Nothing -> return Nothing
Just a -> unMaybeT (f a)
Cela nous permettra D'effectuer IO avec la possibilité d'échouer gracieusement dans certaines circonstances.
Par exemple, imaginez que nous avions une fonction comme celle-ci:
getDatabaseResult :: String -> IO (Maybe String)
Nous pouvons manipuler les monades indépendamment avec le résultat de cette fonction, mais si nous la composons comme ceci:
MaybeT . getDatabaseResult :: String -> MaybeT IO String
Nous pouvons oublier cette couche monadique supplémentaire, et la traiter comme une normal monade.