Ejemplo de transformador de mónada no trivial más simple para "dummies", IO + Tal vez


Podría alguien dar un ejemplo de transformador de mónada súper simple (pocas líneas), que no es trivial (es decir, no usar la mónada de Identidad, eso entiendo).

Por ejemplo, ¿cómo crearía alguien una mónada que hace IO y puede manejar fallas (Tal vez)?

¿Cuál sería el ejemplo más simple que demostraría esto?

He hojeado algunos tutoriales de transformadores de mónadas y todos parecen usar Mónadas de Estado o analizadores o algo complicado (para un newbee). Me gustaría ver algo más simple que eso. Creo que IO + Tal vez sería simple, pero realmente no sé cómo hacerlo yo mismo.

¿Cómo podría usar una pila IO+Maybe monad? ¿Qué estaría encima? ¿Qué habría en el fondo? ¿Por qué?

¿En qué tipo de caso de uso uno querría usar la mónada IO+Maybe o la mónada Maybe+IO? ¿Tendría sentido crear tal mónada compuesta? En caso afirmativo, ¿cuándo y por qué?

Author: undur_gongor, 2015-09-15

3 answers

Esto está disponible aquí como a .archivo lhs.

El transformador MaybeT nos permitirá salir de un cálculo de mónada como lanzar una excepción.

Primero repasaré rápidamente algunos preliminares. Saltar hacia abajo a Añadiendo Tal vez poderes a IO para un ejemplo trabajado.

Primero algunas importaciones:

 import Control.Monad
 import Control.Monad.Trans
 import Control.Monad.Trans.Maybe

Reglas de oro:

En una pila de mónadas, IO siempre está en la parte inferior.

Otras mónadas similares a io también, como regla general, siempre aparecen en la parte inferior, por ejemplo, la mónada del transformador de estado ST.

MaybeT m es un nuevo tipo de mónada que añade la potencia de la mónada Maybe a la mónada m - por ejemplo, MaybeT IO.

Vamos a entrar en lo que ese poder es más tarde. Por ahora, acostúmbrate a pensar en MaybeT IO como la pila de mónadas maybe+IO.

Al igual que IO Int es una expresión de mónada que devuelve un Int, MaybeT IO Int es una expresión MaybeT IO que devuelve un Int.

Consiguiendo acostumbrado a leer las firmas de tipo compuesto es la mitad de la batalla para entender los transformadores de mónada.

Toda expresión en un bloque do debe ser de la misma mónada.

Esto funciona porque cada declaración está en la IO-mónada:

 greet :: IO ()                               -- type:
 greet = do putStr "What is your name? "      -- IO ()
            n <- getLine                      -- IO String
            putStrLn $ "Hello, " ++ n         -- IO ()

Esto no funcionará porque putStr no está en la MaybeT IO mónada:

mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? "    -- IO monad - need MaybeT IO here
            ...

Afortunadamente hay una manera de arreglar esto.

Para transformar una expresión IO en una expresión MaybeT IO use liftIO.

liftIO es polimórfico, pero en nuestro caso tiene el tipo:

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 ()

Ahora toda la declaración en mgreet son de la MaybeT IO mónada.

Cada transformador de mónada tiene una función "run".

La función run "ejecuta" la capa superior de una pila de mónadas que devuelve un valor de la capa interior.

Para MaybeT IO, la función run es:

runMaybeT :: MaybeT IO a -> IO (Maybe a)

Ejemplo:

ghci> :t runMaybeT mgreet 
mgreet :: IO (Maybe ())

ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()

También intente ejecutar:

runMaybeT (forever mgreet)

Necesita usar Ctrl-C para salir del bucle.

Hasta ahora mgreet no hace nada más que lo que podríamos hacer en IO. Ahora trabajaremos en un ejemplo que demuestra el poder de mezclar la Tal Vez mónada con IO.

Añadiendo tal vez poderes a IO

Comenzaremos con un programa que hace algunas preguntas: {[62]]}

 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)

Ahora supongamos que queremos darle al usuario la capacidad de finalizar la encuesta temprano escribiendo FIN en respuesta a una pregunta. Podríamos manejarlo este camino:

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

El problema es que survey1 tiene el problema familiar de escalonamiento que no se escala si añadimos más preguntas.

Podemos usar el transformador MaybeT monad para ayudarnos aquí.

 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

Observe cómo todos los statemens en askfor2 tienen el mismo tipo de mónada.

