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ónadam
, 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 unInt
,MaybeT IO Int
es una expresiónMaybeT IO
que devuelve unInt
.
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ónMaybeT IO
, useliftIO
.
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").