In che modo l'operatore di frecce è un applicativo in Haskell?

7

Nota: continua ad imparare Haskell, non raggiunto i monoidi, studiando i Functional Applicative.

Ho visto questa implementazione e non ne sono del tutto chiaro:

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

Questo è quello che penso:

Ora ottengo ciò che (->) r a è, ma non sono del tutto chiaro su cosa faccia. Questo è quello che penso che faccia:

  • Richiede due argomenti (tipi) e restituisce una funzione dal primo al secondo tipo, cioè -> Int Int restituisce Int -> Int che è di tipo * ma che dire della definizione?

Ora ((->) r è goind da definire come un functor applicativo che è un functor contenente r -> x . Quindi abbiamo due casi:

  • [ pure x = (\_ -> x) ]: per pure dobbiamo racchiudere un dato -> r x in qualche functor, ma stiamo definendo una funzione che ignora semplicemente il tipo di dati di input e restituisce il tipo di dati x . esempio pure (*2) dovrebbe restituire Maybe (Int -> Int) o qualche altro functor che associa la funzione data -> r x
  • [ f <*> g = \x -> f x (g x) ]: per due funzioni date il cui primo argomento (tipo di dati) è uguale a r vogliamo <*> o fmap uno su un altro, cioè (((->) r) f) <*> (((->) r) g) o (r->f) <*> (r->g) ma isn 't <*> come (.) per le funzioni ma per (.) dovrebbero essere qualcosa come a->b e b->c ? esempio (*2) <*> (+7) della forma (Int -> Int) <*> (Int -> Int) ? (che risulta non valido ma (+) <$> (*2) <*> (+7) è valido?)

Nota: per (+) <$> (*2) <*> (+7) Penso che sia ((+) <$> (*2)) <*> (+7) che è + (*2 x) y fmap -ed su (+7) ma come viene raggiunto?

Che cosa sto sbagliando?

    
posta RE60K 02.05.2017 - 23:37
fonte

3 risposte

6

Il tipo (->) r a è solo un alias per r -> a , niente di più. Il suo tipo è * ("tipo").

Il costruttore di tipi (->) r può essere pensato come r -> ___ dove r è già noto ma ___ deve essere compilato in seguito. Il suo tipo è * -> * ("tipo costruttore").

Il costruttore di tipo (->) r è esso stesso parametrizzato dal tipo di input r . Per ridurre il rischio di confusione, potrebbe essere utile correggere r in qualcosa di concreto. Quindi per il resto di questa risposta, sceglierò r = Int . La spiegazione seguente funzionerà per qualsiasi scelta di r .

Sia i funtori applicativi che i funtori classificano costruttori di tipi , non tipi :

  • Diciamo che [___] (list constructor) è un functor, Int -> ___ (le funzioni che aspettano un intero) è un functor, IO ___ è un functor, Maybe ___ è un functor, ecc.
  • Facciamo non diciamo [Int] (lista di interi) è un functor, né diciamo Int -> String (le funzioni aspettano un intero e restituiscono una stringa) è un functor, né IO () , né Maybe String , ecc. (Naturalmente, nella scrittura informale le persone lo dicono per sciatteria, ma mentre imparano è importante tenere a mente questa distinzione.)
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 funzione pure crea una funzione che restituirà a , ignorando qualsiasi percentuale diInt che riceverà. Fornisce un modo per "avvolgere" le cose in un funtore applicativo.

Come esempio concreto, se scrivi pure "hi" , viene creata una funzione \_ -> "hi" che restituisce sempre "hi" indipendentemente dall'input che ottiene. Per riavere il tuo "hi" , applica il risultato a qualsiasi numero intero, "estraendo" il functor applicativo:

>>> (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 funzione <*> ha due funzioni:

  • La prima funzione f restituirà una funzione a -> b in cambio di un Int .
  • La seconda funzione g restituirà un valore a in cambio di un Int .

L'obiettivo di <*> è di creare una nuova funzione che produca qualche valore b in cambio di un Int . Esiste un modo "ovvio" per giocare a questo gioco: quando in futuro otterrai Int , lo invierai a f e g e in cambio ti forniranno a -> b e a . Quindi puoi applicare a -> b a a per ottenere b , che raggiunge l'obiettivo.

Ecco un esempio concreto: pure (+) <*> pure 2.0 <*> pure 3.0 . Questa espressione crea una funzione che restituisce sempre 5.0 in cambio di qualsiasi percentuale diInt che dai. Come al solito puoi "scartarlo" applicandolo a qualche Int :

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

Ma finora, nessuno di questi esempi è stato molto interessante, perché si limitano a "avvolgere" le cose usando pure , che ignora sempre l'argomento. Ecco uno più interessante:

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

Rispetto agli esempi precedenti, la funzione get è piuttosto insolita in quanto fa effettivamente ispeziona l'argomento Int , restituendo esattamente ciò che vede. Ciò significa che il risultato di questa catena di calcoli varia a seconda dell'argomento che riceve alla fine.

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

In effetti, se guardi l'espressione applicativa originale e fai uno strappo un po 'per ignorare pure e <*> , puoi leggerlo direttamente come

  (-) INPUT 7
≡ INPUT - 7

dove INPUT rappresenta il futuro valore Int .

Ora considera il tuo esempio:

  (+) <$> (*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 questo descrive il seguente calcolo:

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

Ad esempio, se l'input è 42, dovresti aspettarti 133 come risultato:

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

Lo scopo del functor applicativo ((->) Int) per consentire al programmatore di separare la logica che dipende dall'argomento di input dal codice effettivo che fornisce il valore concreto. In altre parole, nel codice reale probabilmente si costruisce un'espressione applicativa piuttosto ampia e complicata, senza sapere quale sarà l'input. Non dovresti scartare subito il valore applicativo, ma probabilmente un posto lontano nel codice (forse in un altro progetto completamente!).

    
risposta data 03.05.2017 - 06:45
fonte
5

Usiamo un ragionamento equo.

Il nostro functor f è (->) r , quindi f a è (->) r a che è solo r -> a .

pure :: a -> f a , quindi in questo caso pure :: a -> (r -> a) .

<*> :: f (a -> b) -> f a -> f b , quindi otteniamo <*> :: r -> (a -> b) -> (r -> a) -> (r -> b) .

fmap :: (a -> b) -> f a -> f b , quindi otteniamo fmap :: (a -> b) -> (r -> a) -> (r -> b) .

Se guardi cosa fa fmap , prende una funzione a singolo argomento e "inserisce" il functor. Per quanto riguarda le funzioni di due o più argomenti? Ecco perché abbiamo dei funtori applicativi.

Supponiamo di avere una funzione g :: a -> b -> c -> d . Vogliamo applicarlo agli argomenti x :: f a , y :: f b e z :: f c .

Quindi facciamo pure g <*> x <*> y <*> z .

pure g :: f (a -> b -> c -> d) . Riscriviamolo come f (a -> (b -> c -> d)) , poiché <*> si aspetta un argomento di tipo f (a -> b) .

pure g <*> x :: f (b -> c -> d) . Possiamo riscriverlo come f (b -> (c -> d)) per chiarezza. Puoi vedere dove sta andando e finisci con un risultato di tipo f d .

Possiamo fare la stessa cosa per fmap , quindi fmap f x = pure f <*> x . Dal momento che inizieremo tutte queste catene con la stessa cosa, possiamo anche abbreviare fmap come <$> , quindi le cose assomigliano a g <$> x <*> y <*> z .

but isn't <*> same as (.)

No, e puoi dirlo guardando la firma del tipo. (.) :: (b -> c) -> (a -> b) -> (a -> c) , che corrisponde alla nostra firma del tipo per fmap sopra, tranne per il mescolamento delle lettere.

(*2) <*> (+7)

Ciò non corrisponde al tipo, come hai notato. Perché?

Diciamo f = (*2) e g = (+7) , ed entrambi sono di tipo Int -> Int .

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

Ma sappiamo che r = Int , quindi <*> :: (Int -> (a -> b)) -> (Int -> a) -> (Int -> b) . Il nostro secondo argomento ci dice che a = Int , che va bene, ma il nostro primo argomento è Int -> Int e non ha argomenti sufficienti.

I think it is ((+) <$> (*2)) <*> (+7) which is + (*2 x) y fmap-ed onto (+7) but how is this achieved?

((+) <$> (*2)) <*> (+7) significa \x -> (x*2) + (x+7) . Se osservi la definizione di <*> , vedi che x viene alimentato sia in f che in g .

    
risposta data 03.05.2017 - 02:34
fonte
4

Una cosa che ti manca è che "l'operatore freccia" (->) funzioni su tipi non su valori. (In altre parole è un tipo costruttore )

(->) Int Int è identico a Int -> Int che è un tipo di funzione familiare che conosci e ami. Non è una funzione; è un tipo di funzione.

Now ((->) r is goind to be defined as an applicative functor that is a functor containing r -> x. So we have two cases:

Non sono sicuro di cosa intendi per "contenere" qui. Questo sta definendo che (->) r aka r -> è un funtore. Non sta definendo r -> x come un funtore. E un funtore non "contiene" nulla. (Bene, List è un funtore che contiene cose, ma altri funtori non devono)

For pure we need to wrap a given -> r x into some functor

No. Per pure dobbiamo racchiudere un dato x in un -> r x a.k.a. r -> x (che è un functor -> r applicato a x ).

but we are defining a function that just ignores input data type and returns data type x.

In effetti. È così che converti una x in una -> r x a.k.a. r -> x .

example pure (*2) should return Maybe (Int -> Int)

Funziona con il functor Maybe . pure (*2) :: (Maybe (Int -> Int)) lo farebbe, ma non restituirà Maybe (Int -> Int) , perché è un tipo; restituirà Just (*2) .

f <*> g = \x -> f x (g x): For two given functions whose first argument (data type) is same as r we wish to <*> or fmap one onto another, i.e. (((->) r) f) <*> (((->) r) g) or (r->f) <*> (r->g)

(<*>) non è uguale a fmap . Per questo functor fmap è definito come uguale a (.) e (<*>) no.

but isn't <*> same as (.) for functions

No.

but for (.) they should be something like a->b and b->c?

Giusto ... e per (<*>) non sono così, quindi chiaramente (<*>) è diverso da (.)

example (*2) <*> (+7) of the form (Int -> Int) <*> (Int -> Int)?

Nel functor applicativo (->) r , <*> può essere pensato come "applicazione differita".

(->) r è simile a Reader r monad - rappresenta calcoli che hanno anche accesso ad alcuni "valori ambientali". Un "valore" di tipo a che dipende dall'ambiente è rappresentato come (->) r a a.k.a. r -> a (cfr Reader r a ). In altre parole, un r -> a è un " a avvolto nel functor r -> ")

Ora possiamo confrontare le cose con le loro forme equivalenti "esterne" al funtore, per vedere come funzionano le cose "dentro".

All'interno del functor, (*2) e (+7) sono valori (sono due volte il valore dell'ambiente e il valore dell'ambiente più sette, rispettivamente).

Non puoi applicare un valore a un altro valore, quindi questo non funziona. ( (*2) <*> (+7) è analogo a 4 9 per esempio)

(which comes out to be invalid but (+) <$> (*2) <*> (+7) is valid?)

(+) <$> (*2) <*> (+7) applica la funzione (+) a questi valori - ottenendo un altro "valore avvolto in un functor" che sarà tre volte il valore dell'ambiente più sette. Il risultato è equivalente a \x -> (x*2) + (x+7) .

    
risposta data 03.05.2017 - 03:39
fonte

Leggi altre domande sui tag