haskell - traduccion - mónada
¿Qué ventaja nos da Monad sobre un Aplicativo? (7)
Aquí hay un par de funciones que usan la interfaz Monad
.
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= /z -> if z then x else y
whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)
No puede implementarlos con la interfaz Applicative
. Pero por el bien de la iluminación, intentemos ver dónde van las cosas mal. Qué tal si..
import Control.Applicative
ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (/c'' x'' y'' -> if c'' then x'' else y'') <$> c <*> x <*> y
¡Se ve bien! Tiene el tipo correcto, ¡debe ser lo mismo! Vamos a verificar para asegurarnos ...
*Main> ifM (Just True) (Just 1) (Just 2)
Just 1
*Main> ifM (Just True) (Just 1) (Nothing)
Just 1
*Main> ifA (Just True) (Just 1) (Just 2)
Just 1
*Main> ifA (Just True) (Just 1) (Nothing)
Nothing
Y ahí está tu primer indicio de la diferencia. No puede escribir una función usando solo la interfaz Applicative
que replica ifM
.
Si se divide esto en pensar en valores de la forma fa
como "efectos" y "resultados" (ambos son términos aproximados muy confusos que son los mejores términos disponibles, pero no muy buenos), puede mejorar su comprensión aquí. En el caso de valores de tipo Maybe a
, el "efecto" es éxito o fracaso, como un cálculo. El "resultado" es un valor de tipo a
que podría estar presente cuando el cálculo se complete. (El significado de estos términos depende en gran medida del tipo concreto, por lo que no creo que esta sea una descripción válida de algo más que Maybe
como tipo).
Dado ese ajuste, podemos ver la diferencia con más profundidad. La interfaz Applicative
permite que el flujo de control de "resultado" sea dinámico, pero requiere que el flujo de control de "efecto" sea estático. Si su expresión implica 3 cálculos que pueden fallar, la falla de cualquiera de ellos ocasiona la falla de todo el cálculo. La interfaz de Monad
es más flexible. Permite que el flujo de control de "efecto" dependa de los valores de "resultado". ifM
elige qué "efectos" del argumento incluir en sus propios "efectos" en función de su primer argumento. Esta es la gran diferencia fundamental entre ifA
e ifM
.
Hay algo aún más serio pasando con whileM
. Tratemos de hacer whileA
y ver qué pasa.
whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)
Bueno ... Lo que sucede es un error de compilación. (<*>)
no tiene el tipo correcto allí. whileA p step
tiene el tipo a -> fa
y el step x
tiene el tipo fa
. (<*>)
no es la forma correcta para unirlos. Para que funcione, el tipo de función debería ser f (a -> a)
.
Puedes probar muchas cosas más, pero eventualmente descubrirás que whileA
no tiene una implementación que funcione de manera whileM
como lo whileM
. Quiero decir, puedes implementar el tipo, pero simplemente no hay forma de hacerlo tanto en bucle como en terminación.
Hacer que funcione requiere join
o (>>=)
. (Bueno, o uno de los muchos equivalentes de uno de esos) Y esas cosas extra que obtienes de la interfaz de Monad
.
He leído este artículo , pero no entendí la última sección.
El autor dice que Monad nos da sensibilidad al contexto, pero es posible lograr el mismo resultado utilizando solo una instancia Aplicable:
let maybeAge = (/futureYear birthYear -> if futureYear < birthYear
then yearDiff birthYear futureYear
else yearDiff futureYear birthYear) <$> (readMay futureYearString) <*> (readMay birthYearString)
Es más feo sin sin-sintaxis, pero aparte de eso, no veo por qué necesitamos a Monad. ¿Alguien puede aclarar esto para mí?
Con Applicative, la secuencia de acciones efectivas que se realizará se fija en tiempo de compilación. Con Monad, se puede variar en tiempo de ejecución en función de los resultados de los efectos.
Por ejemplo, con un analizador aplicativo, la secuencia de acciones de análisis se fija para siempre. Eso significa que potencialmente puede realizar "optimizaciones" en él. Por otro lado, puedo escribir un analizador Monadic que analiza una descripción de la gramática BNF, construye dinámicamente un analizador para esa gramática y luego ejecuta ese analizador sobre el resto de la entrada. Cada vez que ejecuta este analizador, potencialmente construye un analizador completamente nuevo para analizar la segunda parte de la entrada. El solicitante no tiene ninguna esperanza de hacer tal cosa, y no hay posibilidad de realizar optimizaciones en tiempo de compilación en un analizador que aún no existe ...
Como puede ver, a veces la "limitación" de Applicative es en realidad beneficiosa, y algunas veces se requiere la potencia adicional que ofrece Monad para realizar el trabajo. Es por eso que tenemos ambos.
Con las mónadas, los efectos posteriores pueden depender de los valores previos. Por ejemplo, puede tener:
main = do
b <- readLn :: IO Bool
if b
then fireMissiles
else return ()
No se puede hacer eso con Applicative
s - el valor resultante de un cálculo con efecto no puede determinar qué efecto seguirá.
Algo relacionado:
- ¿Por qué los funtores aplicativos tienen efectos secundarios, pero los funtores no?
- Buenos ejemplos de Not a Functor / Functor / Applicative / Monad?
Por otro lado, aquí hay un ejemplo práctico de la división Applicative
/ Monad
donde los Applicative
tienen una ventaja: ¡manejo de errores! Es claro que tenemos una implementación de Either
que lleva errores, pero siempre termina temprano.
Left e1 >> Left e2 === Left e1
Puedes pensar en esto como un efecto de mezclar valores y contextos. Dado que (>>=)
tratará de pasar el resultado del valor Either ea
a una función como a -> Either eb
, debe fallar inmediatamente si la entrada Either
está a la Left
.
Applicative
solo pasan sus valores al cómputo final puro después de ejecutar todos los efectos. Esto significa que pueden demorar el acceso a los valores durante más tiempo y podemos escribir esto.
data AllErrors e a = Error e | Pure a deriving (Functor)
instance Monoid e => Applicative (AllErrors e) where
pure = Pure
(Pure f) <*> (Pure x) = Pure (f x)
(Error e) <*> (Pure _) = Error e
(Pure _) <*> (Error e) = Error e
-- This is the non-Monadic case
(Error e1) <*> (Error e2) = Error (e1 <> e2)
Es imposible escribir una instancia de AllErrors
para AllErrors
que ap
coincida (<*>)
porque (<*>)
aprovecha el primero y el segundo contexto antes de usar cualquier valor para obtener ambos errores y (<>)
juntos. Monad
ic (>>=)
y (join)
solo pueden acceder a contextos entrelazados con sus valores. Es por eso que la instancia Either
de Either
está sesgada a la izquierda, por lo que también puede tener una instancia armónica de Monad
.
> Left "a" <*> Left "b"
Left ''a''
> Error "a" <*> Error "b"
Error "ab"
Si intenta convertir la firma de tipo de bind
de bind
y Aplicativo <*>
al lenguaje natural, encontrará que:
bind
: te daré el valor contenido y me devolverás un nuevo valor empaquetado
<*>
: Usted me da una función empaquetada que acepta un valor contenido y devuelve un valor y lo usaré para crear un nuevo valor empaquetado basado en mis reglas.
Ahora, como puede ver en la descripción anterior, bind
le da más control en comparación con <*>
Si trabaja con Applicatives, la "forma" del resultado ya está determinada por la "forma" de la entrada, por ejemplo, si llama [f,g,h] <*> [a,b,c,d,e]
, su resultado será una lista de 15 elementos, independientemente de los valores que tengan las variables. No tiene esta garantía / limitación con mónadas. Considera [x,y,z] >>= join replicate
: para [0,0,0]
obtendrás el resultado []
, para [1,2,3]
el resultado [1,2,2,3,3,3]
.
Como dijo Stephen Tetley en un comentario , ese ejemplo en realidad no usa la sensibilidad al contexto. Una forma de pensar acerca de la sensibilidad al contexto es que permite elegir qué acciones tomar dependiendo de los valores monádicos. Los cálculos aplicativos siempre deben tener la misma "forma", en cierto sentido, independientemente de los valores involucrados; los cómputos monádicos no necesitan. Personalmente creo que esto es más fácil de entender con un ejemplo concreto, así que veamos uno. Aquí hay dos versiones de un programa simple que le piden que ingrese una contraseña, verifique que haya ingresado la correcta e imprima una respuesta según lo haya hecho o no.
import Control.Applicative
checkPasswordM :: IO ()
checkPasswordM = do putStrLn "What''s the password?"
pass <- getLine
if pass == "swordfish"
then putStrLn "Correct. The secret answer is 42."
else putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
checkPasswordA :: IO ()
checkPasswordA = if'' . (== "swordfish")
<$> (putStrLn "What''s the password?" *> getLine)
<*> putStrLn "Correct. The secret answer is 42."
<*> putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
if'' :: Bool -> a -> a -> a
if'' True t _ = t
if'' False _ f = f
Carguemos esto en GHCi y verifiquemos qué sucede con la versión monádica:
*Main> checkPasswordM
What''s the password?
swordfish
Correct. The secret answer is 42.
*Main> checkPasswordM
What''s the password?
zvbxrpl
INTRUDER ALERT! INTRUDER ALERT!
Hasta aquí todo bien. Pero si usamos la versión aplicativa:
*Main> checkPasswordA
What''s the password?
hunter2
Correct. The secret answer is 42.
INTRUDER ALERT! INTRUDER ALERT!
Ingresamos la contraseña incorrecta, ¡pero todavía tenemos el secreto! ¡Y una alerta de intruso! Esto es porque <$>
y <*>
, o equivalentemente, liftA n
/ liftM n
, siempre ejecutan los efectos de todos sus argumentos. La versión aplicativa se traduce, en notación, a
do pass <- putStrLn "What''s the password?" *> getLine)
unit1 <- putStrLn "Correct. The secret answer is 42."
unit2 <- putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
pure $ if'' (pass == "swordfish") unit1 unit2
Y debe quedar claro por qué esto tiene un comportamiento incorrecto. De hecho, cada uso de functors aplicativos es equivalente al código monádico de la forma
do val1 <- app1
val2 <- app2
...
valN <- appN
pure $ f val1 val2 ... valN
(donde se permite que algunas appI
sean de la forma pure xI
). Y, de manera equivalente, cualquier código monádico en esa forma puede reescribirse como
f <$> app1 <*> app2 <*> ... <*> appN
o equivalentemente como
liftAN f app1 app2 ... appN
Para pensar sobre esto, considere los métodos de Applicative
:
pure :: a -> f a
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
Y luego considere lo que Monad
agrega:
(=<<) :: (a -> m b) -> m a -> m b
join :: m (m a) -> m a
(Recuerde que solo necesita uno de esos).
Moviendo mucho, si lo piensas, la única forma en que podemos armar las funciones aplicativas es construir cadenas de la aplicación f <$> app1 <*> ... <*> appN
, y posiblemente anidar esas cadenas ( ej. , f <$> (g <$> x <*> y) <*> z
). Sin embargo, (=<<)
(o (>>=)
) nos permite tomar un valor y producir diferentes cómputos monádicos dependiendo de ese valor, que podría construirse sobre la marcha. Esto es lo que usamos para decidir si computamos "imprimir el secreto", o calcular "imprimir una alerta de intruso", y por qué no podemos tomar esa decisión solo con los funtores aplicativos; ninguno de los tipos de funciones aplicativas le permite consumir un valor simple.
Puedes pensar en join
en concierto con fmap
de manera similar: como mencioné en un comentario , puedes hacer algo como
checkPasswordFn :: String -> IO ()
checkPasswordFn pass = if pass == "swordfish"
then putStrLn "Correct. The secret answer is 42."
else putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
checkPasswordA'' :: IO (IO ())
checkPasswordA'' = checkPasswordFn <$> (putStrLn "What''s the password?" *> getLine)
Esto es lo que sucede cuando queremos elegir un cálculo diferente según el valor, pero solo tenemos funcionalidades de aplicación disponibles para nosotros. Podemos elegir dos cálculos diferentes para regresar, pero están envueltos dentro de la capa externa del funtor aplicativo. Para usar realmente el cálculo que hemos elegido, necesitamos join
:
checkPasswordM'' :: IO ()
checkPasswordM'' = join checkPasswordA''
Y esto hace lo mismo que la versión monádica anterior (siempre que import Control.Monad
primero, para join
):
*Main> checkPasswordM''
What''s the password?
12345
INTRUDER ALERT! INTRUDER ALERT!