Perché abbiamo bisogno delle monadi?


366

A mio modesto parere, le risposte alla famosa domanda "Cos'è una monade?" , specialmente quelli più votati, cercano di spiegare cos'è una monade senza spiegare chiaramente perché le monadi siano davvero necessarie . Possono essere spiegati come la soluzione di un problema?




4
Che ricerca hai già fatto? Dove hai guardato? Quali risorse hai trovato? Ci aspettiamo che tu faccia una notevole quantità di ricerche prima di fare una domanda e ci mostri nella domanda quale ricerca hai fatto . Esistono molte risorse che tentano di spiegare la motivazione delle risorse: se non ne hai trovato alcuna, potresti dover fare qualche ricerca in più. Se ne hai trovati alcuni ma non ti hanno aiutato, sarebbe una domanda migliore se spiegassi cosa hai trovato e perché nello specifico non hanno funzionato per te.
DW,

8
Questa è sicuramente una soluzione migliore per Programmers.StackExchange e non adatta per StackOverflow. Vorrei votare per migrare se potessi, ma non posso. = (
jpmc26,

3
@ jpmc26 Molto probabilmente verrebbe chiuso lì come "principalmente basato sull'opinione"; qui almeno esiste una possibilità (come dimostrato dall'enorme numero di voti, riapertura rapida ieri e ancora nessun voto ravvicinato)
Izkata,

Risposte:


580

Perché abbiamo bisogno delle monadi?

  1. Vogliamo programmare solo usando le funzioni . ("programmazione funzionale (FP)" dopo tutto).
  2. Quindi, abbiamo un primo grosso problema. Questo è un programma:

    f(x) = 2 * x

    g(x,y) = x / y

    Come possiamo dire cosa deve essere eseguito per primo ? Come possiamo formare una sequenza ordinata di funzioni (ad es. Un programma ) usando nient'altro che funzioni ?

    Soluzione: comporre funzioni . Se vuoi prima ge poi f, basta scrivere f(g(x,y)). In questo modo, "il programma" è una funzione così: main = f(g(x,y)). Ok ma ...

  3. Altri problemi: alcune funzioni potrebbero non funzionare (ovvero g(2,0)dividere per 0). Non abbiamo "eccezioni" in FP (un'eccezione non è una funzione). Come lo risolviamo?

    Soluzione: consentiamo alle funzioni di restituire due tipi di cose : invece di avere g : Real,Real -> Real(funzione da due reali in un reale), consentiamo g : Real,Real -> Real | Nothing(funzione da due reali in (reale o nulla)).

  4. Ma le funzioni dovrebbero (per essere più semplici) restituire solo una cosa .

    Soluzione: creiamo un nuovo tipo di dati da restituire, un " tipo di boxe " che racchiuda forse un reale o semplicemente nulla. Quindi, possiamo avere g : Real,Real -> Maybe Real. Ok ma ...

  5. Cosa succede adesso f(g(x,y))? fnon è pronto a consumare a Maybe Real. E, non vogliamo cambiare ogni funzione con cui potremmo connetterci gper consumare a Maybe Real.

    Soluzione: disponiamo di una funzione speciale per "connettere" / "comporre" / "link" funzioni . In questo modo, dietro le quinte, possiamo adattare l'output di una funzione per alimentare quella successiva.

    Nel nostro caso: g >>= f(connettiti / componi ga f). Vogliamo >>=ottenere gl'output, ispezionarlo e, nel caso sia Nothingsolo non chiamare fe restituire Nothing; o al contrario, estrarre la scatola Reale nutrirla fcon essa. (Questo algoritmo è solo l'implementazione di >>=per il Maybetipo). Si noti inoltre che >>=deve essere scritto una sola volta per "tipo di boxe" (casella diversa, algoritmo di adattamento diverso).

  6. Sorgono molti altri problemi che possono essere risolti utilizzando questo stesso modello: 1. Utilizzare una "casella" per codificare / memorizzare diversi significati / valori e avere funzioni simili grestituiscono quei "valori in scatola". 2. Avere un compositore / linker g >>= fper aiutare a collegare gl'output fall'input, quindi non è necessario modificarne faffatto.

  7. I problemi notevoli che possono essere risolti usando questa tecnica sono:

    • avendo uno stato globale che ogni funzione nella sequenza di funzioni ("il programma") può condividere: soluzione StateMonad.

    • Non ci piacciono le "funzioni impure": funzioni che producono output diversi per lo stesso input. Pertanto, contrassegniamo quelle funzioni, facendole restituire un valore con tag / box: IOmonade.

Felicità totale!


64
@Carl Per favore, scrivi una risposta migliore per illuminarci
XrXr

15
@Carl Penso che sia chiaro nella risposta che ci sono molti molti problemi che beneficiano di questo modello (punto 6) e che la IOmonade è solo un altro problema nell'elenco IO(punto 7). D'altra parte IOappare solo una volta e alla fine, quindi, non capisco il tuo "gran parte del suo tempo parlando ... di IO".
cibercitizen1,

4
Le grandi idee sbagliate sulle monadi: monadi sullo stato; monadi sulla gestione delle eccezioni; non c'è modo di implementare IO in puro FPL senza monadi; le monadi sono inequivocabili (il contrargument è Either). La maggior parte della risposta riguarda "Perché abbiamo bisogno di agenti?".
vlastachu,

4
"6. 2. Avere un compositore / linker g >>= fper aiutare a connettere gl'output fall'input, quindi non dobbiamo cambiarne faffatto." questo non è affatto giusto . Prima, dentro f(g(x,y)), fpoteva produrre qualsiasi cosa. Potrebbe essere f:: Real -> String. Con "composizione monadica" deve essere cambiato per produrreMaybe String , altrimenti i tipi non si adatteranno. Inoltre, >>=in sé non va bene !! È >=>questa la composizione, no >>=. Vedi la discussione con Dfeuer sotto la risposta di Carl.
Will Ness,

3
La tua risposta è giusta nel senso che le monadi IMO in effetti sono meglio descritte come relative alla composizione / alità delle "funzioni" (frecce di Kleisli in realtà), ma i dettagli precisi di quale tipo va dove sono ciò che le rende "monadi". potresti cablare le scatole in tutti i modi (come Functor, ecc.). Questo modo specifico di collegarli insieme è ciò che definisce "la monade".
Will Ness,

219

La risposta è, ovviamente, "Noi no" . Come per tutte le astrazioni, non è necessario.

Haskell non ha bisogno di un'astrazione di monade. Non è necessario per eseguire IO in un linguaggio puro. Il IOtipo se ne occupa da solo. Il Dezuccheraggio monadic esistente di doblocchi può essere sostituito con Dezuccheraggio a bindIO, returnIOe failIOcome definito nel GHC.Basemodulo. (Non è un modulo documentato sull'hackage, quindi dovrò puntare alla sua fonte per la documentazione.) Quindi no, non c'è bisogno dell'astrazione della monade.

Quindi se non è necessario, perché esiste? Perché è stato scoperto che molti schemi di calcolo formano strutture monadiche. L'astrazione di una struttura consente di scrivere codice che funziona su tutte le istanze di quella struttura. Per dirla in modo più conciso: riutilizzo del codice.

Nei linguaggi funzionali, lo strumento più potente trovato per il riutilizzo del codice è stata la composizione delle funzioni. Il buon vecchio (.) :: (b -> c) -> (a -> b) -> (a -> c)operatore è estremamente potente. Rende facile scrivere minuscole funzioni e incollarle insieme con un minimo sovraccarico sintattico o semantico.

Ma ci sono casi in cui i tipi non funzionano abbastanza bene. Cosa fai quando hai foo :: (b -> Maybe c)e bar :: (a -> Maybe b)? foo . barnon seleziona il controllo, perché be Maybe bnon sono dello stesso tipo.

Ma ... è quasi giusto. Vuoi solo un po 'di margine di manovra. Vuoi essere in grado di trattare Maybe bcome se fosse fondamentalmente b. Tuttavia, è una cattiva idea trattarli allo stesso modo. È più o meno la stessa cosa dei puntatori null, che Tony Hoare ha definito l'errore di miliardi di dollari . Quindi se non riesci a trattarli come lo stesso tipo, forse puoi trovare un modo per estendere il meccanismo di composizione (.)fornito.

In tal caso, è importante esaminare davvero la teoria alla base (.). Fortunatamente, qualcuno ha già fatto questo per noi. Si scopre che la combinazione di (.)e idforma un costrutto matematico noto come categoria . Ma ci sono altri modi per formare categorie. Una categoria Kleisli, ad esempio, consente di aumentare leggermente gli oggetti composti. Una categoria Kleisli perMaybe consisterebbe in (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)e id :: a -> Maybe a. Cioè, gli oggetti nella categoria aumentano (->)con a Maybe, così (a -> b)diventa (a -> Maybe b).

E improvvisamente, abbiamo esteso il potere della composizione a cose su cui l' (.)operazione tradizionale non funziona. Questa è una fonte di nuovo potere di astrazione. Le categorie Kleisli funzionano con più tipi di semplici Maybe. Funzionano con ogni tipo in grado di assemblare una categoria adeguata, obbedendo alle leggi di categoria.

  1. Identità sinistra: id . f=f
  2. Identità corretta: f . id=f
  3. Associatività: f . (g . h)=(f . g) . h

Finché puoi dimostrare che il tuo tipo obbedisce a queste tre leggi, puoi trasformarlo in una categoria Kleisli. E qual è il grosso problema? Bene, si scopre che le monadi sono esattamente la stessa cosa delle categorie Kleisli. MonadE ' returnlo stesso di Kleisli id. Monad's (>>=)non è identico a Kleisli(.) , ma si rivela molto facile scrivere ciascuno in funzione dell'altra. E le leggi di categoria sono le stesse delle leggi della monade, quando le traduci attraverso la differenza tra (>>=)e (.).

Quindi perché passare tutto questo fastidio? Perché avere unMonad un'astrazione nella lingua? Come ho accennato in precedenza, consente il riutilizzo del codice. Consente persino il riutilizzo del codice lungo due diverse dimensioni.

La prima dimensione del riutilizzo del codice deriva direttamente dalla presenza dell'astrazione. Puoi scrivere codice che funzioni su tutte le istanze dell'astrazione. C'è l'intero pacchetto monad-loops composto da loop che funzionano con qualsiasi istanza di Monad.

La seconda dimensione è indiretta, ma deriva dall'esistenza della composizione. Quando la composizione è semplice, è naturale scrivere codice in blocchi piccoli e riutilizzabili. Questo è lo stesso modo in cui l' (.)operatore per le funzioni incoraggia a scrivere piccole funzioni riutilizzabili.

Allora perché esiste l'astrazione? Perché ha dimostrato di essere uno strumento che consente una maggiore composizione nel codice, risultando nella creazione di codice riutilizzabile e incoraggiando la creazione di codice più riutilizzabile. Il riutilizzo del codice è uno dei santi graal della programmazione. L'astrazione della monade esiste perché ci sposta un po 'verso quel santo graal.


2
Puoi spiegare la relazione tra le categorie in generale e le categorie Kleisli? Le tre leggi che descrivi sono valide in qualsiasi categoria.
Dfeuer,

1
@dfeuer Oh. Per dirla in codice, newtype Kleisli m a b = Kleisli (a -> m b). Le categorie di Kleisli sono funzioni in cui il tipo di ritorno categoriale ( bin questo caso) è l'argomento di un costruttore di tipi m. Iff Kleisli mforma una categoria, mè una Monade.
Carl

1
Che cos'è esattamente un tipo di ritorno categoriale? Kleisli msembra formare una categoria i cui oggetti sono tipi di Haskell e tali che le frecce da aa bsono le funzioni da aa m b, con id = returne (.) = (<=<). È giusto o sto mescolando diversi livelli di cose o qualcosa?
Dfeuer,

1
@dfeuer È corretto. Gli oggetti sono di tutti i tipi e i morfismi sono tra i tipi ae b, ma non sono semplici funzioni. Sono decorate con un extra mnel valore di ritorno della funzione.
Carl,

1
La terminologia della teoria delle categorie è davvero necessaria? Forse, Haskell sarebbe più facile se trasformassi i tipi in immagini in cui il tipo sarebbe il DNA per come sono disegnate le foto (tipi dipendenti però *), e quindi usi l'immagine per scrivere il tuo programma con i nomi piccoli caratteri rubini sopra l'icona.
aoeu256

24

Benjamin Pierce ha detto in TAPL

Un sistema di tipi può essere considerato come il calcolo di una sorta di approssimazione statica ai comportamenti runtime dei termini in un programma.

Ecco perché un linguaggio dotato di un potente sistema di tipi è strettamente più espressivo di un linguaggio mal digitato. Puoi pensare alle monadi allo stesso modo.

Come @Carl e sigfpe point, puoi equipaggiare un tipo di dati con tutte le operazioni che desideri senza ricorrere a monadi, macchine da scrivere o qualsiasi altra cosa astratta. Tuttavia, le monadi consentono non solo di scrivere codice riutilizzabile, ma anche di sottrarre tutti i dettagli ridondanti.

Ad esempio, supponiamo di voler filtrare un elenco. Il modo più semplice è usare la filterfunzione filter (> 3) [1..10]:, che è uguale [4,5,6,7,8,9,10].

Una versione leggermente più complicata di filter, che passa anche un accumulatore da sinistra a destra, è

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]

