Perché gli effetti collaterali sono modellati come monadi in Haskell?


172

Qualcuno potrebbe dare alcuni suggerimenti sul perché i calcoli impuri in Haskell sono modellati come monadi?

Voglio dire, la monade è solo un'interfaccia con 4 operazioni, quindi qual era il ragionamento per modellare gli effetti collaterali in essa?


15
Le monadi definiscono solo due operazioni.
Dario,

3
ma per quanto riguarda il ritorno e il fallimento? (oltre a (>>) e (>> =))
bodacydo

55
Le due operazioni sono returne (>>=). x >> yè uguale a x >>= \\_ -> y(ovvero ignora il risultato del primo argomento). Non ne parliamo fail.
porges

2
@Porges Perché non parlare del fallimento? È in qualche modo utile, ad esempio forse, Parser, ecc.
alternativa il

16
@monadic: failè in Monadclasse a causa di un incidente storico; appartiene davvero MonadPlus. Si noti che la definizione predefinita non è sicura.
JB.

Risposte:


292

Supponiamo che una funzione abbia effetti collaterali. Se prendiamo tutti gli effetti che produce come parametri di input e output, allora la funzione è pura per il mondo esterno.

Quindi, per una funzione impura

f' :: Int -> Int

aggiungiamo il RealWorld alla considerazione

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

allora fè di nuovo puro. Definiamo un tipo di dati parametrizzato type IO a = RealWorld -> (a, RealWorld), quindi non è necessario digitare RealWorld così tante volte e possiamo semplicemente scrivere

f :: Int -> IO Int

Per il programmatore, gestire un RealWorld direttamente è troppo pericoloso, in particolare se un programmatore mette le mani su un valore di tipo RealWorld, potrebbe provare a copiarlo , il che è sostanzialmente impossibile. (Pensa di provare a copiare l'intero filesystem, per esempio. Dove lo metteresti?) Pertanto, la nostra definizione di IO incapsula anche gli stati di tutto il mondo.

Composizione di funzioni "impure"

Queste funzioni impure sono inutili se non possiamo collegarle insieme. Tener conto di

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

Noi vogliamo

  • ottenere un nome file dalla console,
  • leggere quel file e
  • stampa il contenuto di quel file sulla console.

Come lo faremmo se potessimo accedere agli stati del mondo reale?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Vediamo uno schema qui. Le funzioni si chiamano così:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Quindi potremmo definire un operatore ~~~per vincolarli:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

allora potremmo semplicemente scrivere

printFile = getLine ~~~ getContents ~~~ putStrLn

senza toccare il mondo reale.

"Impurification"

Supponiamo ora di voler rendere maiuscolo anche il contenuto del file. L'upgrade è una funzione pura

upperCase :: String -> String

Ma per farlo nel mondo reale, deve restituire un IO String. È facile sollevare una tale funzione:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Questo può essere generalizzato:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

così impureUpperCase = impurify . upperCase, e possiamo scrivere

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Nota: normalmente scriviamo getLine ~~~ getContents ~~~ (putStrLn . upperCase))

Abbiamo sempre lavorato con le monadi

Ora vediamo cosa abbiamo fatto:

  1. Abbiamo definito un operatore (~~~) :: IO b -> (b -> IO c) -> IO cche mette insieme due funzioni impure
  2. Abbiamo definito una funzione impurify :: a -> IO ache converte un valore puro in impuro.

Ora facciamo l'identificazione (>>=) = (~~~)e return = impurify, e vediamo? Abbiamo una monade.


Nota tecnica

Per assicurarsi che sia davvero una monade, ci sono ancora alcuni assiomi che devono essere controllati anche:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    Lasciato come esercizio.


5
+1 ma voglio notare che questo riguarda in modo specifico il caso IO. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html è abbastanza simile, ma si generalizza RealWorldin ... beh, vedrai.
effimero

4
Nota che questa spiegazione non può davvero applicarsi a quella di Haskell IO, perché quest'ultima supporta l'interazione, la concorrenza e il non determinismo. Vedi la mia risposta a questa domanda per qualche altro suggerimento.
Conal,

