Gli applicativi compongono, le monadi no


110

Gli applicativi compongono, le monadi no.

Cosa significa la dichiarazione di cui sopra? E quando è preferibile l'uno all'altro?


5
Da dove hai preso questa dichiarazione? Potrebbe essere utile vedere un contesto.
fuz

@ FUZxxl: L'ho sentito ripetutamente da molte persone diverse, recentemente da debasishg su Twitter.
missingfaktor il

3
@stephen tetley: Nota che molti di questi Applicativesono in realtà un'intera famiglia di Monads, vale a dire uno per ogni "forma" di struttura possibile. ZipListnon è una Monad, ma le ZipLists di lunghezza fissa lo sono. Readerè un comodo caso speciale (o è generale?) in cui la dimensione della "struttura" è fissata come cardinalità del tipo di ambiente.
CA McCann

3
@CAMcCann Tutti quegli applicativi scattanti (troncati o tamponati) si limitano alle monadi se si fissa la forma in un modo che equivale a una Readermonade fino all'isomorfismo. Una volta fissata la forma di un contenitore, codifica efficacemente una funzione da posizioni, come un trie di memo. Peter Hancock chiama tali funtori "Naperian", poiché obbediscono alle leggi dei logaritmi.
Pigworker

4
@stephen tetley: Altri esempi includono l'applicativo monoide costante (che è una composizione di monadi ma non una monade) e l'applicativo ritardo unità (che è meglio non ammettere l'unione).
Pigworker

Risposte:


115

Se confrontiamo i tipi

(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m =>       m s -> (s -> m t) -> m t

abbiamo un'idea di cosa separa i due concetti. Quello (s -> m t)nel tipo di (>>=)mostra che un valore in spuò determinare il comportamento di un calcolo in m t. Le monadi consentono l'interferenza tra il valore e gli strati di calcolo. L' (<*>)operatore non consente tale interferenza: i calcoli di funzioni e argomenti non dipendono dai valori. Questo morde davvero. Confrontare

miffy :: Monad m => m Bool -> m x -> m x -> m x
miffy mb mt mf = do
  b <- mb
  if b then mt else mf

che utilizza il risultato di un effetto per decidere tra due calcoli (ad esempio il lancio di missili e la firma di un armistizio), mentre

iffy :: Applicative a => a Bool -> a x -> a x -> a x
iffy ab at af = pure cond <*> ab <*> at <*> af where
  cond b t f = if b then t else f

che usa il valore di abper scegliere tra i valori di due calcoli ate af, avendoli effettuati entrambi, forse con effetto tragico.

La versione monadica si basa essenzialmente sul potere extra di (>>=)scegliere un calcolo da un valore, e questo può essere importante. Tuttavia, supportare quel potere rende le monadi difficili da comporre. Se proviamo a costruire un "doppio legame"

(>>>>==) :: (Monad m, Monad n) => m (n s) -> (s -> m (n t)) -> m (n t)
mns >>>>== f = mns >>-{-m-} \ ns -> let nmnt = ns >>= (return . f) in ???

siamo arrivati ​​fin qui, ma ora i nostri strati sono tutti confusi. Abbiamo un n (m (n t)), quindi dobbiamo sbarazzarci dell'esterno n. Come dice Alexandre C, possiamo farlo se ne abbiamo uno adatto

swap :: n (m t) -> m (n t)

permutare l' ninterno e joinquesto verso l'altro n.

La "doppia applicazione" più debole è molto più facile da definire

(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs

perché non c'è interferenza tra gli strati.

Di conseguenza, è bene riconoscere quando hai davvero bisogno della potenza extra di Monads e quando puoi farla franca con la rigida struttura di calcolo che Applicativesupporta.

Nota, a proposito, che sebbene comporre le monadi sia difficile, potrebbe essere più del necessario. Il tipo m (n v)indica il calcolo con m-effects, quindi il calcolo con -effects na v-value, dove gli m-effects terminano prima che ninizino gli -effects (da qui la necessità di swap). Se vuoi solo intercalare m-effetti con n-effetti, allora la composizione è forse troppo da chiedere!


3
Per l'esempio incerto si afferma che "usa il valore di ab per scegliere tra i valori di due calcoli at e af, dopo averli eseguiti entrambi, forse con effetti tragici". La natura pigra di Haskell non ti protegge da questo? Se ho list = (\ btf -> if b then t else f): [] e poi eseguo l'istruzione: list <*> pure True <*> pure "hello" <*> pure (errore "bad"). ... Ottengo "ciao" e l'errore non si verifica mai. Questo codice non è sicuro o controllato come una monade, ma il post sembra suggerire che gli applicativi richiedono una valutazione rigorosa. Nel complesso un ottimo post però! Grazie!
shj

7
Hai ancora gli effetti di entrambi, ma puro (errore "cattivo") non ne ha. Se, d'altra parte, provi iffy (pure True) (puro "ciao") (errore "cattivo"), ottieni un errore che miffy evita. Inoltre, se provi qualcosa come iffy (pure True) (pure 0) [1,2], otterrai [0,0] invece di [0]. Gli applicativi hanno una sorta di rigore su di loro, in quanto costruiscono sequenze fisse di calcoli, ma i valori risultanti da quei calcoli sono ancora combinati pigramente, come osservi.
Pigworker

È vero che per qualsiasi monade me npuoi sempre scrivere un trasformatore monade mte operare in n (m t)uso mt n t? Quindi puoi sempre comporre monadi, è solo più complicato, usando i trasformatori?
ron

4
Tali trasformatori esistono spesso, ma per quanto ne so, non esiste un modo canonico per generarli. C'è spesso una scelta genuina su come risolvere gli effetti interlacciati dalle diverse monadi, il classico esempio sono le eccezioni e lo stato. Un'eccezione dovrebbe annullare le modifiche di stato o no? Entrambe le scelte hanno il loro posto. Detto questo, c'è una cosa "monade libera" che esprime "interleaving arbitrario". data Free f x = Ret x | Do (f (Free f x)), quindi data (:+:) f g x = Inl (f x) | Tnr (g x), e considera Free (m :+: n). Ciò ritarda la scelta di come eseguire gli intrecci.
operaio suino

@pigworker Riguardo al dibattito pigro / rigoroso. Penso che con gli applicativi non sia possibile controllare l'effetto dall'interno del calcolo, ma lo strato effetto può benissimo decidere di non valutare i valori successivi. Per i parser (applicativi) ciò significa che se il parser fallisce in anticipo, i parser successivi non vengono valutati / applicati all'input. Per Maybequesto significa che un precoce Nothingsopprimerà la valutazione del adi un successivo / successivo Just a. È corretto?
ziggystar

75

Gli applicativi compongono, le monadi no.

Monadi fanno comporre, ma il risultato potrebbe non essere una monade. Al contrario, la composizione di due applicativi è necessariamente un applicativo. Sospetto che l'intenzione dell'affermazione originale fosse che "l'applicatività compone, mentre la monadità no". Riformulato, " Applicativeè chiuso in composizione, e Monadnon lo è".


24
Inoltre, due applicativi qualsiasi si compongono in modo completamente meccanico, mentre la monade formata dalla composizione di due monadi è specifica di quella composizione.
Apocalisp

12
Inoltre le monadi si compongono in altri modi, il prodotto di due monadi è una monade, sono solo i coprodotti che necessitano di qualche tipo di legge distributiva.
Edward KMETT

Con, @Apocalisp, commento incluso, questa è la risposta migliore e più concisa.
Paul Draper

39

Se hai applicativi A1e A2, allora il tipodata A3 a = A3 (A1 (A2 a)) è applicativo (puoi scrivere un'istanza del genere in modo generico).

D'altra parte, se si hanno le monadi M1e M2il tipo data M3 a = M3 (M1 (M2 a))non è necessariamente una monade (non esiste un'implementazione generica sensata per >>=o joinper la composizione).

Un esempio può essere il tipo [Int -> a](qui componiamo un costruttore di tipi []con (->) Int, entrambi monadi). Puoi scrivere facilmente

app :: [Int -> (a -> b)] -> [Int -> a] -> [Int -> b]
app f x = (<*>) <$> f <*> x

E questo generalizza a qualsiasi applicativo:

app :: (Applicative f, Applicative f1) => f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)

Ma non esiste una definizione sensata di

join :: [Int -> [Int -> a]] -> [Int -> a]

Se non sei convinto di questo, considera questa espressione:

join [\x -> replicate x (const ())]

La lunghezza della lista restituita deve essere impostata prima che venga fornito un numero intero, ma la sua lunghezza corretta dipende dal numero intero fornito. Pertanto, non joinpuò esistere alcuna funzione corretta per questo tipo.


1
... quindi evitare le monadi quando una funzione andrà bene?
andrew cooke

2
@andrew, se intendevi funtore, allora sì, i funtori sono più semplici e dovrebbero essere usati quando sufficienti. Nota che non è sempre. Ad esempio IOsenza un Monadsarebbe molto difficile da programmare. :)
Rotsor

17

Sfortunatamente, il nostro vero obiettivo, la composizione delle monadi, è piuttosto più difficile. .. In effetti, possiamo effettivamente dimostrare che, in un certo senso, non c'è modo di costruire una funzione di join con il tipo precedente usando solo le operazioni delle due monadi (vedi l'appendice per uno schema della dimostrazione). Ne consegue che l'unico modo in cui potremmo sperare di formare una composizione è se ci sono alcune costruzioni aggiuntive che collegano i due componenti.