Per ottenere tutto i, in modo tale i <= 10, sum [1..i] > 4, sum [1..i] < 25, possiamo scrivere

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

che è uguale [3,4,5,6].

Oppure possiamo ridefinire la nubfunzione, che rimuove elementi duplicati da un elenco, in termini di filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4]uguale [1,2,4,5,3,8,9]. Qui viene passato un elenco come accumulatore. Il codice funziona, perché è possibile lasciare la monade elenco, quindi l'intero calcolo rimane puro ( notElemnon utilizza >>=effettivamente, ma potrebbe). Tuttavia, non è possibile lasciare in sicurezza la monade IO (cioè non è possibile eseguire un'azione IO e restituire un valore puro: il valore verrà sempre racchiuso nella monade IO). Un altro esempio sono le matrici mutabili: dopo aver lasciato la monade ST, dove vive una matrice mutabile, non è più possibile aggiornare la matrice in tempo costante. Quindi abbiamo bisogno di un filtro monadico dal Control.Monadmodulo:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterMesegue un'azione monadica per tutti gli elementi da un elenco, producendo elementi per i quali ritorna l'azione monadica True.

Un esempio di filtro con un array:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

stampa [1,2,4,5,3,8,9]come previsto.

E una versione con la monade IO, che chiede quali elementi restituire:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Per esempio

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

