Le visualizzazioni n. 1 e n. 2 non sono corrette in generale.
- Qualsiasi tipo di tipo di dati
* -> *
può funzionare come un'etichetta, le monadi sono molto più di questo.
- (Ad eccezione della
IO
monade) i calcoli all'interno di una monade non sono impuri. Rappresentano semplicemente calcoli che percepiamo avere effetti collaterali, ma sono puri.
Entrambi questi equivoci derivano dal concentrarsi sulla IO
monade, che in realtà è un po 'speciale.
Proverò a elaborare un po 'il n. 3, senza entrare nella teoria delle categorie, se possibile.
Calcoli standard
Tutti i calcoli in un linguaggio di programmazione funzionale, può essere visto come funzioni con un tipo di origine e un tipo di destinazione: f :: a -> b
. Se una funzione ha più di un argomento, possiamo convertirla in una funzione a un argomento tramite il curry (vedi anche wiki di Haskell ). E se abbiamo solo un valore x :: a
(una funzione con argomenti 0), possiamo convertirlo in una funzione che prende un argomento del tipo di unità : (\_ -> x) :: () -> a
.
Possiamo costruire programmi più complessi da quelli più semplici componendo tali funzioni usando l' .
operatore. Ad esempio, se abbiamo f :: a -> b
e g :: b -> c
otteniamo g . f :: a -> c
. Nota che funziona anche per i nostri valori convertiti: se lo abbiamo x :: a
e lo convertiamo nella nostra rappresentazione, otteniamo f . ((\_ -> x) :: () -> a) :: () -> b
.
Questa rappresentazione ha alcune proprietà molto importanti, vale a dire:
- Abbiamo una funzione molto speciale: la funzione di identità
id :: a -> a
per ogni tipo a
. È un elemento identitario rispetto a .
: f
è uguale sia a f . id
che a id . f
.
- L'operatore di composizione della funzione
.
è associativo .
Calcoli monadici
Supponiamo di voler selezionare e lavorare con una speciale categoria di calcoli, il cui risultato contiene qualcosa di più del semplice valore restituito. Non vogliamo specificare cosa significhi "qualcosa in più", vogliamo mantenere le cose il più generali possibile. Il modo più generale di rappresentare "qualcosa in più" è rappresentarlo come una funzione di tipo - un tipo m
di tipo * -> *
(cioè converte un tipo in un altro). Quindi per ogni categoria di calcoli con cui vogliamo lavorare, avremo una funzione di tipo m :: * -> *
. (In Haskell, m
è []
, IO
, Maybe
, etc.) e la categoria volontà contiene tutte le funzioni di tipi a -> m b
.
Ora vorremmo lavorare con le funzioni in tale categoria allo stesso modo del caso di base. Vogliamo essere in grado di comporre queste funzioni, vogliamo che la composizione sia associativa e vogliamo avere un'identità. Abbiamo bisogno:
- Avere un operatore (chiamiamolo così
<=<
) che compone le funzioni f :: a -> m b
e g :: b -> m c
in qualcosa come g <=< f :: a -> m c
. E deve essere associativo.
- Per avere una funzione di identità per ogni tipo, chiamiamola
return
. Vogliamo anche che f <=< return
sia uguale f
e uguale a return <=< f
.
Qualsiasi m :: * -> *
per cui abbiamo tali funzioni return
ed <=<
è chiamato una monade . Ci consente di creare calcoli complessi da quelli più semplici, proprio come nel caso di base, ma ora i tipi di valori di ritorno vengono trasformati da m
.
(In realtà, ho leggermente abusato del termine categoria qui. Nel senso della teoria delle categorie possiamo definire la nostra costruzione una categoria solo dopo che sappiamo che obbedisce a queste leggi.)
Monadi di Haskell
In Haskell (e altri linguaggi funzionali) lavoriamo principalmente con valori, non con funzioni di tipo () -> a
. Quindi, invece di definire <=<
per ogni monade, definiamo una funzione (>>=) :: m a -> (a -> m b) -> m b
. Tale definizione alternativa è equivalente, possiamo esprimere >>=
usando <=<
e viceversa (prova come un esercizio o vedi le fonti ). Il principio ora è meno ovvio, ma rimane lo stesso: i nostri risultati sono sempre di tipi m a
e componiamo funzioni di tipi a -> m b
.
Per ogni monade che creiamo, non dobbiamo dimenticare di verificarlo return
e di <=<
avere le proprietà richieste: associatività e identità sinistra / destra. Espressi usando return
e >>=
sono chiamati leggi della monade .
Un esempio: elenchi
Se scegliamo m
di esserlo []
, otteniamo una categoria di funzioni di tipi a -> [b]
. Tali funzioni rappresentano calcoli non deterministici, i cui risultati potrebbero essere uno o più valori, ma anche nessun valore. Questo dà origine alla cosiddetta lista monade . La composizione di f :: a -> [b]
e g :: b -> [c]
funziona come segue: g <=< f :: a -> [c]
significa calcolare tutti i possibili risultati di tipo [b]
, applicare g
a ciascuno di essi e raccogliere tutti i risultati in un unico elenco. Espresso in Haskell
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
o usando >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Si noti che in questo esempio i tipi restituiti erano [a]
quindi era possibile che non contenessero alcun valore di tipo a
. In effetti, per una monade non esiste tale requisito che il tipo restituito debba avere tali valori. Alcune monadi hanno sempre (come IO
o State
), ma altre no, come []
o Maybe
.
La monade IO
Come ho già detto, la IO
monade è in qualche modo speciale. Un valore di tipo IO a
indica un valore di tipo a
costruito interagendo con l'ambiente del programma. Quindi (diversamente da tutte le altre monadi), non possiamo descrivere un valore di tipo IO a
usando una costruzione pura. Ecco IO
semplicemente un tag o un'etichetta che distingue i calcoli che interagiscono con l'ambiente. Questo è (l'unico caso) in cui le viste n. 1 e n. 2 sono corrette.
Per la IO
monade:
- Composizione
f :: a -> IO b
e g :: b -> IO c
mezzi: calcolo f
che interagisce con l'ambiente, quindi calcolo g
che utilizza il valore e calcola il risultato interagendo con l'ambiente.
return
aggiunge semplicemente il IO
"tag" al valore (semplicemente "calcoliamo" il risultato mantenendo intatto l'ambiente).
- Le leggi della monade (associatività, identità) sono garantite dal compilatore.
Alcune note:
- Dato che i calcoli monadici hanno sempre il tipo di risultato di
m a
, non c'è modo di "sfuggire" alla IO
monade. Il significato è: una volta che un calcolo interagisce con l'ambiente, non è possibile costruirne uno che non lo fa.
- Quando un programmatore funzionale non sa come fare qualcosa in modo puro, può (come ultima risorsa) programmare l'attività mediante un calcolo con stato all'interno della
IO
monade. Questo è il motivo per cui IO
viene spesso chiamato sin bin di un programmatore .
- Si noti che in un mondo impuro (nel senso della programmazione funzionale) la lettura di un valore può anche cambiare l'ambiente (come consumare l'input dell'utente). Ecco perché funzioni come
getChar
devono avere un tipo di risultato di IO something
.