haskell-notes

концов отрезка выбрем тот, у которого знак значения функции f отличается от знака значения в сере-

дине отрезка. Далее повторим эту процедуру на новом отрезке. И так пока мы не найдём корень или

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

изменяйте их внутри типа ST.

Упражнения | 125

Глава 8

IO

Пока мы не написали ещё ни одной программы, которой можно было бы пользоваться вне интерпретато-

ра. Предполагается, что программа как-то взаимодействует с пользователем (ожидает ввода с клавиатуры)

и изменяет состояние компьютера (выводит сообщения на экран, записывает данные в файлы). Но пока что

мы не знаем как взаимодействовать с окружающим миром.

Самое время узнать! Сначала мы посмотрим какие проблемы связаны с реализацией взаимодействия с

пользователем. Как эти проблемы решаются в Haskell. Потом мы научимся решать несколько типичных задач,

связанных с вводом/выводом.

8.1 Чистота и побочные эффекты

Когда мы определяем новые функции или константы мы лишь даём новые имена комбинациям значений.

В этом смысле у нас ничего не изменяется. По-другому это называется функциональной чистотой (referential

transparency). Это свойство говорит о том, что мы свободно можем заменить в тексте программы любой

синоним на его определение и это никак не скажется на результате.

Функция является чистой, если её выход зависит только от её входов. В любой момент выполнения про-

граммы для одних и тех же входов будет один и тот же выход. Это свойство очень ценно. Оно облегчает

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

ко явно. Если мы видим, что другая функция используется в данной функции, то она используется в этой

функции. У нас нет таинственных глобальных переменных, в которые мы можем записывать данные из од-

ной функции и читать их с помощью другой. Мы вообще не можем ничего записывать и ничего читать. Мы

не можем изменять состояния, мы можем лишь давать новые имена или строить новые выражения из уже

существующих.

Но в этот статичный мир описаний не вписывается взаимодействие с пользователем. Предположим, что

мы хотим написать такую программу: мы набираем на клавиатуре имя файла, нажимаем Enter и программа

показывает на экране содержимое этого файла, затем мы набираем текст, нажимаем Enter и текст дописыва-

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

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

Ещё один пример. Предположим у нас есть функция 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

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

Читай продолжение на следующей странице
Добавить комментарии