E come illustrazione finale, filterAccumpuò essere definita in termini di filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

con la StateTmonade, che viene utilizzata sotto il cofano, essendo solo un normale tipo di dati.

Questo esempio mostra che le monadi non solo ti permettono di astrarre il contesto computazionale e scrivere codice riutilizzabile pulito (a causa della componibilità delle monadi, come spiega @Carl), ma anche di trattare i tipi di dati definiti dall'utente e le primitive integrate in modo uniforme.


1
Questa risposta spiega perché abbiamo bisogno della tabella dei tipi Monad. Il modo migliore per capire, perché abbiamo bisogno delle monadi e non di qualcos'altro, è leggere la differenza tra monadi e funzioni applicative: una , due .
user3237465,

20

Non penso che IOdebba essere vista come una monade particolarmente eccezionale, ma è sicuramente una delle più sorprendenti per i principianti, quindi la userò per la mia spiegazione.

Costruire ingenuamente un sistema IO per Haskell

Il più semplice sistema IO concepibile per un linguaggio puramente funzionale (e in effetti quello con cui Haskell ha iniziato) è questo:

main :: String -> String
main _ = "Hello World"

Con la pigrizia, quella semplice firma è sufficiente per costruire effettivamente programmi terminali interattivi , anche se molto limitati. Il più frustrante è che possiamo solo produrre testo. E se aggiungessimo alcune possibilità di output più interessanti?