2
@Conal GHC in realtà implementa in IOquesto modo, ma in RealWorldrealtà non rappresenta il mondo reale, è solo un segno per mantenere le operazioni in ordine (la "magia" è che RealWorldè l'unico tipo di unicità di GHC Haskell)
Jeremy List

2
@JeremyList A quanto ho capito, GHC implementa IOtramite una combinazione di questa rappresentazione e la magia del compilatore non standard (che ricorda il famoso virus del compilatore C di Ken Thompson ). Per altri tipi, la verità è nel codice sorgente insieme alla solita semantica di Haskell.
Conal,

1
@Clonal Il mio commento è dovuto al fatto che ho letto le parti pertinenti del codice sorgente GHC.
Elenco Jeremy,

43

Qualcuno potrebbe dare alcuni suggerimenti sul perché i calcoli non puri in Haskell sono modellati come monadi?

Questa domanda contiene un malinteso diffuso. Impurità e Monade sono nozioni indipendenti. L'impurità non è modellata da Monad. Piuttosto, ci sono alcuni tipi di dati, come ad esempio IO, che rappresentano il calcolo imperativo. E per alcuni di questi tipi, una piccola parte della loro interfaccia corrisponde al modello di interfaccia chiamato "Monade". Inoltre, non esiste alcuna spiegazione pura / funzionale / denotativa nota di IO(e è improbabile che sia una, considerando lo scopo di "sin bin"IO ), sebbene vi sia la storia comunemente raccontata World -> (a, World)sull'essere il significato di IO a. Quella storia non può descrivere sinceramente IO, perchéIOsupporta la concorrenza e il non determinismo. La storia non funziona nemmeno quando si tratta di calcoli deterministici che consentono l'interazione di medio calcolo con il mondo.

Per ulteriori spiegazioni, vedi questa risposta .

Modifica : rileggendo la domanda, non credo che la mia risposta sia abbastanza corretta. I modelli di calcolo imperativo risultano spesso essere monadi, proprio come diceva la domanda. Il richiedente potrebbe davvero non presumere che la monadness in alcun modo permetta la modellizzazione del calcolo imperativo.


1
@KennyTM: Ma RealWorldè, come dicono i documenti, "profondamente magico". È un token che rappresenta ciò che sta facendo il sistema di runtime, in realtà non significa nulla del mondo reale. Non puoi nemmeno evocarne uno nuovo per creare un "thread" senza fare inganno in più; l'approccio ingenuo creerebbe semplicemente un'unica azione bloccante con molta ambiguità su quando verrà eseguita.
CA McCann,

4
Inoltre, direi che le monadi sono essenzialmente di natura imperativa. Se il functor rappresenta una struttura con valori incorporati, un'istanza monade significa che puoi costruire e appiattire nuovi livelli in base a tali valori. Quindi, qualunque sia il significato che assegni a un singolo livello del funzione, una monade significa che puoi creare un numero illimitato di livelli con una nozione rigorosa di causalità che va da uno all'altro. I casi specifici potrebbero non avere una struttura intrinsecamente imperativa, ma Monadin generale lo è davvero.
CA McCann,

3
Con " Monadin generale" intendo approssimativamente forall m. Monad m => ..., cioè, lavorare su un'istanza arbitraria. Le cose che puoi fare con una monade arbitraria sono quasi esattamente le stesse cose che puoi fare con IO: ricevere primitive opache (come argomenti di funzione, o da librerie, rispettivamente), costruire no-ops returno trasformare un valore in modo irreversibile usando (>>=). L'essenza della programmazione in una monade arbitraria sta generando un elenco di azioni irrevocabili: "fai X, poi fai Y, poi ...". Mi sembra abbastanza imperativo!
CA McCann,

2
No, ti manca ancora il mio punto qui. Ovviamente non useresti quella mentalità per nessuno di quei tipi specifici, perché hanno una struttura chiara e significativa. Quando dico "monadi arbitrarie" intendo "non puoi scegliere quale"; la prospettiva qui è dall'interno del quantificatore, quindi pensare mcome esistenziale potrebbe essere più utile. Inoltre, la mia "interpretazione" è una riformulazione delle leggi; l'elenco delle istruzioni "do X" è precisamente il monoide libero sulla struttura sconosciuta creata tramite (>>=); e le leggi della monade sono solo leggi monoide sulla composizione dell'endofunctor.
CA McCann,

3
In breve, il limite inferiore più grande di ciò che tutte le monadi descrivono insieme è una marcia cieca e insignificante verso il futuro. IOè un caso patologico proprio perché non offre quasi nulla di più di questo minimo. In casi specifici, i tipi possono rivelare più struttura e quindi avere un significato effettivo; ma per il resto le proprietà essenziali di una monade - basate sulle leggi - sono altrettanto antitetiche per cancellare la denotazione come lo IOsono. Senza esportare costruttori, elencare esaustivamente azioni primitive o qualcosa di simile, la situazione è senza speranza.
CA McCann,

13

A quanto ho capito, qualcuno chiamato Eugenio Moggi notò per la prima volta che un costrutto matematico precedentemente oscuro chiamato "monade" poteva essere usato per modellare gli effetti collaterali nei linguaggi informatici, e quindi specificare la loro semantica usando il calcolo Lambda. Quando Haskell era in fase di sviluppo, c'erano vari modi in cui venivano modellati i calcoli impuri ( per maggiori dettagli, vedi il documento "Camicia per capelli" di Simon Peyton Jones ), ma quando Phil Wadler introdusse le monadi divenne rapidamente ovvio che questa era la risposta. E il resto è storia.


3
Non proprio. È noto che una monade può modellare l'interpretazione per molto tempo (almeno da "Topoi: un'analisi categorica della logica). D'altra parte, non è stato possibile esprimere chiaramente i tipi di monadi fino a quando non è stato tipizzato in modo funzionale le lingue arrivarono e poi Moggi ne mise due e due insieme.
nomen

