haskell monads monad-transformers

haskell - El ejemplo más simple de transformador de mónada no trivial para "dummies", IO+Maybe



monads monad-transformers (3)

¿Podría alguien dar un ejemplo de transformador de mónada súper simple (pocas líneas), que no sea trivial (es decir, no usar la mónada de identidad, que yo entiendo)?

Por ejemplo, ¿cómo podría alguien crear una mónada que hace IO y puede manejar el error (quizás)?

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

He hojeado algunos tutoriales de transformadores de mónada y todos parecen usar State Monad o Parsers 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 no sé cómo hacerlo yo mismo.

¿Cómo podría usar una pila de monada IO + Maybe? Que estaría en la parte superior? ¿Qué estaría en el fondo? ¿Por qué?

¿En qué tipo de caso de uso se querría usar la mónada IO + o la mónada Maybe + IO? ¿Tendría eso sentido crear una mónada tan compuesta? Si es así, ¿cuándo y por qué?


Claro, el transformador de monada MaybeT es:

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

Podemos implementar su instancia de mónada como tal:

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, imagina que tenemos una función como esta:

getDatabaseResult :: String -> IO (Maybe String)

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

MaybeT . getDatabaseResult :: String -> MaybeT IO String

Podemos olvidarnos de esa capa monádica adicional y tratarla como una mónada normal.


Esto está disponible here como un archivo .lhs.

El transformador MaybeT nos permitirá salir de un cómputo de mónada como si fuera una excepción.

Primero repasaré rápidamente algunos preliminares. Salte a Agregar poderes Tal vez a IO para un ejemplo trabajado.

Primero algunas importaciones:

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

Reglas de juego:

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

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

MaybeT m es un nuevo tipo de mónada que agrega el poder 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, MaybeT IO a pensar en MaybeT IO como la pila de mónadas de MaybeT IO vez + 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 .

Acostumbrarse a leer firmas de tipo compuesto es la mitad de la batalla para comprender los transformadores de mónada.

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

Es decir, esto funciona porque cada declaración está en la mónada IO:

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 mónada MaybeT IO :

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 todos los enunciados en mgreet son de la mónada MaybeT IO .

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

La función de ejecución "ejecuta" la capa superior de una pila de mónadas que devuelve un valor desde la capa interna.

Para MaybeT IO , la función de ejecución 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)

Tendrá que 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 mónada Maybe con IO.

Añadiendo Tal vez poderes a IO

Comenzaremos con un programa que hace algunas preguntas:

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 dar al usuario la capacidad de terminar la encuesta antes de tiempo escribiendo END en respuesta a una pregunta. Podríamos manejarlo de esta manera:

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 escaleras que no se survey1 si agregamos más preguntas.

Podemos usar el transformador de monada MaybeT 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 estados en askfor2 tienen el mismo tipo de mónada.

Hemos utilizado una nueva función:

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

Aquí es cómo 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í el return es de la IO-mónada.

Ahora podemos escribir nuestra función de encuesta así:

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

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

Atajos

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

La expresion:

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

Además, otra forma de escribir MaybeT (return Nothing) es:

mzero

Además, dos declaraciones de liftIO consecutivas siempre se pueden combinar en un solo liftIO , por ejemplo:

do liftIO $ statement1 liftIO $ statement2

es lo mismo que:

liftIO $ do statement1 statement2

Con estos cambios se puede escribir nuestra función askfor2 :

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 convierte en una forma de salir de la mónada, como lanzar una excepción.

Otro ejemplo

Considere este simple bucle de solicitud 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 bien.

En un lenguaje convencional, podríamos escribir esto como un bucle infinito while con una sentencia break:

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 de 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 , el control vuelve a la parte superior del bloque do. Tenga en cuenta que el único valor que puede devolver loop2 es Nothing que corresponde a salir del bucle.

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


Supongamos que tiene que trabajar con valores de IO 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 ataduras produce rápidamente la temida "escalera de la fatalidad":

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? Quizás podríamos idear un nuevo tipo alrededor de IO (Maybe a) con una función de enlace que verifique automáticamente si el primer argumento es un Nothing en IO , lo que nos ahorra la molestia de verificarlo nosotros mismos. Algo como

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

Con la función de enlace:

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 esto es, en esencia, cómo funciona el transformador MaybeT . He MaybeOverM IO algunos detalles, como cómo implementar el return para el transformador y cómo "elevar" los valores de IO en los valores de MaybeOverM IO .

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