Composizione di monadi, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf


4
Tl; dr per lettori impazienti: puoi comporre monadi se (f?) Puoi fornire una trasformazione naturaleswap : N M a -> M N a
Alexandre C.

@Alexandre C .: Solo "se", sospetto. Non tutti i trasformatori di monade sono descritti dalla composizione del funtore diretto. Ad esempio, ContT r m anon è né m (Cont r a)Cont r (m a), ed StateT s m aè approssimativamente Reader s (m (Writer s a)).
CA McCann

@CA McCann: Non riesco a passare da (M monade, N monade, MN monade, NM monade) a (esiste uno scambio: MN -> NM naturale). Quindi restiamo al "se" per ora (forse la risposta è sul giornale, devo confessare di averla cercata velocemente)
Alexandre C.

1
@Alexandre C .: Il solo fatto di specificare che le composizioni sono monadi potrebbe non essere comunque sufficiente - hai anche bisogno di un modo per mettere in relazione le due parti con il tutto. L'esistenza di swapimplica che la composizione permetta ai due di "cooperare" in qualche modo. Inoltre, nota che sequenceè un caso speciale di "scambio" per alcune monadi. Così è flip, in realtà.
CA McCann

7
Per scriverlo swap :: N (M x) -> M (N x)mi sembra che tu possa usare returns(opportunamente fmapped) per inserire una Mdavanti e una Ndietro, andando da N (M x) -> M (N (M (N x))), poi usa il joindel composito per ottenere la tua M (N x).
Pigworker