1
Forse le monadi potrebbero essere più facili da capire se fossero definite in termini di involucro della mappa e di scartare, con il ritorno che è sinonimo di involucro.
aoeu256,

9

Qualcuno potrebbe dare alcuni suggerimenti sul perché i calcoli non puri in Haskell sono modellati come monadi?

Bene, perché Haskell è puro . Avete bisogno di un concetto matematico di distinguere tra calcoli unpure e puri sul tipo di livello e al modello di programma scorre rispettivamente.

Ciò significa che dovrai finire con un tipo IO ache modella un calcolo non puro. Quindi devi sapere come combinare questi calcoli che si applicano in sequenza ( >>=) e sollevare un valore ( return) sono i più ovvi e basilari.

Con questi due hai già definito una monade (senza nemmeno pensarci);)

Inoltre, le monadi forniscono astrazioni molto generali e potenti , quindi molti tipi di flusso di controllo possono essere convenientemente generalizzati in funzioni monadiche come sequence, liftMo sintassi speciali, rendendo l'irregolarità non un caso così speciale.

Vedi monadi nella programmazione funzionale e nella tipizzazione di unicità (l'unica alternativa che conosco) per ulteriori informazioni.


6

Come dici tu, Monadè una struttura molto semplice. La metà della risposta è: Monadè la struttura più semplice che potremmo eventualmente dare alle funzioni con effetti collaterali ed essere in grado di usarle. Con Monadpossiamo fare due cose: possiamo considerare un valore puro come un valore di effetto collaterale ( return) e possiamo applicare una funzione di effetto collaterale a un valore di effetto collaterale per ottenere un nuovo valore di effetto collaterale ( >>=). Perdere la capacità di fare una di queste cose sarebbe paralizzante, quindi il nostro tipo di effetto collaterale deve essere "almeno" Monad, e risulta Monadsufficiente per implementare tutto ciò di cui abbiamo bisogno finora.

L'altra metà è: qual è la struttura più dettagliata che potremmo dare ai "possibili effetti collaterali"? Possiamo certamente pensare allo spazio di tutti i possibili effetti collaterali come un insieme (l'unica operazione che richiede è l'appartenenza). Possiamo combinare due effetti collaterali facendoli uno dopo l'altro, e questo darà origine a un effetto collaterale diverso (o forse lo stesso - se il primo era "computer spento" e il secondo era "scrivi file", quindi il risultato di comporre questi è solo "spegnimento del computer").

Ok, quindi cosa possiamo dire di questa operazione? È associativo; vale a dire, se combiniamo tre effetti collaterali, non importa in quale ordine eseguiamo la combinazione. Se lo facciamo (scriviamo il file quindi leggi il socket) quindi spegniamo il computer, è come fare il file di scrittura allora (leggi il socket quindi chiudi l'arresto computer). Ma non è commutativo: ("scrivi file" quindi "elimina file") è un effetto collaterale diverso da ("elimina file" quindi "scrivi file"). E abbiamo un'identità: lo speciale effetto collaterale "nessun effetto collaterale" funziona ("nessun effetto collaterale" quindi "elimina file" è lo stesso effetto collaterale del semplice "elimina file"). A questo punto qualsiasi matematico sta pensando a "Gruppo!" Ma i gruppi hanno inversioni e non c'è modo di invertire un effetto collaterale in generale; "cancella il file" è irreversibile. Quindi la struttura che ci rimane è quella di un monoide, il che significa che le nostre funzioni di effetto collaterale dovrebbero essere monadi.

C'è una struttura più complessa? Sicuro! Potremmo dividere i possibili effetti collaterali in effetti basati su filesystem, effetti basati su rete e altro, e potremmo inventare regole di composizione più elaborate che conservino questi dettagli. Ma ancora una volta si riduce a: Monadè molto semplice, eppure abbastanza potente da esprimere la maggior parte delle proprietà a cui teniamo. (In particolare, l'associatività e gli altri assiomi ci permettono di testare la nostra applicazione in piccoli pezzi, con la certezza che gli effetti collaterali dell'applicazione combinata saranno gli stessi della combinazione degli effetti collaterali dei pezzi).


4

In realtà è un modo abbastanza pulito di pensare all'I / O in modo funzionale.

Nella maggior parte dei linguaggi di programmazione, si eseguono operazioni di input / output. In Haskell, immagina di scrivere il codice non per fare le operazioni, ma per generare un elenco delle operazioni che vorresti fare.

Le monadi sono semplicemente una sintassi abbastanza per questo.

Se vuoi sapere perché le monadi invece di qualcos'altro, suppongo che la risposta sia che sono il miglior modo funzionale di rappresentare l'I / O a cui le persone potrebbero pensare quando stavano realizzando Haskell.


3

AFAIK, il motivo è di poter includere controlli degli effetti collaterali nel sistema dei tipi. Se vuoi saperne di più, ascolta quegli episodi SE-Radio : Episodio 108: Simon Peyton Jones su Programmazione funzionale e Haskell Episodio 72: Erik Meijer su LINQ


2

Sopra ci sono ottime risposte dettagliate con background teorico. Ma voglio dare la mia opinione su IO monade. Non ho esperienza con il programmatore haskell, quindi può essere abbastanza ingenuo o addirittura sbagliato. Ma mi ha aiutato a gestire la monade IO in una certa misura (nota che non si riferisce ad altre monadi).

Innanzitutto voglio dire che quell'esempio con "mondo reale" non è troppo chiaro per me in quanto non possiamo accedere ai suoi stati (del mondo reale) precedenti. Può essere che non si riferisca affatto ai calcoli della monade, ma è desiderato nel senso di trasparenza referenziale, che è generalmente presente nel codice haskell.

Quindi vogliamo che la nostra lingua (haskell) sia pura. Ma abbiamo bisogno di operazioni di input / output in quanto senza di esse il nostro programma non può essere utile. E quelle operazioni non possono essere pure per loro natura. Quindi l'unico modo per affrontarlo dobbiamo separare le operazioni impure dal resto del codice.

Qui arriva la monade. In realtà, non sono sicuro che non possano esistere altri costrutti con proprietà necessarie simili, ma il punto è che la monade ha queste proprietà, quindi può essere usata (e usata con successo). La proprietà principale è che non possiamo evitarlo. L'interfaccia della monade non ha operazioni per sbarazzarsi della monade attorno al nostro valore. Altre monadi (non IO) forniscono tali operazioni e consentono la corrispondenza dei modelli (ad es. Forse), ma tali operazioni non sono nell'interfaccia monade. Un'altra proprietà richiesta è la capacità di concatenare le operazioni.

Se pensiamo a ciò di cui abbiamo bisogno in termini di sistema dei tipi, arriviamo al fatto che abbiamo bisogno del tipo con il costruttore, che può essere avvolto attorno a qualsiasi valore. Il costruttore deve essere privato, in quanto vietiamo la sua fuga (ovvero la corrispondenza dei modelli). Ma abbiamo bisogno della funzione per mettere valore in questo costruttore (qui viene in mente il ritorno). E abbiamo bisogno del modo di concatenare le operazioni. Se ci pensiamo per un po 'di tempo, arriveremo al fatto che l'operazione di concatenamento deve avere il tipo come >> = has. Quindi, arriviamo a qualcosa di molto simile alla monade. Penso che se ora analizziamo possibili situazioni contraddittorie con questo costrutto, arriveremo agli assiomi delle monadi.

Nota che quel costrutto sviluppato non ha nulla in comune con l'impurità. Ha solo proprietà, che avremmo voluto essere in grado di affrontare operazioni impure, vale a dire, non fuggire, concatenare e un modo per entrare.

Ora una serie di operazioni impure è predefinita dalla lingua all'interno di questa IO monade selezionata. Siamo in grado di combinare tali operazioni per creare nuove operazioni impure. E tutte quelle operazioni dovranno avere IO nel loro tipo. Si noti tuttavia che la presenza di IO nel tipo di alcune funzioni non rende impura questa funzione. Ma a quanto ho capito, è una cattiva idea scrivere funzioni pure con IO nel loro tipo, poiché inizialmente era nostra idea separare funzioni pure e impure.

Infine, voglio dire che la monade non trasforma le operazioni impure in azioni pure. Permette solo di separarli efficacemente. (Ripeto, è solo mia comprensione)


1
Ti aiutano a digitare il controllo del tuo programma permettendoti di digitare gli effetti di controllo e puoi definire i tuoi DSL creando monadi per limitare gli effetti che le tue funzioni possono fare in modo che il compilatore possa controllare i tuoi errori di sequenziamento.
aoeu256,

Questo commento di aoeu256 è il "perché" che manca in tutte le spiegazioni fornite finora. (vale a dire: le monadi non sono per gli umani, ma per i compilatori)
João Otero
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.