¿Cómo es el operador de flecha un Functor aplicativo en Haskell?

7

Nota: Todavía estoy aprendiendo a Haskell, no he alcanzado los Monoids, estudiando los Functors aplicativos.

Vi esta implementación y no estoy del todo claro:

instance Applicative ((->) r) where  
    pure x = (\_ -> x)  
    f <*> g = \x -> f x (g x)

Esto es lo que pienso:

Ahora obtengo lo que (->) r a es pero no estoy del todo claro en lo que hace. Esto es lo que creo que hace:

  • Toma dos argumentos (tipos) y devuelve una función del primer al segundo tipo, es decir, -> Int Int devuelve Int -> Int , que es de tipo * , pero ¿qué pasa con la definición?

Ahora ((->) r se va a definir como un functor aplicativo que es un functor que contiene r -> x . Así que tenemos dos casos:

  • [ pure x = (\_ -> x) ]: para pure necesitamos envolver un -> r x en algún funtor, pero estamos definiendo una función que simplemente ignora el tipo de datos de entrada y devuelve el tipo de datos x . el ejemplo pure (*2) debería devolver Maybe (Int -> Int) o algún otro functor que vincule la función dada -> r x
  • [ f <*> g = \x -> f x (g x) ]: para dos funciones dadas cuyo primer argumento (tipo de datos) es igual a r , deseamos <*> o fmap uno sobre otro, es decir, (((->) r) f) <*> (((->) r) g) o (r->f) <*> (r->g) pero no 't <*> igual que (.) para las funciones pero para (.) deberían ser algo así como a->b y b->c ? ejemplo (*2) <*> (+7) de la forma (Int -> Int) <*> (Int -> Int) ? (¿Qué es inválido pero (+) <$> (*2) <*> (+7) es válido?)

Nota: para (+) <$> (*2) <*> (+7) Creo que es ((+) <$> (*2)) <*> (+7) , que es + (*2 x) y fmap -ed en (+7) , pero ¿cómo se logra esto?

¿En qué me estoy equivocando?

    
pregunta RE60K 02.05.2017 - 23:37

3 respuestas

6

El tipo (->) r a es solo un alias para r -> a , nada más. Su tipo es * ("tipo").

El constructor de tipo (->) r se puede considerar como r -> ___ , donde r ya se conoce, pero ___ se debe completar más adelante. Su tipo es * -> * ("constructor de tipo").

El constructor de tipo (->) r está parametrizado por el tipo de entrada r . Para reducir la confusión de riesgos, podría ser útil arreglar r a algo concreto. Entonces, para el resto de esta respuesta, elegiré r = Int . La siguiente explicación funcionará para cualquier opción de r .

Tanto los funtores aplicativos como los funtores clasifican constructores de tipo , no tipos :

  • Decimos que [___] (constructor de listas) es un functor, Int -> ___ (funciones que esperan un entero) es un functor, IO ___ es un functor, Maybe ___ es un functor, etc.
  • Nosotros no decimos que [Int] (lista de enteros) es un functor, ni decimos que Int -> String (las funciones esperan un entero y devuelven una cadena) es un functor, ni IO () , ni Maybe String , etc. (Por supuesto, en la escritura informal la gente dice esto por descuido, pero al aprender es importante tener en cuenta esta distinción).
pure :: a ->        f a     -- general type, where f is Applicative
pure :: a -> (->) Int a     -- specialized type for f = ((->) Int)
pure :: a ->  (Int -> a)    -- just another way to write the above
pure x = (\_ -> x)

La función pure crea una función que devolverá a , ignorando cualquier Int que recibirá. Proporciona una manera de "envolver" las cosas en un funtor aplicativo.

Como ejemplo concreto, si escribe pure "hi" , esto crea una función \_ -> "hi" que siempre devuelve "hi" sin importar qué entrada reciba. Para recuperar tu "hi" , debes aplicar el resultado a cualquier entero, "desenvolviendo" el functor aplicativo:

>>> (pure "hi") 42
"hi"
(<*>) ::        f (a -> b)  ->        f a  ->        f b     -- general type, where f is Applicative
(<*>) :: (->) Int (a -> b)  -> (->) Int a  -> (->) Int b     -- specialized type for f = ((->) Int)
(<*>) ::  (Int -> (a -> b)) ->  (Int -> a) ->  (Int -> b)    -- just another way to write the above
f <*> g = \x -> f x (g x)

La función <*> tiene dos funciones:

  • La primera función f devolverá una función a -> b a cambio de un Int .
  • La segunda función g devolverá algún valor a a cambio de un Int .

El objetivo de <*> es crear una nueva función que produzca algún valor b a cambio de un Int . Hay una forma "obvia" de jugar a este juego: cuando obtienes Int más adelante en el futuro, lo envías tanto a f como a g y, a cambio, te darán a -> b y% co_de. %. Luego, puede aplicar a a a -> b para obtener a , lo que logra el objetivo.

Aquí hay un ejemplo concreto: b . Esta expresión crea una función que siempre produce pure (+) <*> pure 2.0 <*> pure 3.0 a cambio de cualquier 5.0 que le des. Como de costumbre, puede "desenvolverlo" aplicándolo a algunos Int :

>>> (pure (+) <*> pure 2.0 <*> pure 3.0) 42
5.0

Pero hasta ahora, ninguno de estos ejemplos fue muy interesante, ya que solo "envuelven" las cosas usando Int , lo que siempre ignora el argumento. Aquí hay una más interesante:

>>> let get input = input
>>> (pure (-) <*> get <*> pure 7) 42
35
>>> (pure (-) <*> get <*> pure 7) 10
3

En comparación con los ejemplos anteriores, la función pure es bastante inusual ya que inspecciona realmente el argumento get , devolviendo exactamente lo que ve. Esto significa que el resultado de esta cadena de cálculos difiere según el argumento que reciba al final.

  pure (-) <*> get <*> pure 7
≡ (\input -> (-)) <*> (\input -> input) <*> (\input -> 7)
≡ (\input -> (-) input) <*> (\input -> 7)
≡ (\input -> (-) input 7)
≡ (\input -> input - 7)

De hecho, si solo observa la expresión aplicativa original y entrecierra los ojos un poco para ignorar Int y pure , puede leerlo directamente como

  (-) INPUT 7
≡ INPUT - 7

donde <*> representa el valor futuro de INPUT .

Ahora considera tu ejemplo:

  (+) <$> (*2) <*> (+7)
≡ (+) <$> (\input -> input * 2) <*> (\input -> input + 7)
≡ (\input -> (+) (input * 2)) <*> (\input -> input + 7)
≡ (\input -> (+) (input * 2) (input + 7))
≡ (\input -> (input * 2) + (input + 7))

Informalmente, esto describe el siguiente cálculo:

  (+) (INPUT * 2) (INPUT + 7)
≡ (INPUT * 2) + (INPUT + 7)

Por ejemplo, si la entrada es 42, debe esperar 133 como resultado:

>>> ((+) <$> (*2) <*> (+7)) 42
133

El propósito del functor aplicativo Int es permitir al programador separar la lógica que depende del argumento de entrada del código real que suministra el valor concreto. En otras palabras, en el código real es probable que esté creando una expresión aplicativa bastante grande y complicada, sin saber cuál será la entrada. No estaría desenvolviendo el valor aplicativo de inmediato, pero probablemente en un lugar lejano en el código (¡quizás en otro proyecto por completo!).

    
respondido por el Rufflewind 03.05.2017 - 06:45
5

Usemos algún razonamiento ecuacional.

Nuestro functor f es (->) r , por lo que f a es (->) r a , que es solo r -> a .

pure :: a -> f a , por lo que en este caso pure :: a -> (r -> a) .

<*> :: f (a -> b) -> f a -> f b , entonces obtenemos <*> :: r -> (a -> b) -> (r -> a) -> (r -> b) .

fmap :: (a -> b) -> f a -> f b , por lo que obtenemos fmap :: (a -> b) -> (r -> a) -> (r -> b) .

Si miras lo que hace fmap , toma una función de argumento único y "la pone en" el functor. ¿Qué pasa con las funciones de dos o más argumentos? Por eso tenemos funtores aplicativos.

Digamos que tenemos una función g :: a -> b -> c -> d . Queremos aplicarlo a los argumentos x :: f a , y :: f b y z :: f c .

Así que hacemos pure g <*> x <*> y <*> z .

pure g :: f (a -> b -> c -> d) . Vamos a reescribirlo como f (a -> (b -> c -> d)) , ya que <*> espera un argumento de tipo f (a -> b) .

pure g <*> x :: f (b -> c -> d) . Podemos reescribirlo como f (b -> (c -> d)) para mayor claridad. Puede ver a dónde va esto y terminar con un resultado del tipo f d .

Podemos hacer lo mismo para fmap , así que fmap f x = pure f <*> x . Ya que vamos a comenzar todas estas cadenas con la misma cosa, incluso podemos abreviar fmap como <$> , por lo que las cosas parecen g <$> x <*> y <*> z .

  

pero no es <*> igual que (.)

No, y se puede ver mirando la firma de tipo. (.) :: (b -> c) -> (a -> b) -> (a -> c) , que coincide con nuestra firma de tipo para fmap anterior, excepto para barajar las letras.

  

(*2) <*> (+7)

Esto no se comprueba, como notaste. ¿Por qué?

Digamos que f = (*2) y g = (+7) , y ambos son del tipo Int -> Int .

<*> :: (r -> (a -> b)) -> (r -> a) -> (r -> b) .

Pero sabemos que r = Int , entonces <*> :: (Int -> (a -> b)) -> (Int -> a) -> (Int -> b) . Nuestro segundo argumento nos dice que a = Int , que está bien, pero nuestro primer argumento es Int -> Int y no tiene suficientes argumentos.

  

Creo que es ((+) <$> (*2)) <*> (+7) que es + (*2 x) y fmap-ed en (+7) pero, ¿cómo se logra esto?

((+) <$> (*2)) <*> (+7) significa \x -> (x*2) + (x+7) . Si observas la definición de <*> , verás que x se alimenta tanto en f como en g .

    
respondido por el Michael Shaw 03.05.2017 - 02:34
4

Una cosa que falta es que el "operador de flecha" (->) funciona en tipos , no en valores. (En otras palabras, es un constructor de tipo )

(->) Int Int es idéntico a Int -> Int , que es un tipo de función familiar que conoces y amas. No es una función; es un tipo de función.

  

Ahora ((->) r se va a definir como un functor aplicativo que es un functor que contiene r -> x . Así que tenemos dos casos:

No estoy seguro de lo que quieres decir con "contener" aquí. Esto es definir que (->) r aka r -> es un functor. No está definiendo r -> x para ser un functor. Y un funtor no "contiene" nada. (Bueno, List es un functor que contiene cosas, pero otros functors no tienen que hacerlo)

  

Para pure necesitamos incluir un -> r x en algún funtor

No. Para pure necesitamos ajustar un determinado x en un -> r x a.k.a. r -> x (que es un functor -> r aplicado a x ).

  

pero estamos definiendo una función que simplemente ignora el tipo de datos de entrada y devuelve el tipo de datos x.

De hecho. Así es como se convierte un x en un -> r x a.k.a. r -> x .

  

el ejemplo pure (*2) debería devolver Maybe (Int -> Int)

Lo hace - con el functor Maybe . pure (*2) :: (Maybe (Int -> Int)) haría eso, pero no devolverá Maybe (Int -> Int) , porque ese es un tipo; devolverá Just (*2) .

  

f <*> g = \x -> f x (g x) : para dos funciones dadas cuyo primer argumento (tipo de datos) es el mismo que r, deseamos <*> o fmap uno sobre otro, es decir, (((->) r) f) <*> (((->) r) g) o (r->f) <*> (r->g)

(<*>) no es lo mismo que fmap . Para este functor, fmap se define como el mismo (.) , y (<*>) no lo es.

  

pero no es <*> igual que (.) para funciones

No.

  

pero para (.) deberían ser algo como a->b y b->c ?

Bien ... y para (<*>) no son así, por lo que claramente (<*>) es diferente de (.)

  

ejemplo (*2) <*> (+7) de la forma (Int -> Int) <*> (Int -> Int) ?

En el funtor aplicativo (->) r , <*> puede considerarse como "aplicación diferida".

(->) r es similar al Reader r mónada: representa cálculos que también tienen acceso a algún "valor de entorno". Un "valor" de tipo a que depende del entorno se representa como (->) r a a.k.a. r -> a (cf. Reader r a ). En otras palabras, un r -> a es un " a envuelto en el functor r -> ")

Ahora podemos comparar las cosas con sus formas equivalentes "fuera" del funtor, para ver cómo funcionan las cosas "adentro".

Dentro del functor, (*2) y (+7) son valores (son el doble del valor del entorno y el valor del entorno más siete, respectivamente).

No puede aplicar un valor a otro valor, por lo que esto no funciona. ( (*2) <*> (+7) es análogo a 4 9 por ejemplo)

  

(que no es válido pero ¿ (+) <$> (*2) <*> (+7) es válido?)

(+) <$> (*2) <*> (+7) aplica la función (+) a estos valores, lo que produce otro "valor envuelto en un functor" que será tres veces el valor del entorno más siete. El resultado es equivalente a \x -> (x*2) + (x+7) .

    
respondido por el immibis 03.05.2017 - 03:39

Lea otras preguntas en las etiquetas