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?

27
demandé sur undur_gongor 2015-09-15 09:28:38

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 monade m - par exemple MaybeT 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 une Int, MaybeT IO Int est un MaybeT IO expression retournant une Int.

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 expression MaybeT IO, utilisez liftIO.

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.

65
répondu ErikR 2016-11-04 12:30:48

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").

12
répondu danidiaz 2015-09-18 22:06:36

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.

7
répondu AJFarmar 2015-09-18 21:55:35