¿Qué ventaja nos da Monad sobre un Aplicativo?

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-syntax, pero aparte de eso, no veo por qué necesitamos a Monad. ¿Alguien puede aclarar esto para mí?

Aquí hay un par de funciones que usan la interfaz Monad .

 ifM :: Monad m => m Bool -> ma -> ma -> ma ifM cxy = c >>= \z -> if z then x else y whileM :: Monad m => (a -> m Bool) -> (a -> ma) -> a -> ma whileM p step x = ifM (px) (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 -> fa -> fa -> fa ifA cxy = (\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 dynamic, 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 -> fa) -> a -> fa whileA p step x = ifA (px) (whileA p step <*> step x) (pure x) 

Bueno … Lo que sucede es un error de comstackció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 .

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?

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 progtwig 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 -> fa (<$>) :: (a -> b) -> fa -> fb (<*>) :: f (a -> b) -> fa -> fb 

Y luego considere lo que Monad agrega:

 (=<<) :: (a -> mb) -> ma -> mb join :: m (ma) -> ma 

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

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 ea = Error e | Pure a deriving (Functor) instance Monoid e => Applicative (AllErrors e) where pure = Pure (Pure f) <*> (Pure x) = Pure (fx) (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" 

Con Applicative, la secuencia de acciones efectivas que se realizará se fija en tiempo de comstackció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 rest 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 comstackció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.

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] .

    Intereting Posts