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:
- Abbiamo definito un operatore
(~~~) :: IO b -> (b -> IO c) -> IO c
che mette insieme due funzioni impure
- Abbiamo definito una funzione
impurify :: a -> IO a
che 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:
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
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
f >>= (\x -> g x >>= h) = (f >>= g) >>= h
Lasciato come esercizio.