haskell - Más diversión con los funtores aplicativos.
monads applicative (5)
Earlier , pregunté sobre la traducción de un código monádico para usar solo la instancia del functor aplicativo de Parsec. Desafortunadamente, recibí varias respuestas que respondían a la pregunta que literalmente hice, pero realmente no me dio mucha información. Así que déjame intentar esto de nuevo ...
Resumiendo mi conocimiento hasta ahora, un funtor aplicativo es algo más restringido que una mónada. En la tradición de "menos es más", restringir lo que el código puede hacer aumenta las posibilidades de manipulación de código loco. En cualquier caso, muchas personas parecen creer que usar aplicativo en lugar de mónada es una solución superior donde es posible.
La clase Applicative
se define en Control.Applicative
, cuyo listado de Haddock separa de manera útil los métodos de la clase y las funciones de utilidad con una amplia franja de instancias de clase entre ellos, para que sea difícil ver rápidamente todo lo que aparece en la pantalla a la vez. Pero las firmas de tipo pertinentes son
pure :: x -> f x
<*> :: f (x -> y) -> f x -> f y
*> :: f x -> f y -> f y
<* :: f x -> f y -> f x
<$> :: (x -> y) -> f x -> f y
<$ :: x -> f y -> f x
Tiene perfecto sentido, ¿verdad?
Bueno, Functor
ya nos da fmap
, que es básicamente <$>
. Es decir, dada una función de x
a y
, podemos asignar un fx
a un fy
. Applicative
agrega dos elementos esencialmente nuevos. Uno es pure
, que tiene aproximadamente el mismo tipo de return
(y varios otros operadores en varias clases de teoría de categorías). El otro es <*>
, que nos permite tomar un contenedor de funciones y un contenedor de entradas y producir un contenedor de salidas.
Usando los operadores anteriores, podemos hacer muy bien algo como
foo <$> abc <*> def <*> ghi
Esto nos permite tomar una función N-aria y generar sus argumentos a partir de N functors de manera que se generalice fácilmente a cualquier N.
Esto ya lo entiendo mucho. Hay dos cosas principales que todavía no entiendo.
Primero, las funciones *>
, <*
y <$
. De sus tipos, <* = const
, *> = flip const
, y <$
podría ser algo similar. Sin embargo, presumiblemente esto no describe lo que realmente hacen estas funciones. (??!)
En segundo lugar, cuando se escribe un analizador Parsec, cada entidad analizable suele tener un aspecto similar al siguiente:
entity = do
var1 <- parser1
var2 <- parser2
var3 <- parser3
...
return $ foo var1 var2 var3...
Dado que un funtor aplicativo no nos permite vincular los resultados intermedios a las variables de esta manera, me sorprende cómo reunirlos para la etapa final. No he podido envolver mi mente en torno a la idea lo suficiente como para comprender cómo hacerlo.
Las funciones <*
y *>
son muy simples: funcionan de la misma manera que >>
. El <*
funcionaría de la misma manera que <<
excepto <<
no existe. Básicamente, dado a *> b
, primero "haces" a
, luego "haces" b
y devuelves el resultado de b
. Para a <* b
, usted primero sigue "do" a
luego "do" b
, pero devuelve el resultado de a
. (Para los significados apropiados de "hacer", por supuesto.)
La función <$
es solo fmap const
. Entonces a <$ b
es igual a fmap (const a) b
. Simplemente tira el resultado de una "acción" y, en su lugar, devuelve un valor constante. La función Control.Monad
void
, que tiene un tipo Functor f => fa -> f ()
podría escribirse como () <$
.
Estas tres funciones no son fundamentales para la definición de un funtor aplicativo. ( <$
, de hecho, funciona para cualquier functor.) Esto, de nuevo, es como >>
para las mónadas. Creo que están en la clase para que sea más fácil optimizarlos para instancias específicas.
Cuando utiliza functors aplicativos, no "extrae" el valor del functor. En una mónada, esto es lo que hace >>=
, y para qué foo <- ...
desugars. En su lugar, pasa los valores ajustados a una función directamente usando <$>
y <*>
. Así que podrías reescribir tu ejemplo como:
foo <$> parser1 <*> parser2 <*> parser3 ...
Si desea variables intermedias, puede usar una instrucción let
:
let var1 = parser1
var2 = parser2
var3 = parser3 in
foo <$> var1 <*> var2 <*> var3
Como usted supuso correctamente, pure
es solo otro nombre para el return
. Entonces, para que la estructura compartida sea más obvia, podemos reescribir esto como:
pure foo <*> parser1 <*> parser2 <*> parser3
Espero que esto aclare las cosas.
Ahora solo una pequeña nota. La gente recomienda usar funciones de functor aplicativas para el análisis. Sin embargo, solo debes usarlos si tienen más sentido. Para cosas suficientemente complejas, la versión de la mónada (especialmente con notación-hacer) puede ser más clara. La razón por la que la gente recomienda esto es que
foo <$> parser1 <*> parser2 <*> parser3
es más corto y más legible que
do var1 <- parser1
var2 <- parser2
var3 <- parser3
return $ foo var1 var2 var3
Esencialmente, la f <$> a <*> b <*> c
es esencialmente como una aplicación de función levantada. Puede imaginar que <*>
es un reemplazo para un espacio (por ejemplo, la aplicación de función) de la misma manera que fmap
es un reemplazo para la aplicación de función. Esto también debería darle una idea intuitiva de por qué usamos <$>
- es como una versión elevada de $
.
Las respuestas ya dadas son excelentes, pero hay un pequeño punto (ish) que me gustaría explicar explícitamente, y tiene que ver con <*
, <$
y *>
.
Uno de los ejemplos fue
do var1 <- parser1
var2 <- parser2
var3 <- parser3
return $ foo var1 var2 var3
que también se puede escribir como foo <$> parser1 <*> parser2 <*> parser3
.
Supongamos que el valor de var2
es irrelevante para foo
, por ejemplo, es solo un espacio en blanco separado. Entonces tampoco tiene sentido que foo
acepte este espacio en blanco solo para ignorarlo. En este caso, foo
debería tener dos parámetros, no tres. Usando do
notation, puedes escribir esto como:
do var1 <- parser1
parser2
var3 <- parser3
return $ foo var1 var3
Si desea escribir esto usando solo <$>
y <*>
debería ser algo como una de estas expresiones equivalentes:
(/x _ z -> foo x z) <$> parser1 <*> parser2 <*> parser3
(/x _ -> foo x) <$> parser1 <*> parser2 <*> parser3
(/x -> const (foo x)) <$> parser1 <*> parser2 <*> parser3
(const . foo) <$> parser1 <*> parser2 <*> parser3
¡Pero eso es un poco difícil de conseguir con más argumentos!
Sin embargo, también puede escribir foo <$> parser1 <* parser2 <*> parser3
. Podría llamar a foo
la función semántica que se alimenta del resultado de parser1
y parser3
mientras ignora el resultado de parser2
en medio. La ausencia de >
está destinada a ser indicativa de la ignorancia.
Si desea ignorar el resultado de parser1
pero usa los otros dos resultados, puede escribir foo <$ parser1 <*> parser2 <*> parser3
, usando <$
lugar de <$>
.
Nunca he encontrado mucho uso para *>
, normalmente escribiría id <$ p1 <*> p2
para el analizador que ignora el resultado de p1
y simplemente analiza con p2
; podría escribir esto como p1 *> p2
pero eso aumenta la carga cognitiva para los lectores del código.
He aprendido esta forma de pensar solo para analizadores, pero luego se ha generalizado a Applicative
s; pero creo que esta notación viene de la biblioteca uuparsing ; Al menos lo usé en Utrecht hace más de 10 años.
Me gustaría agregar / reformular un par de cosas a las muy útiles respuestas existentes:
Los aplicativos son "estáticos". En pure f <*> a <*> b
, b
no depende de a
, por lo que puede analizarse estáticamente . Esto es lo que intentaba mostrar en mi respuesta a tu pregunta anterior (pero supongo que fallé, lo siento), que dado que en realidad no había una dependencia secuencial de los analizadores, no había necesidad de mónadas.
La diferencia clave que aportan las mónadas a la tabla es (>>=) :: Monad m => ma -> (a -> mb) -> ma
, o, alternativamente, join :: Monad m => m (ma)
. Tenga en cuenta que cada vez que tenga x <- y
dentro de notación, está usando >>=
. Estos dicen que las mónadas le permiten usar un valor "dentro" de una mónada para producir una nueva mónada, "dinámicamente". Esto no se puede hacer con un aplicativo. Ejemplos:
-- parse two in a row of the same character
char >>= /c1 ->
char >>= /c2 ->
guard (c1 == c2) >>
return c1
-- parse a digit followed by a number of chars equal to that digit
-- assuming: 1) `digit`s value is an Int,
-- 2) there''s a `manyN` combinator
-- examples: "3abcdef" -> Just {rest: "def", chars: "abc"}
-- "14abcdef" -> Nothing
digit >>= /d ->
manyN d char
-- note how the value from the first parser is pumped into
-- creating the second parser
-- creating ''half'' of a cartesian product
[1 .. 10] >>= /x ->
[1 .. x] >>= /y ->
return (x, y)
Por último, los aplicativos habilitan la aplicación de función levantada como lo menciona @WillNess. Para intentar tener una idea de cómo se ven los resultados "intermedios", puede mirar los paralelos entre la aplicación de función normal y levantada. Suponiendo que add2 = (+) :: Int -> Int -> Int
:
-- normal function application
add2 :: Int -> Int -> Int
add2 3 :: Int -> Int
(add2 3) 4 :: Int
-- lifted function application
pure add2 :: [] (Int -> Int -> Int)
pure add2 <*> pure 3 :: [] (Int -> Int)
pure add2 <*> pure 3 <*> pure 4 :: [] Int
-- more useful example
[(+1), (*2)]
[(+1), (*2)] <*> [1 .. 5]
[(+1), (*2)] <*> [1 .. 5] <*> [3 .. 8]
Desafortunadamente, no puede imprimir de manera significativa el resultado de pure add2 <*> pure 3
por la misma razón que no puede para add2
... frustrante. También es posible que desee ver la Identity
y sus instancias de clase de tipos para obtener un manejo de los aplicativos.
Puede ver los funtores, los aplicativos y las mónadas de esta forma: todos llevan una especie de "efecto" y un "valor". (Tenga en cuenta que los términos "efecto" y "valor" son solo aproximaciones; en realidad, no es necesario que existan efectos secundarios o valores, como en Identity
o Const
.)
- Con
Functor
, puede modificar los valores posibles dentro defmap
, pero no puede hacer nada con efectos dentro. Con
Applicative
, puede crear un valor sin ningún efecto conpure
, y puede secuenciar efectos y combinar sus valores en el interior. Pero los efectos y los valores están separados: cuando se secuencian efectos, un efecto no puede depender del valor de uno anterior. Esto se refleja en<*
,<*>
y*>
: secuencia los efectos y combinan sus valores, pero no puede examinar los valores internos de ninguna manera.Usted podría definir
Applicative
usando este conjunto alternativo de funciones:fmap :: (a -> b) -> (f a -> f b) pureUnit :: f () pair :: f a -> f b -> f (a, b) -- or even with a more suggestive type (f a, f b) -> f (a, b)
(donde
pureUnit
no tiene ningún efecto) y definapure
y<*>
partir de ellos (y viceversa). Aquí elpair
secuencia dos efectos y recuerda los valores de ambos. Esta definición expresa el hecho de queApplicative
es un functor monoidal .Ahora considere una expresión arbitraria (finita) que consiste en
pair
,fmap
,pureUnit
y algunos valores aplicativos primitivos. Tenemos varias reglas que podemos utilizar:fmap f . fmap g ==> fmap (f . g) pair (fmap f x) y ==> fmap (/(a,b) -> (f a, b)) (pair x y) pair x (fmap f y) ==> -- similar pair pureUnit y ==> fmap (/b -> ((), b)) y pair x pureUnit ==> -- similar pair (pair x y) z ==> pair x (pair y z)
Usando estas reglas, podemos reordenar
pair
, empujarfmap
s hacia afuera y eliminarpureUnit
s, por lo que eventualmente dicha expresión se puede convertir enfmap pureFunction (x1 `pair` x2 `pair` ... `pair` xn)
o
fmap pureFunction pureUnit
De hecho, en primer lugar, podemos recopilar todos los efectos juntos utilizando el
pair
y luego modificar el valor resultante utilizando una función pura.Con
Monad
, un efecto puede depender del valor de un valor monádico anterior. Esto los hace tan poderosos.
Puedo hacer algunas observaciones aquí, espero que sean útiles. Esto refleja mi comprensión de que en sí podría estar equivocado.
pure
es inusualmente llamado. Por lo general, las funciones se nombran en referencia a lo que producen, pero en pure x
es x
que es puro . pure x
produce un funtor aplicativo que "transporta" el pure x
. "Lleva" por supuesto es aproximado. Un ejemplo: pure 1 :: ZipList Int
es un ZipList
, que tiene un valor Int
puro, 1
.
<*>
, *>
y <*
no son funciones, sino métodos (esto responde a su primera preocupación). f
en sus tipos no es general (como lo sería, para las funciones) sino específico, según lo especificado por una instancia específica. Es por eso que de hecho no son solo $
, flip const
y const
. El tipo especializado f
especifica la semántica de combinación . En la programación de estilo aplicativo habitual, combinación significa aplicación. Pero con los funtores, una dimensión adicional está presente, representada por el tipo "portador" f
. En fx
, hay un "contenido", x
, pero también hay un "contexto", f
.
El estilo de los "funtores aplicativos" buscó habilitar la programación del "estilo aplicativo", con efectos. Los efectos están representados por funtores, transportistas, proveedores de contexto; "aplicativo" que se refiere al estilo aplicativo normal de la aplicación funcional. Escribir solo fx
para denotar la aplicación fue una vez una idea revolucionaria . Ya no había necesidad de sintaxis adicional, no (funcall fx)
, no había sentencias CALL
, ninguna de estas cosas adicionales - la combinación era aplicación ... No así, con efectos, aparentemente - nuevamente había esa necesidad de la sintaxis especial , cuando Programación con efectos. La bestia asesinada reapareció de nuevo.
Así que vino la Programación Aplicativa con Efectos para hacer de nuevo que la combinación signifique solo aplicación, en el contexto especial (quizás efectivo), si es que realmente lo era. Entonces, para a :: f (t -> r)
y b :: ft
, la combinación (casi simple) a <*> b
es una aplicación de contenido (o tipos t -> r
y t
), en un contexto dado (de tipo f
).
La distinción principal de las mónadas es que las mónadas no son lineales . En
do { x <- a
; y <- b x
; z <- c x y
; return
(x, y, z) }
el cálculo bx
depende de x
, y cxy
depende tanto de x
como de y
. Las funciones están anidadas :
a >>= (/x -> b x >>= (/y -> c x y >>= (/z -> .... )))
Si b
y c
no dependen de los resultados anteriores ( x
, y
), esto se puede hacer plano haciendo que las etapas de cómputo se vuelvan a empaquetar, los datos compuestos (esto aborda su segunda preocupación):
a >>= (/x -> b >>= (/y-> return (x,y))) -- `b ` sic
>>= (/(x,y) -> c >>= (/z-> return (x,y,z))) -- `c `
>>= (/(x,y,z) -> ..... )
y esto es esencialmente un estilo aplicativo ( b
, c
son completamente conocidos de antemano, independientemente del valor x
producido por a
, etc.). Entonces, cuando sus combinaciones crean datos que abarcan toda la información que necesitan para combinaciones adicionales, y no hay necesidad de "variables externas" (es decir, todos los cálculos ya son completamente conocidos, independientemente de los valores producidos por cualquiera de las etapas anteriores), puede Usa este estilo de combinación.
Pero si su cadena monádica tiene ramas que dependen de los valores de dichas variables "externas" (es decir, los resultados de las etapas anteriores del cálculo monádico), entonces no puede crear una cadena lineal a partir de ella. Es esencialmente monádico entonces.
Como ilustración, el primer ejemplo de ese artículo muestra cómo funciona la función "monádica"
sequence :: [IO a] → IO [a]
sequence [ ] = return [ ]
sequence (c : cs) = do
{ x <- c
; xs <- sequence cs -- `sequence cs` fully known, independent of `x`
; return
(x : xs) }
En realidad, se puede codificar en este estilo "plano y lineal" como
sequence :: (Applicative f) => [f a] -> f [a]
sequence [] = pure []
sequence (c : cs) = pure (:) <*> c <*> sequence cs
-- (:) x xs
No hay ningún uso aquí para la capacidad de la mónada de ramificarse en los resultados anteriores.
una nota sobre la excelente respuesta de Petr Pudlák : en mi "terminología" aquí, su pair
es una combinación sin aplicación . Muestra que la esencia de lo que los Functors aplicativos agregan a los Functors planos, es la capacidad de combinar. Entonces la aplicación se logra con el buen viejo fmap
. Esto sugiere funtores combinatorios como quizás un mejor nombre ( actualización: de hecho, "Monoidal Functors" es el nombre).