haskell-notes

сначала сохранить текст, затем прочитать обновления. Тогда текст останется прежним.

Ещё один пример. Предположим у нас есть функция getChar, которая читает букву с клавиатуры. И

функция print, которая выводит строку на экран И посмотрим на такое выражение:

let c = getChar

in

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 where

idK

:: 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

readConfig :: String -> IO Config

print

:: Show a => a -> IO ()

— большая и сложная, но !чистая! функция

algorithm

:: Int -> Config -> Result

Функция readInit читает начальное значение, функция readConfig читает из файла наcтройки, функ-

ция print выводит значение на экран, если это значение можно преобразовать в строку. Функция algorithm

это большая функция, которая вычисляет какие-то данные. Фактически наше программа это и есть функция

algorithm. В этой схеме мы добавили взаимодействие с пользователем лишь в одном месте, вся функция

algorithm построена по правилам мира описаний. Так мы внесли порядок выполнения в программу, сохра-

нив возможность определения чистых функций.

Если у нас будет ещё один “кадр”, ещё одно действие, например как только функция algorithm закончила

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162