7

La soluzione di legge distributiva l: MN -> NM è sufficiente

per garantire la monadicità di NM. Per vederlo hai bisogno di un'unità e di un mult. Mi concentrerò sul mult (l'unità è unit_N unitM)

NMNM - l -> NNMM - mult_N mult_M -> NM

Ciò non garantisce che MN sia una monade.

L'osservazione cruciale, tuttavia, entra in gioco quando si hanno soluzioni di diritto distributivo

l1 : ML -> LM
l2 : NL -> LN
l3 : NM -> MN

quindi, LM, LN e MN sono monadi. Sorge la domanda se LMN sia una monade (o di

(MN) L -> L (MN) o da N (LM) -> (LM) N

Abbiamo una struttura sufficiente per creare queste mappe. Tuttavia, come osserva Eugenia Cheng , abbiamo bisogno di una condizione esagonale (che equivale a una presentazione dell'equazione di Yang-Baxter) per garantire la monadicità di entrambe le costruzioni. Infatti, con la condizione esagonale, le due diverse monadi coincidono.


9
Questa è probabilmente una grande risposta, ma è andato whoosh modo sopra la mia testa.
Dan Burton

1
Questo perché, usando il termine Applicativo e tag haskell, questa è una domanda su haskell ma con una risposta in una notazione diversa.
codeshot
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.