Интересно, что класс Monadбыл придуман как раз для решения проблемы ввода-вывода. Классы типов
изначально задумывались для решения проблемы определения арифметических операций на разных числах
и функции сравнения на равенство для разных типов, мало кто тогда догадывался, что классы типов сыграют
такую роль, станут основополагающей особенностью языка.
a
f
IO b
b
g
IO c
До
После
a
g
f
IO c
a
f>>g
IO c
Рис. 8.1: Композиция для монады IO
Посмотрим на (рис. 8.1). Это рисунок для класса Kleisli. Здесь под >>понимается композиция, как мы
её определяли в главе 6, а не метод класса Monad, вспомним определение:
class Kleislim where
idK
::a ->m a
( >>) ::(a ->m b) ->(b ->m c) ->(a ->m c)
Монада IO | 127
Композиция специальных функций типа a -> IOb вносит порядок вычисления. Считается, что сначала
будет вычислена функция слева от композиции, а затем функция справа от композиции. Это происходит за
счёт скрытой передачи фиктивного состояния. Теперь перейдём к классу Monad. Там композиция заменяется
на применение или операция связывания:
ma >>=mf
Для типа IOэта запись говорит о том, что сначала будет выполнено выражение ma и результат будет под-
ставлен в выражение mf и только затем будет выполнено mf. Оператор связывания для специальных функций
вида:
a -> IOb
раскалывает наш статический мир на “до” и “после”. Однажды попав в сети IO, мы не можем из них
выбраться, поскольку теперь у нас нет функции runST. Но это не так страшно. Тип IOдробит наш статический
мир на кадры. Но мы спокойно можем создавать статические чистые функции и поднимать их в мир IOлишь
там где это действительно нужно.
Рассмотрим такой пример, программа читает с клавиатуры начальное значение, затем загружает файл
настроек. Потом запускается, какая-то сложная функция и в самом конце мы выводим результат на экран.
Схематично мы можем записать эту программу так:
program =liftA2 algorithm readInit (readConfig ”file”) >>=print
-- функции с побочными эффектами
readInit
:: IO Int
readConfig :: String -> IO Config
print
:: Showa =>a -> IO()
-- большая и сложная, но !чистая! функция
algorithm
:: Int -> Config -> Result
Функция readInit читает начальное значение, функция readConfig читает из файла наcтройки, функ-
ция print выводит значение на экран, если это значение можно преобразовать в строку. Функция algorithm
это большая функция, которая вычисляет какие-то данные. Фактически наше программа это и есть функция
algorithm. В этой схеме мы добавили взаимодействие с пользователем лишь в одном месте, вся функция
algorithm построена по правилам мира описаний. Так мы внесли порядок выполнения в программу, сохра-
нив возможность определения чистых функций.
Если у нас будет ещё один “кадр”, ещё одно действие, например как только функция algorithm закончила
вычисления ей нужны дополнительные данные от пользователя, на основе которых мы сможем продолжить
вычисления с помощью какой-нибудь другой функции. Тогда наша программа примет вид:
program =
liftA2 algorithm2 readInit
(liftA2 algorithm1 readInit (readConfig ”file”))
>>=print
-- функции с побочными эффектами
readInit
:: IO Int
readConfig :: String -> IO Config
print
:: Showa =>a -> IO()
-- большие и сложные, но !чистые! функции
algorithm1
:: Int -> Config -> Result1
algorithm2
:: Int -> Result1 -> Result2
Теперь у нас два кадра, программа выполняется в два этапа. Каждый из них разделён участками взаимо-
действия с пользователем. Но тип IOприсутствует лишь в первых шести строчках, остальные два миллиона
строк написаны в мире описаний, исключительно чистыми функциями, которые поднимаются в мир специ-
альных функций с помощью функций liftA2 и стыкуются с помощью операции связывания >>=.
Попробуем тип IOв интерпретаторе. Мы будем пользоваться двумя стандартными функциями getChar и
print
-- читает символ с клавиатуры
getChar :: IO Char
-- выводит значение на экран
print :: IO()
128 | Глава 8: IO
Функция print возвращает значение единичного типа, завёрнутое в тип IO, поскольку нас интересует не
само значение а побочный эффект, который выполняет эта функция, в данном случае это вывод на экран.
Читать дальше