Припустимо, функція має побічні ефекти. Якщо ми врахуємо всі ефекти, які він виробляє, як параметри входу та виходу, то функція є чистою для зовнішнього світу.
Отже, для нечистої функції
f' :: Int -> Int
ми додамо 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.
тоді f
знову чисто. Ми визначаємо параметризований тип даних type IO a = RealWorld -> (a, RealWorld)
, тому нам не потрібно вводити RealWorld стільки разів, а можемо просто писати
f :: Int -> IO Int
Для програміста поводження з RealWorld безпосередньо занадто небезпечно - зокрема, якщо програміст отримує свої значення типу RealWorld, вони можуть спробувати скопіювати його, що в принципі неможливо. (Подумайте, спробуйте скопіювати, наприклад, всю файлову систему. Де б ви її помістили?) Тому наше визначення IO інкапсулює також стани всього світу.
Склад «нечистих» функцій
Ці нечисті функції марні, якщо ми не можемо зв'язати їх між собою. Розглянемо
getLine :: IO String ~ RealWorld -> (String, RealWorld)
getContents :: String -> IO String ~ String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO () ~ String -> RealWorld -> ((), RealWorld)
Ми хочемо
- отримати ім’я файлу з консолі,
- прочитати цей файл і
- вивести вміст цього файлу на консоль.
Як би ми це зробили, якби ми могли отримати доступ до держав реального світу?
printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
(contents, world2) = (getContents filename) world1
in (putStrLn contents) world2 -- results in ((), world3)
Ми бачимо викрійку тут. Функції називаються так:
...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...
Тож ми могли б визначити оператора, ~~~
який їх прив'язує:
(~~~) :: (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
тоді ми могли б просто написати
printFile = getLine ~~~ getContents ~~~ putStrLn
не торкаючись реального світу.
"Імпуріфікація"
Тепер припустимо, що ми хочемо також зробити верхній регістр вмісту файлів. Перевищення - це чиста функція
upperCase :: String -> String
Але для того, щоб потрапити в реальний світ, він повинен повернути своє IO String
. Таку функцію легко зняти:
impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)
Це можна узагальнити:
impurify :: a -> IO a
impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)
так що impureUpperCase = impurify . upperCase
, і ми можемо писати
printUpperCaseFile =
getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(Примітка. Зазвичай ми пишемо getLine ~~~ getContents ~~~ (putStrLn . upperCase)
)
Ми весь час працювали з монадами
Тепер давайте подивимося, що ми зробили:
- Ми визначили оператора,
(~~~) :: IO b -> (b -> IO c) -> IO c
який поєднує дві нечисті функції разом
- Ми визначили функцію,
impurify :: a -> IO a
яка перетворює чисте значення в нечисте.
Тепер ми робимо ідентифікацію (>>=) = (~~~)
і return = impurify
, і бачимо? У нас монада.
Технічна примітка
Щоб переконатися, що це дійсно монада, є ще кілька аксіом, які також потрібно перевірити:
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
Зліва як вправа.