Предположим, что функция имеет побочные эффекты. Если мы возьмем все эффекты, которые он производит, в качестве входных и выходных параметров, то функция будет чистой для внешнего мира.
Итак, для нечистой функции
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
не касаясь реального мира.
"Impurification"
Теперь предположим, что мы хотим сделать содержимое файла также заглавными. Верхний регистр - это чистая функция
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
Оставил как упражнение.