Załóżmy, że funkcja ma skutki uboczne. Jeśli weźmiemy wszystkie wywoływane przez nią efekty jako parametry wejściowe i wyjściowe, wówczas funkcja jest czysta dla świata zewnętrznego.
Więc dla nieczystej funkcji
f' :: Int -> Int
do rozważań dodajemy RealWorld
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.
potem f
znów jest czysty. Definiujemy sparametryzowany typ danych type IO a = RealWorld -> (a, RealWorld)
, więc nie musimy tyle razy wpisywać RealWorld i możemy po prostu pisać
f :: Int -> IO Int
Dla programisty bezpośrednia obsługa RealWorld jest zbyt niebezpieczna - w szczególności, jeśli programista zdobędzie wartość typu RealWorld, może spróbować skopiować , co jest w zasadzie niemożliwe. (Pomyśl na przykład o próbie skopiowania całego systemu plików. Gdzie byś to umieścił?) Dlatego nasza definicja IO obejmuje również stany całego świata.
Skład funkcji „nieczystych”
Te nieczyste funkcje są bezużyteczne, jeśli nie możemy ich połączyć. Rozważać
getLine :: IO String ~ RealWorld -> (String, RealWorld)
getContents :: String -> IO String ~ String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO () ~ String -> RealWorld -> ((), RealWorld)
Chcemy, aby
- pobrać nazwę pliku z konsoli,
- przeczytaj ten plik i
- wypisuje zawartość tego pliku na konsoli.
Jak byśmy to zrobili, gdybyśmy mieli dostęp do stanów świata rzeczywistego?
printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
(contents, world2) = (getContents filename) world1
in (putStrLn contents) world2 -- results in ((), world3)
Widzimy tutaj wzór. Funkcje nazywane są w ten sposób:
...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...
Moglibyśmy więc zdefiniować operator, ~~~
aby je powiązać:
(~~~) :: (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
wtedy moglibyśmy po prostu napisać
printFile = getLine ~~~ getContents ~~~ putStrLn
bez dotykania prawdziwego świata.
„Nieczystość”
Załóżmy teraz, że chcemy, aby zawartość pliku również była wielka. Wielkie litery to czysta funkcja
upperCase :: String -> String
Ale aby trafić do prawdziwego świata, musi zwrócić plik IO String
. Łatwo jest podnieść taką funkcję:
impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)
Można to uogólnić:
impurify :: a -> IO a
impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)
więc impureUpperCase = impurify . upperCase
i możemy pisać
printUpperCaseFile =
getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(Uwaga: zwykle piszemy getLine ~~~ getContents ~~~ (putStrLn . upperCase)
)
Cały czas pracowaliśmy z monadami
Zobaczmy teraz, co zrobiliśmy:
- Zdefiniowaliśmy operator,
(~~~) :: IO b -> (b -> IO c) -> IO c
który łączy ze sobą dwie nieczyste funkcje
- Zdefiniowaliśmy funkcję,
impurify :: a -> IO a
która przekształca czystą wartość w nieczystą.
Teraz możemy sprawić, że identyfikacja (>>=) = (~~~)
i return = impurify
, i zobaczyć? Mamy monadę.
Uwaga techniczna
Aby upewnić się, że to naprawdę monada, wciąż jest kilka aksjomatów, które również należy sprawdzić:
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
Pozostawiony jako ćwiczenie.