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