data Output = TxtOutput String
            | Beep Frequency

main :: String -> [Output]
main _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

carino, ma ovviamente un "output alterativo" molto più realistico sarebbe scrivere su un file . Ma poi vorresti anche un modo per leggere dai file. Qualche chance?

Bene, quando prendiamo il nostro main₁programma e semplicemente reindirizziamo un file al processo (usando le strutture del sistema operativo), abbiamo essenzialmente implementato la lettura dei file. Se potessimo attivare la lettura di quel file all'interno della lingua Haskell ...

readFile :: Filepath -> (String -> [Output]) -> [Output]

Questo userebbe un "programma interattivo" String->[Output], gli darebbe una stringa ottenuta da un file e produrrebbe un programma non interattivo che esegue semplicemente quello dato.

C'è un problema qui: non abbiamo davvero idea di quando il file viene letto. L' [Output]elenco dà sicuramente un buon ordine agli output , ma non abbiamo un ordine per quando gli input saranno fatti.

Soluzione: fare input-eventi anche elementi nell'elenco delle cose da fare.

data IO = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main :: String -> [IO₀]
main _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

Ok, ora puoi individuare uno squilibrio: puoi leggere un file e rendere l'output dipendente da esso, ma non puoi usare il contenuto del file per decidere, ad esempio, di leggere anche un altro file. Soluzione ovvia: rendere il risultato degli eventi di input anche qualcosa di tipo IO, non solo Output. Questo include sicuramente un semplice output di testo, ma consente anche di leggere file aggiuntivi ecc.

