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 filter
funzione 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 nub
funzione, 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 ( notElem
non 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.Monad
modulo:
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)
filterM
esegue 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, filterAccum
può essere definita in termini di filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
con la StateT
monade, 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.