you - funciona como funcionadores aplicativos(Haskell/LYAH)
monads learn you a haskell (3)
El capítulo 11 de Aprende a ti Haskell presenta la siguiente definición:
instance Applicative ((->) r) where
pure x = (/_ -> x)
f <*> g = /x -> f x (g x)
Aquí, el autor realiza gestos de mano poco característicos ("La implementación de la instancia para <*> es un poco críptica, por lo que es mejor si simplemente [lo mostramos en acción sin explicarlo]"). Espero que alguien aquí me ayude a resolverlo.
De acuerdo con la definición de clase aplicativa, (<*>) :: f (a -> b) -> fa -> fb
En la instancia, sustituyendo ((->)r)
por f
: r->(a->b)->(r->a)->(r->b)
Entonces, la primera pregunta, ¿cómo puedo obtener ese tipo para f <*> g = /x -> fx (gx)
?
Pero incluso si tomo esa última fórmula por sentado, tengo problemas para que esté de acuerdo con los ejemplos que doy a GHCi. Por ejemplo:
Prelude Control.Applicative> (pure (+5)) <*> (*3) $ 4
17
En su lugar, esta expresión parece consistente con f <*> g = /x -> f (gx)
(tenga en cuenta que en esta versión x
no aparece después de f
.
Me doy cuenta de que esto es complicado, así que gracias por tener paciencia conmigo.
"En la instancia, sustituyendo
((->)r)
porf
:r->(a->b)->(r->a)->(r->b)
"
Por qué, eso no está bien. En realidad es (r->(a->b)) -> (r->a) -> (r->b)
, y eso es lo mismo que (r->a->b) -> (r->a) -> r -> b
. Es decir, mapeamos un infijo y una función que devuelve el infijo ''argumento de la mano derecha, a una función que toma solo el infijo'' LHS y devuelve su resultado. Por ejemplo,
Prelude Control.Applicative> (:) <*> (/x -> [x]) $ 2
[2,2]
Antes que nada, recuerda cómo se define fmap
para los fmap
:
fmap f x = pure f <*> x
Esto significa que su ejemplo es el mismo que (fmap (+ 5) (* 3)) 4
. La función fmap
para funciones es solo composición, por lo que su expresión exacta es la misma que ((+ 5) . (* 3)) 4
.
Ahora, pensemos por qué la instancia está escrita tal como es. Lo que <*>
hace es esencialmente aplicar una función en el funtor a un valor en el funtor. Especializado en (->) r
, esto significa que aplica una función devuelta por una función de r
a un valor devuelto por una función de r
. Una función que devuelve una función es solo una función de dos argumentos. Entonces la verdadera pregunta es esta: ¿cómo aplicarías una función de dos argumentos ( r
y a
, devolviendo b
) a un valor a
devuelto por una función de r
?
Lo primero que debe observar es que debe devolver un valor de tipo (->) r
que significa que el resultado también debe ser una función de r
. Como referencia, aquí está la función <*>
:
f <*> g = /x -> f x (g x)
Como queremos devolver una función tomando un valor de tipo r
, x :: r
. La función que devolvemos debe tener un tipo r -> b
. ¿Cómo podemos obtener un valor de tipo b
? Bueno, tenemos una función f :: r -> a -> b
. Como r
va a ser el argumento de la función de resultado, obtenemos eso de forma gratuita. Entonces ahora tenemos una función de a -> b
. Entonces, mientras tengamos algún valor de tipo a
, podemos obtener un valor de tipo b
. Pero, ¿cómo obtenemos un valor de tipo a
? Bueno, tenemos otra función g :: r -> a
. Entonces podemos tomar nuestro valor de tipo r
(el parámetro x
) y usarlo para obtener un valor de tipo a
.
Entonces, la idea final es simple: utilizamos el parámetro para obtener primero un valor de tipo a
conectándolo a g
. El parámetro tiene tipo r
, g
tiene tipo r -> a
, entonces tenemos una a
. Luego, conectamos el parámetro y el nuevo valor en f
. Necesitamos ambos porque f
tiene un tipo r -> a -> b
. Una vez que conectamos tanto una r
como a
a, tenemos una b1
. Como el parámetro está en lambda, el resultado tiene un tipo r -> b
, que es lo que queremos.
Repasando tu pregunta original, creo que hay un punto sutil pero muy importante que podrías haber pasado por alto. Usando el ejemplo original de LYAH:
(+) <$> (+3) <*> (*100) $ 5
Esto es lo mismo que:
pure (+) <*> (+3) <*> (*100) $ 5
La clave aquí es el pure
antes (+)
, que tiene el efecto de boxeo (+)
como aplicativo. Si observa qué tan pure
está definido, puede ver que para desempaquetarlo, debe proporcionar un argumento adicional, que puede ser cualquier cosa. Aplicando <*>
a (+) <$> (+3)
, obtenemos
/x -> (pure (+)) x ((+3) x)
Observe en (pure (+)) x
, estamos aplicando x
a pure
a unbox (+)
. Entonces ahora tenemos
/x -> (+) ((+3) x)
Al agregar (*100)
para obtener (+) <$> (+3) <*> (*100)
y aplicar <*>
nuevo, obtenemos
/x -> (+) ((+3) x) ((*100) x)
Entonces, en conclusión, la x
después de f
NO es el primer argumento para nuestro operador binario, se usa para DESBOX el operador dentro de pure
.