data IO = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main :: String -> [IO₁]
main _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

Ciò ora ti permetterebbe di esprimere qualsiasi operazione sui file che potresti desiderare in un programma (anche se forse non con buone prestazioni), ma è un po 'complicata:

  • main₃produce un intero elenco di azioni. Perché non usiamo semplicemente la firma :: IO₁, che ha questo come caso speciale?

  • Le liste non offrono più una panoramica affidabile del flusso del programma: la maggior parte dei calcoli successivi saranno “annunciati” solo come risultato di alcune operazioni di input. Quindi potremmo anche abbandonare la struttura dell'elenco e semplicemente contro un "e poi fare" per ogni operazione di output.

data IO = TxtOut String IO
         | TxtIn (String -> IO₂)
         | Terminate

main :: IO
main = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

Non male!

Quindi cosa c'entra tutto questo con le monadi?

In pratica, non vorrai usare semplici costruttori per definire tutti i tuoi programmi. Dovrebbero esserci un buon paio di costruttori così fondamentali, ma per la maggior parte delle cose di livello superiore vorremmo scrivere una funzione con una bella firma di alto livello. Si scopre che la maggior parte di questi sarebbe abbastanza simile: accettare una sorta di valore digitato in modo significativo e produrre un'azione I / O come risultato.

getTime :: (UTCTime -> IO₂) -> IO
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO

C'è evidentemente uno schema qui, e è meglio scriverlo come

type IO a = (a -> IO₂) -> IO    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO UTCTime
randomRIO :: Random r => (r,r) -> IO r
findFile :: RegEx -> IO (Maybe FilePath)

Ora questo inizia a sembrare familiare, ma abbiamo ancora a che fare con funzioni semplici sottilmente mascherate sotto il cofano, ed è rischioso: ogni "azione di valore" ha la responsabilità di trasmettere effettivamente l'azione risultante di qualsiasi funzione contenuta (altrimenti il flusso di controllo dell'intero programma è facilmente interrotto da un'azione mal condotta nel mezzo). Faremmo meglio a rendere esplicito questo requisito. Bene, risulta che quelle sono le leggi della monade , anche se non sono sicuro che possiamo davvero formularle senza gli operatori standard di bind / join.

Ad ogni modo, abbiamo ora raggiunto una formulazione di IO con un'istanza di monade corretta:

data IO a = TxtOut String (IO a)
           | TxtIn (String -> IO a)
           | TerminateWith a

txtOut :: String -> IO ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO String
txtIn = TxtIn $ TerminateWith

instance Functor IO where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Ovviamente questa non è un'implementazione efficiente di IO, ma è in linea di principio utilizzabile.


@jdlugosz: IO3 a ≡ Cont IO2 a. Ma intendevo quel commento più come un cenno a coloro che già conoscono la monade di continuazione, in quanto non ha esattamente la reputazione di essere adatto ai principianti.
lasciato il

4

