не можем изменять состояния, мы можем лишь давать новые имена или строить новые выражения из уже
существующих.
Но в этот статичный мир описаний не вписывается взаимодействие с пользователем. Предположим, что
мы хотим написать такую программу: мы набираем на клавиатуре имя файла, нажимаем Enter
и программапоказывает на экране содержимое этого файла, затем мы набираем текст, нажимаем Enter
и текст дописыва-ется в конец файла, файл сохраняется. Это описание предполагает упорядоченность действий. Мы не можем
сначала сохранить текст, затем прочитать обновления. Тогда текст останется прежним.
Ещё один пример. Предположим у нас есть функция getChar, которая читает букву с клавиатуры. И
функция print, которая выводит строку на экран И посмотрим на такое выражение:
let
c = getCharin
print $
c : c : []О чём говорит это выражение? Возможно, прочитай с клавиатуры букву и выведи её на экран дважды.
Но возможен и другой вариант, если в нашем языке все определения это синонимы мы можем записать это
выражение так:
print $
getChar : getChar : []Это выражение уже говорит о том, что читать с клавиатуры необходимо дважды! А ведь мы сделали обыч-
ное преобразование, заменили вхождения синонима на его определение, но смысл изменился. Взаимодей-
ствие с пользователем нарушает чистоту функций, нечистые функции называются функциями с побочными
эффектами.
Как быть? Можно ли внести в мир описаний порядок выполнения, сохранив преимущества функциональ-
ной чистоты? Долгое время этот вопрос был очень трудным для чистых функциональных языков. Как можно
пользоваться языком, который не позволяет сделать такие базовые вещи как ввод/вывод?
126 | Глава 8: IO
8.2 Монада IO
Где-то мы уже встречались с такой проблемой. Когда мы говорили о типе ST
и обновлении значений. Тамтоже были проблемы порядка вычислений, нам удалось преодолеть их с помощью скрытой передачи фиктив-
ного состояния. Тогда наши обновления были
Теперь всё гораздо труднее. Нам всё-таки хочется взаимодействовать с внешним миром. Для обозначения
внешнего мира мы определим специальный тип и назовём его RealWorld
:module IO
(IO
) where
data RealWorld = RealWorld
newtype IO
a = IO (ST RealWorld a)instance Functor
IO where ...
instance Applicative
IO where ...
instance Monad
IO where ...
Тип IO
(от англ. input-output или ввод-вывод) обозначает взаимодействие с внешним миром. Внешниймир словно является состоянием наших вычислений. Экземпляры классов композиции специальных функций
такие же как и для ST
(а следовательно и для State). Но при этом, поскольку мы конкретизировали первыйпараметр типа ST
, мы уже не сможем воспользоваться функцией runST.Тип RealWorld
определён в модуле Control.Monad.ST, там же можно найти и функцию:stToIO :: ST RealWorld
a -> IO aИнтересно, что класс 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 Kleisli
m whereidK
::
a -> m a(>>
) :: (a -> m b) -> (b -> m c) -> (a -> m c)Монада IO | 127
Композиция специальных функций типа a -> IO
b вносит порядок вычисления. Считается, что сначалабудет вычислена функция слева от композиции, а затем функция справа от композиции. Это происходит за
счёт скрытой передачи фиктивного состояния. Теперь перейдём к классу Monad
. Там композиция заменяетсяна применение или операция связывания:
ma >>=
mfДля типа IO
эта запись говорит о том, что сначала будет выполнено выражение ma и результат будет под-ставлен в выражение mf и только затем будет выполнено mf. Оператор связывания для специальных функций
вида:
a -> IO
bраскалывает наш статический мир на “до” и “после”. Однажды попав в сети IO
, мы не можем из нихвыбраться, поскольку теперь у нас нет функции runST. Но это не так страшно. Тип IO
дробит наш статическиймир на кадры. Но мы спокойно можем создавать статические чистые функции и поднимать их в мир IO
лишьтам где это действительно нужно.
Рассмотрим такой пример, программа читает с клавиатуры начальное значение, затем загружает файл
настроек. Потом запускается, какая-то сложная функция и в самом конце мы выводим результат на экран.
Схематично мы можем записать эту программу так:
program =
liftA2 algorithm readInit (readConfig ”file”) >>= print-- функции с побочными эффектами
readInit
:: IO Int