Hemos usado una nueva función:

MaybeT :: IO (Maybe a) -> MaybeT IO a

Así es como funcionan los tipos:

                  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

Aquí return es de la IO-mónada.

Ahora podemos escribir nuestra función de encuesta como esto:

 survey2 :: IO (Maybe (String,String))
 survey2 =
   runMaybeT $ do a <- askfor2 "name"
                  b <- askfor2 "favorite color"
                  return (a,b)

Intente ejecutar survey2 y terminar las preguntas antes escribiendo END como respuesta a cualquiera de las preguntas.

Atajos

Sé que recibiré comentarios de la gente si no menciono los siguientes atajos.

La expresión:

MaybeT (return (Just r))    -- return is from the IO monad

También se puede escribir simplemente como:

return r                    -- return is from the MaybeT IO monad

También, otra forma de escribir MaybeT (return Nothing) es:

mzero

Además, dos declaraciones liftIO consecutivas siempre pueden combinarse en una sola {[38]]}, por ejemplo:

do liftIO $ statement1
   liftIO $ statement2 

Es lo mismo que:

liftIO $ do statement1
            statement2

Con estos cambios nuestra función askfor2 puede escribirse:

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

En cierto sentido, mzero se convierte en una forma de salir de la mónada - como lanzar una excepción.

Otro ejemplo

Considere este sencillo bucle de pregunta de contraseña:

loop1 = do putStr "Password:"
           p <- getLine
           if p == "SECRET"
             then return ()
             else loop1

Esta es una función recursiva (cola) y funciona muy bien.

En un lenguaje convencional podríamos escribir esto como un bucle while infinito con una ruptura declaración:

def loop():
    while True:
        p = raw_prompt("Password: ")
        if p == "SECRET":
            break

Con MaybeT podemos escribir el bucle de la misma manera que el código 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 ()

El último return () continúa la ejecución, y como estamos en un bucle forever, control vuelve a la parte superior del bloque do. Tenga en cuenta que el único valor que loop2 puede devolver es Nothing que corresponde a la ruptura del bucle.

Dependiendo de la situación, puede que le resulte más fácil escribir loop2 en lugar de la recursiva loop1.

 65
Author: ErikR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-04 12:30:48

Supongamos que tienes que trabajar con IO valores que "pueden fallar" en algún sentido, como foo :: IO (Maybe a), func1 :: a -> IO (Maybe b) y func2 :: b -> IO (Maybe c).

La comprobación manual de la presencia de errores en una cadena de enlaces produce rápidamente la temida "escalera de la perdición":

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

Cómo "automatizar" esto de alguna manera? Tal vez podríamos idear un newtype alrededor de IO (Maybe a) con una función bind que comprueba automáticamente si el primer argumento es un Nothing dentro de IO, ahorrándonos la molestia de comprobarlo nosotros mismos. Algo como

newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }

Con la función 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)

Esto funciona! Y, mirándolo más de cerca, nos damos cuenta de que no estamos usando ninguna función particular exclusiva de la mónada IO. Generalizando un poco el newtype, ¡podríamos hacer que esto funcione para cualquier mónada subyacente!

newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }

Y así es, en esencia, cómo funciona el MaybeT transformador . He omitido algunos detalles, como cómo implementar return para el transformador, y cómo "levantar" IO valores en MaybeOverM IO valores.

Observe que MaybeOverIO tiene kind * -> * mientras que MaybeOverM tiene kind (* -> *) -> * -> * (porque su primer "argumento de tipo" es un constructor de tipo de mónada, que a su vez requiere un "argumento de tipo").

 12
Author: danidiaz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-18 22:06:36

Claro, el transformador de la mónada MaybeT es:

newtype MaybeT m a = MaybeT {unMaybeT :: m (Maybe a)}

Podemos implementar su instancia monad de la siguiente manera:

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)

Esto nos permitirá realizar IO con la opción de fallar con gracia en ciertas circunstancias.

Por ejemplo, imagine que tenemos una función como esta:

getDatabaseResult :: String -> IO (Maybe String)

Podemos manipular las mónadas independientemente con el resultado de esa función, pero si la componemos así:

MaybeT . getDatabaseResult :: String -> MaybeT IO String

Podemos olvidarnos de esa capa monádica extra, y simplemente tratarla como un mónada normal.

 7
Author: AJFarmar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-18 21:55:35