Le monadi sono solo una struttura conveniente per risolvere una classe di problemi ricorrenti. Innanzitutto, le monadi devono essere funzioni (cioè devono supportare la mappatura senza guardare gli elementi (o il loro tipo)), devono anche portare un'operazione di associazione (o concatenamento) e un modo per creare un valore monadico da un tipo di elemento ( return). Infine, binde returndeve soddisfare due equazioni (identità sinistra e destra), chiamate anche leggi della monade. (In alternativa si potrebbe definire che le monadi hanno un flattening operationinvece di un legame.)

La monade lista è comunemente usata per affrontare il non determinismo. L'operazione di bind seleziona un elemento dell'elenco (intuitivamente tutti in mondi paralleli ), consente al programmatore di fare un calcolo con loro e quindi combina i risultati in tutti i mondi in un unico elenco (concatenando o appiattendo un elenco nidificato ). Ecco come si definirebbe una funzione di permutazione nel quadro monadico di Haskell:

perm [e] = [[e]]
perm l = do (leader, index) <- zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

Ecco un esempio di sessione di sostituzione :

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

Va notato che la monade elenco non è in alcun modo un calcolo con effetti collaterali. Una struttura matematica essendo una monade (cioè conforme alle interfacce e alle leggi sopra menzionate) non implica effetti collaterali, sebbene i fenomeni di effetto collaterale spesso si adattino perfettamente alla struttura monadica.


3

Le monadi servono sostanzialmente per comporre le funzioni insieme in una catena. Periodo.

Ora il modo in cui si compongono differisce tra le monadi esistenti, determinando così comportamenti diversi (ad esempio, per simulare lo stato mutevole nella monade di stato).

La confusione sulle monadi è che essendo così generali, cioè un meccanismo per comporre funzioni, possono essere usate per molte cose, portando così le persone a credere che le monadi riguardino lo stato, l'IO, ecc., Quando si tratta solo di "comporre funzioni ".

Ora, una cosa interessante delle monadi è che il risultato della composizione è sempre del tipo "M a", cioè un valore all'interno di una busta etichettata con "M". Questa funzionalità sembra essere davvero piacevole da implementare, ad esempio, una chiara separazione tra puro e codice impuro: dichiarare tutte le azioni impure come funzioni di tipo "IO a" e non fornire alcuna funzione, quando si definisce la monade IO, per eliminare il " un "valore dall'interno di" IO a ". Il risultato è che nessuna funzione può essere pura e allo stesso tempo estrarre un valore da un "IO a", perché non c'è modo di prendere tale valore rimanendo puro (la funzione deve essere all'interno della monade "IO" per usare tale valore). (NOTA: niente è perfetto, quindi la "camicia di forza IO" può essere rotta usando "unsafePerformIO: IO a -> a"


2

Hai bisogno di monadi se hai un costruttore di tipi e funzioni che restituiscono valori di quella famiglia di tipi . Alla fine, vorresti combinare questo tipo di funzioni insieme . Questi sono i tre elementi chiave per rispondere al perché .

Lasciami elaborare. Hai Int, Stringe Realle funzioni di tipo Int -> String, String -> Reale così via. È possibile combinare facilmente queste funzioni, finendo con Int -> Real. La vita è bella.

Quindi, un giorno, devi creare una nuova famiglia di tipi . Potrebbe essere perché è necessario considerare la possibilità di non restituire alcun valore ( Maybe), restituire un errore ( Either), più risultati ( List) e così via.

Si noti che Maybeè un costruttore di tipi. Prende un tipo, come Inte restituisce un nuovo tipo Maybe Int. Prima cosa da ricordare, nessun costruttore di tipo, nessuna monade.

Ovviamente, vuoi usare il tuo costruttore di codice nel tuo codice e presto finirai con funzioni come Int -> Maybe Stringe String -> Maybe Float. Ora, non puoi combinare facilmente le tue funzioni. La vita non è più buona.

Ed ecco quando le monadi vengono in soccorso. Ti consentono di combinare di nuovo quel tipo di funzioni. Hai solo bisogno di cambiare la composizione . per > == .


2
Questo non ha nulla a che fare con le famiglie di tipi. Di cosa stai parlando?
dfeuer
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.