типа:

(a -> m b) -> (b -> c) -> (a -> m c)

Оказывается мы можем составить её из методов класса Kleisli. Мы назовём эту функцию композиции

(+> ).

(+> ) :: Kleisli m => (a -> m b) -> (b -> c) -> (a -> m c)

f +> g = f *> (g >> idK)

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

Композиция функций | 87

Три композиции

У нас появилось много композиций целых три:

аргументы

|

результат

обычная

>>

обычная

==

обычная

специальная

+>

обычная

==

специальная

специальная

*>

специальная

==

специальная

При этом важно понимать, что по смыслу это три одинаковые функции. Они обозначают операцию по-

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

Обобщённая формулировка категории Клейсли

Отметим, что мы могли бы сформулировать класс Kleisli и в более общем виде с помощью класса

Category:

class Kleisli m where

idK

:: Category cat => cat a (m a)

(*> ) :: Category cat => cat a (m b) -> cat b (m c) -> cat a (m c)

(+> ) :: (Category cat, Kleisli m)

=> cat a (m b) -> cat b c -> cat a (m c)

f +> g = f *> (g >> idK)

Мы заменили функциональный тип на его обобщение. Для наглядности мы будем пользоваться специ-

альной формулировкой со стрелочным типом.

Для этого мы определим модуль Kleisli. hs

module Kleisli where

import Prelude hiding (id, (>> ))

class Category cat where

id

:: cat a a

(>> ) :: cat a b -> cat b c -> cat a c

class Kleisli m where

idK

:: a -> m a

(*> ) :: (a -> m b) -> (b -> m c) -> (a -> m c)

(+> ) :: Kleisli m => (a -> m b) -> (b -> c) -> (a -> m c)

f +> g = f *> (g >> idK)

-- Экземпляр для функций

instance Category (-> ) where

id

= \x -> x

f >> g

= \x -> g (f x)

Мы не будем импортировать функцию id, а определим её в классе Category. Также в Prelude уже опре-

делена функция (>> ) мы спрячем её с помощью специальной директивы hiding для того, чтобы она нам не

мешалась. Далее мы будем дополнять этот модуль экземплярами класса Kleisli и примерами.

6.2 Примеры специальных функций

Частично определённые функции

Частично определённые функции – это такие функции, которые определены не для всех значений аргу-

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

Поскольку числа натуральные, то для нуля такого числа нет. Для описания этого поведения мы можем вос-

пользоваться специальным типом Maybe. Посмотрим на его определение:

data Maybe a = Nothing | Just a

deriving (Show, Eq, Ord)

88 | Глава 6: Функторы и монады: теория

a

f

b

Nothing

Рис. 6.2: Частично определённая функция

Частично определённая функция имеет тип a -> Maybe b (рис. 6.2), если всё в порядке и значение было

вычислено, она вернёт (Just a), а в случае ошибки будет возвращено значение Nothing. Теперь мы можем

определить нашу функцию так:

pred :: Nat -> Maybe Nat

pred Zero

= Nothing

pred (Succ a)

= Just a

Для Zero предыдущий элемент не определён .

Составляем функции вручную

Значение функции pred завёрнуто в упаковку Maybe, и для того чтобы воспользоваться им нам придётся

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

числа:

pred2 :: Nat -> Maybe Nat

pred2 x =

case pred x of

Just (Succ a) -> Just a

_

-> Nothing

Если мы захотим определить pred3, мы заменим pred в case-выражении на pred2. Вроде не такое уж и

длинное решение. Но всё же мы теряем все преимущества гибких функций, все преимущества бесточечного

стиля. Нам бы хотелось написать так:

pred2 :: Nat -> Maybe Nat

pred2 = pred >> pred

pred3 :: Nat -> Maybe Nat

pred3 = pred >> pred >> pred

Но компилятор этого не допустит.

Композиция

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

ние графически (рис. 6.3). Сверху изображены две частично определённых функции. Если функция f вернула

значение, то оно подставляется в следующую частично определённую функцию. Если же первая функция не

смогла вычислить результат и вернула Nothing, то считается что вся функция (f*> g) вернула Nothing.

Теперь давайте закодируем это определение в Haskell. При этом мы воспользуемся нашим классом

Kleisli. Аналогом функции id для частично определённых функций будет функция, которая просто за-

ворачивает значение в конструктор Just.

instance Kleisli Maybe where

idK

= Just

f *> g = \a -> case f a of

Nothing -> Nothing

Just b

-> g b

Смотрите, в case-выражении мы возвращаем Nothing, если функция f вернула Nothing, а если ей удалось

вычислить значение и она вернула (Just b) мы передаём это значение в следующую функцию, то есть

составляем выражение (g b).

Сохраним это определение в модуле Kleisli, а также определение для функции pred и загрузим модуль

в интерпретатор. Перед этим нам придётся добавить в список функций, которые мы не хотим импортировать

из Prelude функцию pred, она также уже определена в Prelude. Для определения нашей функции нам по-

требуется модуль Nat, который мы уже определили. Скопируем файл Nat. hs в ту же директорию, в которой

содержится файл Kleisli. hs и подключим этот модуль. Шапка модуля примет вид:

Примеры специальных функций | 89

a

f

b

b

g

c

Nothing

Nothing

b

a

g

f

c

Nothing

a

f*>g

c

Nothing

Рис. 6.3: Композиция частично определённых функций

module Kleisli where

import Prelude hiding(id, (>> ), pred)

import Nat

Добавим определение экземпляра Kleisli для Maybe в модуль Kleisli а также определение функции

pred. Сохраним обновлённый модуль и загрузим в интерпретатор.

*Kleisli> :load Kleisli

[1 of 2] Compiling Nat

( Nat. hs, interpreted )

[2 of 2] Compiling Kleisli

( Kleisli. hs, interpreted )

Ok, modules loaded: Kleisli, Nat.

*Kleisli> let pred2 = pred *> pred

*Kleisli> let pred3 = pred *> pred *> pred

*Kleisli> let two

= Succ (Succ Zero)

*Kleisli>

*Kleisli> pred two

Just (Succ Zero)

*Kleisli> pred3 two

Nothing

Обратите внимание на то, как легко определяются производные функции. Желаемое поведение для ча-

стично определённых функций закодировано в функции (*> ) теперь нам не нужно заворачивать значения и

разворачивать их из типа Maybe.

Приведём пример функции, которая составлена из частично определённой функции и обычной. Опреде-

лим функцию beside, которая вычисляет соседей для данного числа Пеано.

*Kleisli> let beside = pred +> \a -> (a, a + 2)

*Kleisli> beside Zero

Nothing

*Kleisli> beside two

Just (Succ Zero, Succ (Succ (Succ Zero)))

*Kleisli> (pred *> beside) two

Just (Zero, Succ (Succ Zero))

В выражении

pred +> \a -> (a, a + 2)

Мы сначала вычисляем предыдущее число, и если оно есть составляем пару из \a -> (a, a+2), в пару

попадёт данное число и число, следующее за ним через одно. Поскольку сначала мы вычислили предыдущее

число в итоговом кортеже окажется предыдущее число и следующее.

90 | Глава 6: Функторы и монады: теория

Итак с помощью функций из класса Kleisli мы можем составлять частично определённые функции в

бесточечном стиле. Обратите внимание на то, что все функции кроме pred были составлены в интерпрета-

торе.Отметим, что в Prelude определена специальная функция maybe, которая похожа на функцию foldr для

списков, она заменяет в значении типа Maybe конструкторы на функции. Посмотрим на её определение:

maybe

:: b -> (a -> b) -> Maybe a -> b

maybe n f Nothing

=

n

maybe n f (Just x) =

f x

С помощью этой функции мы можем переписать определение экземпляра Kleisli так:

instance Kleisli Maybe where

idM

= Just

f *> g

= f >> maybe Nothing g

Многозначные функции

Многозначные функции ветрены и непостоянны. Для некоторых значений аргументов они возвращают

одно значение, для иных десять, а для третьих и вовсе ничего. В Haskell такие функции имеют тип a -> [b].

Функция возвращает список ответов. На (рис. 6.4) изображена схема многозначной функции.

a

f

b

Рис. 6.4: Многозначная функция

Приведём пример. Системы Линденмайера (или L-системы) моделируют развитие живого организма.

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

буква заменяется на новую последовательность букв, согласно определённым правилам. Так организм живёт

и развивается. Приведём пример:

a → ab

b → a

a

ab

aba

abaab

abaababa

У нас есть два правила размножения клеток-букв в организме. На каждом этапе мы во всём слове заме-

няем букву a на слово ab и букву b на a. Начав с одной буквы a, мы за несколько шагов пришли к более

сложному слову.

Опишем этот процесс в Haskell. Для этого определим правила развития организма в виде многозначной

функции:

next :: Char -> String

next ’a’ = ”ab”

next ’b’ = ”a”

Напомню, что строки в Haskell являются списками символов. Теперь нам нужно применить многозначную

функцию к выходу многозначной функции. Для этого мы воспользуемся классом Kleisli.

Композиция

Определим экземпляр класса Kleisli для списков. На (рис. 6.5) изображена схема композиции в случае

многозначных функций. После применения первой функции f мы применяем функцию к каждому элементу

списка, который был получен из f. Так у нас получится список списков. Но нам нужен список, для этого

мы после применения g объединяем все значения в один большой список. Отметим, что функции f и g в

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

число стрелок.

Закодируем эту схему в Haskell:

Примеры специальных функций | 91

a

f

b

b

g

c

g

c

b

b

a

g

f

c

b

g

c

a

f*>g

c

Рис. 6.5: Композиция многозначных функций

instance Kleisli [] where

idK

= \a -> [a]

f *> g

= f >> map g >> concat

Функция тождества принимает одно значение и погружает его в список. В композиции мы сначала при-

меняем f, затем к каждому элементу списка результата применяем g, так у нас получается список списков.

После чего мы сворачиваем его в один список с помощью функции concat.

Вспомним тип функций map и concat:

map

:: (a -> b) -> [a] -> [b]

concat

:: [[a]] -> [a]

С помощью композиции мы можем получить n-тое поколение так:

generate :: Int -> (a -> [a]) -> (a -> [a])

generate 0 f = idK

generate n f = f *> generate (n - 1) f

Или мы можем воспользоваться функцией iterate и написать это определение так:

generate :: Int -> (a -> [a]) -> (a -> [a])

generate n f = iterate (*> f) idK !! n

Функция iterate принимает функцию вычисления следующего элемента и начальное значение и строит

бесконечный список итераций:

iterate :: (a -> a) -> a -> [a]

iterate f a = [a, f a, f (f a), f (f (f a)), ... ]

Если мы подставим наши аргументы то мы получим список:

[id, f, f*> f, f*> f*> f, f*> f*> f*> f, ... ]

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

Kleisli определением экземпляра для списков и функциями next и generate:

*Kleisli> :reload

[2 of 2] Compiling Kleisli

( Kleisli. hs, interpreted )

Ok, modules loaded: Kleisli, Nat.

*Kleisli> let gen n = generate n next ’a’

*Kleisli> gen 0

”a”

92 | Глава 6: Функторы и монады: теория

*Kleisli> gen 1

”ab”

*Kleisli> gen 2

”aba”

*Kleisli> gen 3

”abaab”

*Kleisli> gen 4

”abaababa”

Правила L-системы задаются многозначной функцией. Функция generate позволяет по такой функции

строить произвольное поколение развития буквенного организма.

6.3 Применение функций

Давайте определим в терминах композиции ещё одну полезную функцию. А именно функцию примене-

ния. Вспомним её тип:

($) :: (a -> b) -> a -> b

Эту функцию можно определить через композицию, если у нас есть в наличии постоянная функция и

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

константу в функцию мы можем составить композицию:

($) :: (a -> b) -> a -> b

f $ a = (const a >> f) ()

В самом конце мы подставляем специальное значение (). Это значение единичного типа (unit type) или

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

этом определении. Зачем такое запутанное определение, вместо привычного (f a)? Оказывается точно таким

же способом мы можем определить применение в нашем мире специальных функций a -> m b.

Применение в этом мире происходит особенным образом. Необходимо помнить о том, что второй аргу-

мент функции применения, значение, которое мы подставляем в функцию, также было получено из какой-то

другой функции. Поэтому оно будет иметь такую же форму, что и значения справа от стрелки. В нашем

случае это m b.

Посмотрим на типы специальных функций применения:

(*$) :: (a -> m b) -> m a -> m b

(+$) :: (a -> b)

-> m a -> m b

Функция *$ применяет специальную функцию к специальному значению, а функция +$ применяет обыч-

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

применения, мы только меняем знаки для композиции:

f

$ a = (const a >> f) ()

f *$ a = (const a *> f) ()

f +$ a = (const a +> f) ()

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

ниям. Добавим эти определения в модуль Kleisli и посмотрим как происходит применение в интерпрета-

торе. Одна тонкость заключается в том, что мы определяли применение в терминах класса Kleisli, поэтому

правильно было написать типы новых функций так:

infixr 0 +$, *$

(*$) :: Kleisli m => (a -> m b) -> m a -> m b

(+$) :: Kleisli m => (a -> b)

-> m a -> m b

Также мы определили приоритет выполнения операций.

Загрузим в интерпретатор:

*Kleisli> let three = Succ (Succ (Succ Zero))

*Kleisli> pred *$ pred *$ idK three

Just (Succ Zero)

*Kleisli> pred *$ pred *$ idK Zero

Nothing

Применение функций | 93

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

функции idK.

Вычислим третье поколение L-системы:

*Kleisli> next *$ next *$ next *$ idK ’a’

”abaab”

Мы можем использовать и другие функции на списках:

*Kleisli> next *$ tail $ next *$ reverse $ next *$ idK ’a’

”aba”

Применение функций многих переменных

С помощью функции +$ мы можем применять к специальным значениям обычные функции одного аргу-

мента. А что если нам захочется применить функцию двух аргументов?

Например если мы захотим сложить два частично определённых числа:

?? (+) (Just 2) (Just 2)

На месте ?? должна стоять функция типа:

?? :: (a -> b -> c) -> m a -> m b -> m c

Оказывается с помощью методов класса Kleisli мы можем определить такую функцию для любой обыч-

ной функции, а не только для функции двух аргументов. Мы будем называть такие функции словом liftN,

где N – число, указывающее на арность функции. Функция (liftN f) “поднимает” (от англ. lift) обычную

функцию f в мир специальных функций.

Функция lift1 у нас уже есть, это просто функция +$. Теперь давайте определим функцию lift2:

lift2 :: Kleisli m => (a -> b -> c) -> m a -> m b -> m c

lift2 f a b = ...

Поскольку функция двух аргументов на самом деле является функцией одного аргумента мы можем

применить первый аргумент с помощью функции lift1, посмотрим что у нас получится:

lift1

:: (a’ -> b’) -> m’ a’ -> m’ b’

f

:: (a -> b -> c)

a

:: m a

lift1 f a

:: m (b -> c)

-- m’ == m, a’ == a, b’ == b -> c

Теперь в нашем определении для lift2 появится новое слагаемое g:

lift2 :: Kleisli m => (a -> b -> c) -> m a -> m b -> m c

lift2 f a b = ...

where g = lift1 f a

Один аргумент мы применили, осталось применить второй. Нам нужно составить выражение (g b), но

для этого нам нужна функция типа:

m (b -> c) -> m b -> m c

Эта функция применяет к специальному значению функцию, которая завёрнута в тип m. Посмотрим на

определение этой функции, мы назовём её $$:

($$) :: Kleisli m => m (a -> b) -> m a -> m b

mf $$ ma = ( +$ ma) *$ mf

Вы можете убедиться в том, что это определение проходит проверку типов. Посмотрим как эта функция

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

добавим в модуль Kleisli это определение и загрузим его в интерпретатор:

94 | Глава 6: Функторы и монады: теория

*Kleisli> :reload Kleisli

Ok, modules loaded: Kleisli, Nat.

*Kleisli> Just (+2) $$ Just 2

Just 4

*Kleisli> Nothing $$ Just 2

Nothing

*Kleisli> [(+1), (+2), (+3)] $$ [10,20,30]

[11,21,31,12,22,32,13,23,33]

*Kleisli> [(+1), (+2), (+3)] $$ []

[]

Обратите внимание на то, что в случае списков были составлены все возможные комбинации применений.

Мы применили первую функцию из списка ко всем аргументам, потом вторую функцию, третью и объединили

все результаты в список.

Теперь мы можем закончить наше определение для lift2:

lift2 :: Kleisli m => (a -> b -> c) -> m a -> m b -> m c

lift2 f a b = f’ $$ b

where f’ = lift1 f a

Мы можем записать это определение более кратко:

lift2 :: Kleisli m => (a -> b -> c) -> m a -> m b -> m c

lift2 f a b = lift1 f a $$ b

Теперь давайте добавим это определение в модуль Kleisli и посмотрим в интерпретаторе как работает

эта функция:

*Kleisli> :reload

[2 of 2] Compiling Kleisli

( Kleisli. hs, interpreted )

Ok, modules loaded: Kleisli, Nat.

*Kleisli> lift2 (+) (Just 2) (Just 2)

Just 4

*Kleisli> lift2 (+) (Just 2) Nothing

Nothing

Как на счёт функций трёх и более аргументов? У нас уже есть функции lift1 и lift2 определим функцию

lift3:

lift3 :: Kleisli m => (a -> b -> c -> d) -> m a -> m b -> m c -> m d lift3 f a b c = ...

Первые два аргумента мы можем применить с помощью функции lift2. Посмотрим на тип получивше-

гося выражения:

lift2

:: Kleisli m => (a’ -> b’ -> c’) -> m a’ -> m b’ -> m c’

f

:: a -> b -> c -> d

lift2 f a b :: m (c -> d)

-- a’ == a, b’ == b, c’ == c -> d

У нас опять появился тип m (c -> d) и к нему нам нужно применить значение m c, чтобы получить m d.

Этим как раз и занимается функция $$. Итак итоговое определение примет вид:

lift3 :: Kleisli m => (a -> b -> c -> d) -> m a -> m b -> m c -> m d lift3 f a b c = lift2 f a b $$ c

Так мы можем определить любую функцию liftN через функции liftN-1 и $$.

Несколько полезных функций

Теперь мы умеем применять к специальным значениям произвольные обычные функции. Определим ещё

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

специальный список:

Применение функций | 95

import Prelude hiding (id, (>> ), pred, sequence)

sequence :: Kleisli m => [m a] -> m [a]

sequence = foldr (lift2 (:)) (idK [])

Мы “спрячем” из Prelude одноимённую функцию sequence. Посмотрим на примеры:

*Kleisli> sequence [Just 1, Just 2, Just 3]

Just [1,2,3]

*Kleisli> sequence [Just 1, Nothing, Just 3]

Nothing

Во второй команде вся функция вернула Nothing потому что при объединении списка встретилось зна-

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

которые могут не вычислить результат. Поскольку значение одного из элементов не определено, весь список

не определён.

Посмотрим как работает эта функция на списках:

*Kleisli> sequence [[1,2,3], [11,22]]

[[1,11],[1,22],[2,11],[2,22],[3,11],[3,22]]

Она составляет список всех комбинаций элементов из всех подсписков.

С помощью этой функции мы можем определить функцию mapK. Эта функция является аналогом обычной

функции map, но она применяет специальную функцию к списку значений.

mapK :: Kleisli m => (a -> m b) -> [a] -> m [b]

mapK f = sequence . map f

6.4 Функторы и монады

В этой главе мы выписали вручную все определения для класса Kleisli. Мы сделали это потому, что на

самом деле в арсенале стандартных средств Haskell такого класса нет. Класс Kleisli строит замкнутый мир

специальных функций a -> m b. Его цель построить язык в языке и сделать программирование со специ-

альными функциями таким же удобным как и с обычными функциями. Мы пользовались классом Kleisli

исключительно в целях облегчения понимания этого мира. Впрочем никто не мешает нам определить этот

класс и пользоваться им в наших программах.

А пока посмотрим, что есть в Haskell и как это соотносится с тем, что мы уже увидели. С помощью класса

Kleisli

мы научились делать три различных операции применения:

Применение:

• обычных функций одного аргумента к специальным значениям (функция +$).

• обычных функций произвольного числа аргументов к специальным значениям (функции +$ и $$)

• специальных функций к специальным значениям (функция *$).

В Haskell для решения этих задач предназначены три отдельных класса. Это функторы, аппликативные

функторы и монады.

Функторы

Посмотрим на определение класса Functor:

class Functor f where

fmap :: (a -> b) -> f a -> f b

Тип метода fmap совпадает с типом для функции +$:

(+$) :: Kleisli m => (a -> b) -> m a -> m b

Нам только нужно заменить m на f и зависимость от Kleisli на зависимость от Functor:

Итак в Haskell у нас есть базовая операция fmap применения обычной функции к значению из мира спе-

циальных функций. В модуле Control.Applicative определён инфиксный синоним <$> для этой функции.

96 | Глава 6: Функторы и монады: теория

Аппликативные функторы

Посмотрим на определение класса Applicative:

class Functor f => Applicative f where

pure

:: a -> f a

(<*> )

:: f (a -> b) -> f a -> f b

Если присмотреться к типам методов этого класса, то мы заметим, что это наши старые знакомые idK и

$$. Если для данного типа f определён экземпляр класса Applicative, то из контекста следует, что для него

также определён и экземпляр класса Functor.

Значит у нас есть функции fmap (или lift1) и <*> (или $$). С их помощью мы можем составить функции

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

Класс Applicative определён в модуле Control.Applicative, там же мы сможем найти и функции liftA,

liftA2, liftA3 и символьный синоним <$> для функции fmap. Функции liftAn определены так:

liftA2 f a b

= f <$> a <*> b

liftA3 f a b c = f <$> a <*> b <*> c

Видно что эти определения с точностью до обозначений совпадают с теми, что мы уже писали для класса

Kleisli.

Монады

Посмотрим на определение класса Monad

class Monad m where

return :: a -> m a

(>>=)

:: m a -> (a -> m b) -> m b

Присмотримся к типам методов этого класса:

return :: a -> m a

Их типа видно, что это ни что иное как функция idK. В классе Monad у неё точно такой же смысл. Теперь

функция >>=, она читается как функция связывания (bind).

(>>=)

:: m a -> (a -> m b) -> m b

Так возможно совпадение не заметно, но давайте “перевернём” эту функцию:

(=<< )

:: Monad m => (a -> m b) -> m a -> m b

(=<< ) = flip (>>=)

Поменяв аргументы местами, мы получили знакомую функцию *$. Итак функция связывания это функция

применения специальной функции к специальному значению. У неё как раз такой смысл.

В Prelude определены экземпляры класса Monad для типов Maybe и [].

Они определены по такому же принципу, что и наши определения для Kleisli только не для композиции, а

для применения.

Отметим, что в модуле Control.Monad определены функции sequence и mapM, они несут тот же смысл,

что и функции sequence и mapК, которые мы определяли для класса Kleisli.

Свойства классов

Посмотрим на свойства функторов и аппликативных функторов.

Функторы и монады | 97

Свойства класса Functor

fmap id x

== x

-- тождество

fmap f . fmap g

== fmap (f . g)

-- композиция

Первое свойство говорит о том, что если мы применяем fmap к функции тождества, то мы должны снова

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

альному значению не изменяет это значение. Второе свойство говорит о том, что последовательное примене-

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

обычных функций к специальному значению.

Если всё это звучит туманно, попробуем переписать эти свойства в терминах композиции:

mf +> id

== mf

(mf +> g) +> h

== mf +> (g >> h)

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

рое свойство указывает на ассоциативность композиции одной специальной функции mf и двух обычных

функций g и h.

Свойства класса Applicative

Свойства класса Applicative, для наглядности они сформулированы не через методы класса, а через

производные функции.

fmap f x

== liftA f x

-- связь с Functor

liftA

id x

== x

-- тождество

liftA3 (. ) f g x

== f <*> (g <*> x)

-- композиция

liftA

f (pure x)

== pure (f x)

-- гомоморфизм

Первое свойство говорит о том, что применение специальной функции одного аргумента совпадает с

методом fmap из класса Functor. Свойство тождества идентично аналогичному свойству для класса Functor.

Свойство композиции сформулировано хитро, но давайте посмотрим на типы аргументов:

(. ) :: (b -> c) -> (a -> b) -> (a -> c)

f

:: m (b -> c)

g

:: m (a -> b)

x

:: m a

liftA3 (. ) f g x :: m c

g <*> x

:: m b

f (g <*> x)

:: m c

Слева в свойстве стоит liftA3, а не liftA2, потому что мы сначала применяем композицию (. ) к двум

функциям f и g, а затем применяем составную функцию к значению x.

Последнее свойство говорит о том, что если мы возьмём обычную функцию и обычное значение и подни-

мем их в мир специальных значений с помощью lift и pure, то это тоже самое если бы мы просто применили

бы функцию f к значению в мире обычных значений и затем подняли бы результат в мир специальных зна-

чений.

Полное определение классов

На самом деле я немного схитрил. Я рассказал вам только об основных методах классов Applicative

и Monad. Но они содержат ещё несколько дополнительных методов, которые выражаются через остальные.

Посмотрим на них, начнём с класса Applicative.

class Functor f => Applicative f where

-- | Поднимаем значение в мир специальных значений.

pure :: a -> f a

-- | Применение специального значения-функции.

(<*> ) :: f (a -> b) -> f a -> f b

-- | Константная функция. Отбрасываем первое значение.

98 | Глава 6: Функторы и монады: теория

(*> ) :: f a -> f b -> f b

(*> ) = liftA2 (const id)

-- | Константная функция, Отбрасываем второе значение.

(<*) :: f a -> f b -> f a

(<*) = liftA2 const

Два новых метода (*> ) и (<*) имеют смысл константных функций. Первая функция игнорирует значение

слева, а вторая функция игнорирует значение справа. Посмотрим как они работают в интерпретаторе:

Prelude Control.Applicative> Just 2 *> Just 3

Just 3

Prelude Control.Applicative> Nothing *> Just 3

Nothing

Prelude Control.Applicative> (const id) Nothing

Just 3

Just 3

Prelude Control.Applicative> [1,2] <* [1,2,3]

[1,1,1,2,2,2]

Значение игнорируется, но способ комбинирования специальных функций учитывается. Так во втором

выражении не смотря на то, что мы не учитываем конкретное значение Nothing, мы учитываем, что если один

из аргументов частично определённой функции не определён, то не определено всё значение. Сравните с

результатом выполнения следующего выражения.

По той же причине в последнем выражении мы получили три копии первого списка. Так произошло

потому, что второй список содержал три элемента. К каждому из элементов была применена функция const

x, где x пробегает по элементам списка слева от (<*).

Аналогичный метод есть и в классе Monad:

class

Monad m

where

return

:: a -> m a

(>>=)

:: m a -> (a -> m b) -> m b

(>> )

:: m a -> m b -> m b

fail

:: String -> m a

m >> k

= m >>= const k

fail s

= error s

Функция >> в классе Monad, которую мы прятали из-за символа композиции, является аналогом постоян-

ной функции в классе Monad. Она работает так же как и *> . Функция fail используется для служебных нужд

Haskell при выводе ошибок. Поэтому мы её здесь не рассматриваем. Для определения экземпляра класса

Monad достаточно определить методы return и >>=.

Исторические замечания

Напрашивается вопрос. Зачем нам функции return и pure или *> и >> ? Если вы заглянете в документа-

цию к модулю Control.Monad, то там вы найдёте функции liftM, liftM2, liftM3, которые выполняют те же

операции, что и аналогичные функции из модуля Control.Applicative.

Стандартные библиотеки устроены так, потому что класс Applicative появился гораздо позже класса

Monad. И к появлению этого нового класса уже накопилось огромное число библиотек, которые рассчитаны

на прежние имена. Но в будущем возможно прежние классы будут заменены на такие классы:

class Functor f where

fmap :: (a -> b) -> f a -> f b

class Pointed f where

pure :: a -> f a

class (Functor f, Pointed f) => Applicative f where

(<*> ) :: f (a -> b) -> f a -> f b

(*> )

:: f a -> f b -> f b

(<*)

:: f a -> f b -> f a

class Applicative f => Monad f where

(>>=) :: f a -> (a -> f b) -> f b

Функторы и монады | 99

6.5 Краткое содержание

В этой главе мы долгой обходной дорогой шли к понятию монады и функтора. Эти классы служат для

облегчения работы в мире специальных функций вида a -> m b, в категории Клейсли

С помощью класса Functor можно применять специальные значения к обычным функциям одного аргу-

мента:

class Functor f where

fmap :: (a -> b) -> f a -> f b

С помощью класса Applicative можно применять специальные значения к обычным функциям любого

числа аргументов:

class Functor f => Applicative f where

pure

:: a -> f a

<*>

:: f (a -> b) -> f a -> f b

liftA

:: Applicative f => (a -> b) -> f a -> f b

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

...

С помощью класса Monad можно применять специальные значения к специальным функциям.

class Monad m where

return

:: a -> m a

(>>=)

:: m a -> (a -> m b) -> m b

Функция return является функцией id в мире специальных функций, а функция >>= является функцией

применения ($), с обратным порядком следования аргументов. Вспомним также класс Kleisli, на примере

котором мы узнали много нового из жизни специальных функций:

class Kleisli m where

idK

:: a -> m a

(*> )

:: (a -> m b) -> (b -> m c) -> (a -> m c)

Мы узнали несколько стандартных специальных функций:

Частично определённые функции

a -> Maybe b

data Maybe a = Nothing | Just a

Многозначные функции

a -> [b]

data [a] = [] | a : [a]

6.6 Упражнения

В первых упражнениях вам предлагается по картинке специальной функции написать экземпляр классов

Kleisli и Monad.

Функции с состоянием

b

a

f

s

s

Рис. 6.6: Функция с состоянием

100 | Глава 6: Функторы и монады: теория

В Haskell нельзя изменять значения. Новые сложные значения описываются в терминах базовых значе-

ний. Но как же тогда мы сможем описать функцию с состоянием? Функцию, которая принимает на вход

значение, составляет результат на основе внутреннего состояния и значения аргумента и обновляет состоя-

ние. Поскольку мы не можем изменять состояние единственное, что нам остаётся – это принимать значение

состояния на вход вместе с аргументом и возвращать обновлённое состояние на выходе. У нас получится

такой тип:

a -> s -> (b, s)

Функция принимает одно значение типа a и состояние типа s, а возвращает пару, которая состоит из

результата типа b и обновлённого состояния. Если мы введём синоним:

type State s b = s -> (b, s)

И вспомним о частичном применении, то мы сможем записать тип функции с состоянием так:

a -> State s b

В Haskell пошли дальше и выделили для таких функций специальный тип:

data State s a = State (s -> (a, s))

runState :: State s a -> s -> (a, s)

runState (State f) = f

b

c

a

f

b

g

s

s

s

s

b

c

a

g

f

s

s

s

c

a

f*>g

s

s

Рис. 6.7: Композиция функций с состоянием

Функция runState просто извлекает функцию из оболочки State.

На (рис. 6.6) изображена схема функции с состоянием. В сравнении с обычной функцией у такой функции

один дополнительный выход и один дополнительный вход типа s. По ним течёт и изменяется состояние.

Попробуйте по схеме композиции для функций с состоянием написать экземпляры для классов Kleisli

и Monad для типа State s (рис. 6.7).

Подсказка: В этом определении есть одна хитрость, в отличае от типов Maybe и [a] у типа State два

параметра, это параметр состояния и параметр значения. Но мы делаем экземпляр не для State, а для State

s, то есть мы свяжем тип с некоторым произвольным типом s.

instance Kleisli (State s) where

...

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

a

f

b

env

Рис. 6.8: Функция с окружением

Функции с окружением

Сначала мы рассмотрим функции с окружением. Функции с окружением – это такие функции, у которых

есть некоторое хранилище данных или окружение, из которых они могут читать информацию. Но в отличие

от функций с состоянием они не могут это окружение изменять. Функция с окружением похожа на функцию

с состоянием без одного выхода для состояния (рис. 6.8).

Функция с окружением принимает аргумент a и окружение env и возвращает результат b:

a -> env -> b

Как и в случае функций с состоянием выделим для функции с окружением отдельный тип. В Haskell он на-

зывается Reader (от англ. чтец). Все функции с окружением имеют возможность читать из общего хранилища

данных. Например они могут иметь доступ на чтение к общей базе данных.

data Reader env b = Reader (env -> b)

runReader :: Reader env b -> (env -> b)

runReader (Reader f) = f

Теперь функция с окружением примет вид:

a -> Reader env b

Определите для функций с окружением экземпляр класса Kleisli. У нас возникнет цепочка функций,

каждая из которых будет нуждаться в значении окружения. Поскольку окружение общее для всех функций

мы всем функциям передадим одно и то же значение (рис. 6.9).

a

f

b

b

g

c

env

env

b

a

g

f

c

env

a

f*>g

c

env

Рис. 6.9: Функция с окружением

Функции-накопители

Функции-накопители при вычислении за ширмой накапливают некоторое значение. Функция-накопитель

похожа на функцию с состоянием но без стрелки, по которой состояние подаётся в функцию (рис. 6.10).

Функция-накопитель имеет тип: a -> (b, msg)

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

102 | Глава 6: Функторы и монады: теория

a

f

b

Msg

Рис. 6.10: Функция-накопитель

data Writer msg b = Writer (b, msg)

runWriter :: Writer msg b -> (b, msg)

runWriter (Writer a) = a

Тип функции примет вид:

a -> Writer msg b

Значения типа msg мы будем называть сообщениями. Смысл функций a -> Writer msg b заключается

в том, что при вычислении они накапливают в значении msg какую-нибудь информацию. Это могут быть

отладочные сообщения. Или база данных, которая открыта для всех функций на запись.

Класс Monoid

Как мы будем накапливать результат? Пока мы умеем лишь возвращать из функции пару значений. Одно

из них нам нужно передать в следующую функцию, а что делать с другим?

На помощь нам придёт класс Monoid, он определён в модуле Data.Monoid:

class Monoid a where

mempty

:: a

mappend :: a -> a -> a

В этом классе определено пустое значение mempty и бинарная функция соединения двух значений в одно.

Этот класс очень похож на класс Category и Kleisli. Там тоже было значение, которое ничего не делает и

операция составления нового значения из двух простейших значений. Даже свойства класса похожи:

mempty

‘mappend‘ f

= f

f

‘mappend‘ mempty

= f

f ‘mappend‘ (g ‘mappend‘ h) =

(f ‘mappend‘ g) ‘mappend‘ h

a

g

f

b

b

c

msg

msg

b

a

g

f

c

MsgG

++

MsgF ++ MsgG

MsgF

a

f*>g

c

msg

Рис. 6.11: Композиция функций-накопителей

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

Первые два свойства говорят о том, что значение mempty и вправду является пустым элементом отно-

сительно операции mappend. А третье свойство говорит о том, что порядок при объединении элементов не

важен.

Посмотрим на определение экземпляра для списков:

instance Monoid [a] where

mempty

= []

mappend = (++)

Итак пустой элемент это пустой список, а объединение это операция конкатенации списков. Проверим в

интерпретаторе:

*Kleisli> :m Data.Monoid

Prelude Data.Monoid> [1 .. 4] ‘mappend‘ [4, 3 .. 1]

[1,2,3,4,4,3,2,1]

Prelude Data.Monoid> ”Hello” ‘mappend‘ ” World” ‘mappend‘ mempty

”Hello World”

Напишите экземпляр класса Kleisli для функций накопителей по (рис. 6.11). При этом будем считать,

что тип msg является экземпляром класса Monoid.

Экземпляры для функторов и монад

Представьте, что у нас нет класса Kleisli, а есть лишь Functor, Applicative и Monad. Напишите экзем-

пляры для этих классов для всех рассмотренных в этой главе специальных функций (в том числе и для Reader

и Writer). Экземпляры Functor и Applicative могут быть определены через Monad. Но для тренировки опре-

делите экземпляры полностью. Сначала Functor, затем Applicative и в последнюю очередь Monad.

Деревья

Напишите экземпляры классов Kleisli и Monad для двух типов, которые описывают деревья. Бинарные

деревья:

data BTree a = BList a | BNode a (BTree a) (BTree a)

Деревья с несколькими узлами:

data Tree a = Node a [Tree a]

Считайте, что списки являются частными случаями деревьев. В этом смысле деревья будут описывать

многозначные функции, которые возвращают несколько значений, организованных в иерархическую струк-

туру.

Стандартные функции

Почитайте документацию к модулям Control.Monad и Control.Applicative. Присмотритесь к функциям,

попробуйте применить их в интерпретаторе.

Эквивалентность классов Kleisli и Monad

Покажите, что классы Kleisli и Monad эквивалентны. Для этого нужно для произвольного типа c с одним

параметром m определить два экземпляра:

instance Kleisli m => Monad

m where

instance Monad

m => Kelisli m where

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

Свойства класса Monad

Если класс Monad эквивалентен Kleisli, то в нём должны выполнятся точно такие же свойства. Запишите

свойства класса Kleisli через методы класса Monad

104 | Глава 6: Функторы и монады: теория

Глава 7

Функторы и монады: примеры

В этой главе мы закрепим на примерах то, что мы узнали о монадах и функторах. Напомню, что с по-

мощью монад и функторов мы можем комбинировать специальные функции вида (a -> m b) с другими

специальными функциями.

У нас есть функции тождества и применения:

class Functor f where

fmap :: (a -> b) -> f a -> f b

class Functor f => Applicative f where

pure

:: a -> f a

(<*> )

:: f (a -> b) -> f a -> f b

class Monad m where

return

:: a -> m a

(>>=)

:: m a -> (a -> m b) -> m b

(=<< ) :: (a -> m b) -> m a -> m b

(=<< ) = flip (>>=)

Вспомним основные производные функции для этих классов:

Или в терминах класса Kleisli:

-- Композиция

(>=> ) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)

(<=< ) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)

-- Константные функции

(*> ) :: Applicative f => f a -> f b -> f b

(<*) :: Applicative f => f a -> f b -> f a

-- Применение обычных функций к специальным значениям

(<$> )

:: Functor f => (a -> b) -> f a -> f b

liftA

:: Applicative f => (a -> b)

-> f a -> f b

liftA2 :: Applicative f => (a -> b -> c)

-> f a -> f b -> f c

liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

-- Преобразование элементов списка специальной функцией

mapM

:: Monad m => (a -> m b) -> [a] -> m [b]

Нам понадобится модуль с определениями типов и экземпляров монад для всех типов, которые мы рас-

смотрели в предыдущей главе. Экземпляры для [] и Maybe уже определены в Prelude, а типы State, Reader

и Writer можно найти в библиотеках mtl и transformers. Пока мы не знаем как устанавливать библиотеки

определим эти типы и экземпляры для Monad самостоятельно. Возможно вы уже определили их, выполняя

одно из упражнений предыдущей главы, если это так сейчас вы можете сверить ответы. Определим модуль

Types:

module Types(

State(.. ), Reader(.. ), Writer(.. ),

runState, runWriter, runReader,

| 105

module Control.Applicative,

module Control.Monad,

module Data.Monoid)

where

import Data.Monoid

import Control.Applicative

import Control.Monad

-------------------------------------------------

-- Функции с состоянием

--

--

a -> State s b

data State s a = State (s -> (a, s))

runState :: State s a -> s -> (a, s)

runState (State f) = f

instance Monad (State s) where

return a

= State $ \s -> (a, s)

ma >>= mf = State $ \s0 ->

let (b, s1) = runState ma s0

in

runState (mf b) s1

---------------------------------------------------

-- Функции с окружением

--

--

a -> Reader env b

data Reader env a = Reader (env -> a)

runReader :: Reader env a -> env -> a

runReader (Reader f) = f

instance Monad (Reader env) where

return a

= Reader $ const a

ma >>= mf

= Reader $ \env ->

let b = runReader ma env

in

runReader (mf b) env

---------------------------------------------------

-- Функции-накопители

--

--

Monoid msg => a -> Writer msg b

data Writer msg a = Writer (a, msg)

deriving (Show)

runWriter :: Writer msg a -> (a, msg)

runWriter (Writer f) = f

instance Monoid msg => Monad (Writer msg) where

return a

= Writer (a, mempty)

ma >>= mf

= Writer (c, msgA ‘mappend‘ msgF)

where (b, msgA) = runWriter ma

(c, msgF) = runWriter $ mf b

Я пропустил определения для экземпляров классов Functor и Applicative, их можно получить из экзем-

пляра для класса Monad с помощью стандартных функций liftM, return и ap из модуля Control.Monad.

Нам встретилась новая запись в экспорте модуля. Для удобства мы экспортируем модули

Control.Applicative, Control.Monad и Data.Monoid целиком. Для этого мы написали ключевое слово

module перед экспортируемым модулем. Теперь если мы в каком-нибудь другом модуле импортируем

модуль Types нам станут доступными все функции из этих модулей.

Мы определили экземпляры для Functor и Applicative с помощью производных функций класса Monad.

106 | Глава 7: Функторы и монады: примеры

7.1 Случайные числа

С помощью монады State можно имитировать случайные числа. Мы будем генерировать случайные числа

из интервала от 0 до 1 с помощью алгоритма:

nextRandom :: Double -> Double

nextRandom = snd . properFraction . (105.947 * )

Функция properFraction возвращает пару, которая состоит из целой части и остатка числа. Взяв второй

элемент пары с помощью snd, мы выделяем остаток. Функция nextRandom представляет собой генератор

случайных чисел, который принимает значение с предыдущего шага и строит по нему следующее значение.

Построим тип для случайных чисел:

type Random a = State Double a

next :: Random Double

next = State $ \s -> (s, nextRandom s)

Теперь определим функцию, которая прибавляет к данному числу случайное число из интервала от 0 до

1:

addRandom :: Double -> Random Double

addRandom x = fmap (+x) next

Посмотрим как эта функция работает в интерпретаторе:

*Random> runState (addRandom 5) 0.5

(5.5,0.9735000000000014)

*Random> runState (addRandom 5) 0.7

(5.7,0.16289999999999338)

*Random> runState (mapM addRandom [1 .. 5]) 0.5

([1.5,2.9735000000000014,3.139404500000154,4.769488561516319,

5.5250046269694195],0.6226652135290891)

В последней строчке мы с помощью функции mapM прибавили ко всем элементам списка разные случайные

числа, обновление счётчика происходило за кадром, с помощью функции mapM и экземпляра Monad для State.

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

[-1+a, 1+a], а другое из интервала [-2+b,2+b]:

addRandom2 :: Double -> Double -> Random Double

addRandom2 a b = liftA2 add next next

where add

a b = \x y -> diap a 1 x + diap b 1 y

diap c r = \x

-> x * 2 * r - r + c

Функция diap перемещает интервал от 0 до 1 в интервал от c-r до c+r. Обратите внимание на то как мы

сначала составили обычную функцию add, которая перемещает значения из интервала от 0 до 1 в нужный

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

значения. Посмотрим как работает эта функция:

*Random> runState (addRandom2 0 10) 0.5

(10.947000000000003,0.13940450000015403)

*Random> runState (addRandom2 0 10) 0.7

(9.725799999999987,0.2587662999992979)

Прибавим два списка и получим сумму:

*Random> let res = fmap sum $ zipWithM addRandom2 [1.. 3] [11 .. 13]

*Random> runState res 0.5

(43.060125804029965,0.969511377766409)

*Random> runState res 0.7

(39.86034841613788,0.26599261421101517)

Функция zipWithM является аналогом функции zipWith. Она устроена также как и функция mapM, сначала

применяется обычная функция zipWith, а затем функция sequence.

С помощью типа Random мы можем определить функцию подбрасывания монетки:

Случайные числа | 107

data Coin = Heads | Tails

deriving (Show)

dropCoin :: Random Coin

dropCoin = fmap drop’ next

where drop’ x

| x < 0.5

= Heads

| otherwise = Tails

У монетки две стороны орёл (Heads) и решка (Tails). Поскольку шансы на выпадание той или иной

стороны равны, мы для определения стороны разделяем интервал от 0 до 1 в равных пропорциях.

Подбросим монетку пять раз:

*Random> let res = sequence $ replicate 5 dropCoin

Функция replicate n a составляет список из n повторяющихся элементов a. Посмотрим что у нас полу-

чилось:

*Random> runState res 0.4

([Heads, Heads, Heads, Heads, Tails],0.5184926967068364)

*Random> runState res 0.5

([Tails, Tails, Heads, Tails, Tails],0.6226652135290891)

7.2 Конечные автоматы

С помощью монады State можно описывать конечные автоматы (finite-state machine). Конечный автомат

находится в каком-то начальном состоянии. Он принимает на вход ленту событий. Одно событие происходит

за другим. На каждое событие автомат реагирует переходом из одного состояния в другое.

type FSM s = State s s

fsm :: (ev -> s -> s) -> (ev -> FSM s)

fsm transition = \e -> State $ \s -> (s, transition e s)

Функция fsm принимает функцию переходов состояний transition и возвращает функцию, которая при-

нимает состояние и возвращает конечный автомат. В качестве значения конечный автомат FSM будет возвра-

щать текущее состояние.

С помощью конечных автоматов можно описывать различные устройства. Лентой событий будет ввод

пользователя (нажатие на кнопки, включение/выключение питания).

Приведём простой пример. Рассмотрим колонки, у них есть розетка, кнопка вкл/выкл и регулятор гром-

кости. Возможные состояния:

type Speaker = (SpeakerState, Level)

data SpeakerState = Sleep | Work

deriving (Show)

data Level

= Level Int

deriving (Show)

Тип колонок складывается из двух значений: состояния и уровня громкости. Колонки могут быть вы-

ключенными (Sleep) или работать на определённой громкости (Work). Считаем, что максимальный уровень

громкости составляет 10 единиц, а минимальный ноль единиц. Границы диапазона громкости описываются

такими функциями:

quieter :: Level -> Level

quieter (Level n) = Level $ max 0 (n-1)

louder :: Level -> Level

louder (Level n) = Level $ min 10 (n+1)

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

louder и quieter. Так мы не сможем выйти за пределы заданного диапазона.

Возможные события:

108 | Глава 7: Функторы и монады: примеры

data User = Button | Quieter | Louder

deriving (Show)

Пользователь может либо нажать на кнопку вкл/выкл или повернуть реле громкости влево, чтобы при-

глушить звук (Quieter) или вправо, чтобы сделать погромче (Louder). Будем считать, что колонки всегда

включены в розетку.

Составим функцию переходов:

speaker :: User -> FSM Speaker

speaker = fsm $ trans

where trans Button

(Sleep, n) = (Work, n)

trans Button

(Work,

n) = (Sleep, n)

trans Louder

(s,

n) = (s, louder n)

trans Quieter

(s,

n) = (s, quieter n)

Мы считаем, что при выключении колонок реле остаётся некотором положении, так что при следующем

включении они будут работать на той же громкости. Реле можно крутить и в состоянии Sleep. Посмотрим

на типичную сессию работы колонок:

*FSM> let res = mapM speaker [Button, Louder, Quieter, Quieter, Button]

Сначала мы включаем колонки, затем прибавляем громкость, затем дважды делаем тише и в конце вы-

ключаем. Посмотрим что получилось:

*FSM> runState res (Sleep, Level 2)

([(Sleep, Level 2),(Work, Level 2),(Work, Level 3),(Work, Level 2),

(Work, Level 1)],(Sleep, Level 1))

*FSM> runState res (Sleep, Level 0)

([(Sleep, Level 0),(Work, Level 0),(Work, Level 1),(Work, Level 0),

(Work, Level 0)],(Sleep, Level 0))

Смотрите, изменив начальное значение, мы изменили весь список значений. Обратите внимание на то,

что во втором прогоне мы не ушли в минус по громкости, не смотря на то, что пытались крутить реле за

установленный предел.

Определим колонки другого типа. Наши новые колонки будут безопаснее предыдущих. Представьте си-

туацию, что мы выключили колонки на высоком уровне громкости. Мы слушали домашнюю запись с низким

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

с нормальным уровнем звука. При включении колонок нас оглушил шквал звука. Чтобы этого избежать мы

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

Колонки при выключении будут выставлять уровень громкости на ноль и реле можно будет крутить

только если колонки включены.

safeSpeaker :: User -> FSM Speaker

safeSpeaker = fsm $ trans

where trans Button

(Sleep, _) = (Work,

Level 0)

trans Button

(Work,

_) = (Sleep, Level 0)

trans Quieter (Work,

n) = (Work,

quieter n)

trans Louder

(Work,

n) = (Work,

louder n)

trans _

(Sleep, n) = (Sleep, n)

При нажатии на кнопку вкл/выкл уровень громкости выводится в положение 0. Колонки реагируют на

запросы изменения уровня громкости только в состоянии Work. Посмотрим как работают наши новые колон-

ки:

*FSM> let res = mapM safeSpeaker [Button, Louder, Quieter, Button, Louder]

Мы включаем колонки, делаем по-громче, затем по-тише, затем выключаем и пытаемся изменить гром-

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

громкости 10:

*FSM> runState res (Sleep, Level 10)

([(Sleep, Level 10),(Work, Level 0),(Work, Level 1),(Work, Level 0),

(Sleep, Level 0)],(Sleep, Level 0))

Конечные автоматы | 109

Первое значение в списке является стартовым состоянием, которое мы задали. После этого колонки вклю-

чаются и мы видим, что уровень громкости переключился на ноль. Затем мы увеличиваем громкость, сбав-

ляем её и выключаем. Попытка изменить громкость выключенных колонок не проходит. Это видно по по-

следнему элементу списка и итоговому состоянию колонок, которое находится во втором элементе пары.

Предположим, что колонки работают с самого начала, тогда первым действием мы выключаем их. По-

смотрим, что случится дальше:

*FSM> runState res (Work, Level 10)

([(Work, Level 10),(Sleep, Level 0),(Sleep, Level 0),(Sleep, Level 0),

(Work, Level 0)],(Work, Level 1))

Дальше мы пытаемся изменить громкость но у нас ничего не выходит.

7.3 Отложенное вычисление выражений

В этом примере мы будем выполнять арифметические операции на целых числах. Мы будем их скла-

дывать, вычитать и умножать. Но вместо того, чтобы сразу вычислять выражения мы будем составлять их

описание. Мы будем кодировать операции конструкторами.

data Exp

= Var String

| Lit Int

| Neg Exp

| Add Exp Exp

| Mul Exp Exp

deriving (Show, Eq)

У нас есть тип Exp, который может быть либо переменной Var с данным строчным именем, либо целочис-

ленной константой Lit, либо одной из трёх операций: вычитанием (Neg), сложением (Add) или умножением

(Mul).

Такие типы называют абстрактными синтаксическими деревьями (abstract syntax tree, AST). Они содержат

описание выражений. Теперь вместо того чтобы сразу проводить вычисления мы будем собирать выражения

в значении типа Exp. Сделаем экземпляр для Num:

instance Num Exp where

negate

= Neg

(+)

= Add

(*)

= Mul

fromInteger = Lit . fromInteger

abs

= undefined

signum

= undefined

Также определим вспомогательные функции для обозначения переменных:

var :: String -> Exp

var = Var

n :: Int -> Exp

n = var . show

Функция var составляет переменную с данным именем, а функция n составляет переменную, у которой

имя является целым числом. Сохраним эти определения в модуле Exp. Теперь у нас всё готово для составле-

ния выражений:

*Exp> n 1

Var ”1”

*Exp> n 1 + 2

Add (Var ”1”) (Lit 2)

*Exp> 3 * (n 1 + 2)

Mul (Lit 3) (Add (Var ”1”) (Lit 2))

*Exp> - n 2 * 3 * (n 1 + 2)

Neg (Mul (Mul (Var ”2”) (Lit 3)) (Add (Var ”1”) (Lit 2)))

110 | Глава 7: Функторы и монады: примеры

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

и возвращать целое число.

eval :: Exp -> Int

eval (Lit n)

= n

eval (Neg n)

= negate $ eval n

eval (Add a b)

= eval a + eval b

eval (Mul a b)

= eval a * eval b

eval (Var name) = ???

Как быть с конструктором Var? Нам нужно откуда-то узнать какое значение связано с переменной. Функ-

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

Этот набор значений мы будем называть окружением.

Обратите внимание на то, что в каждом составном конструкторе мы рекурсивно вызываем функцию eval,

мы словно обходим всё дерево выражения. Спускаемся вниз, до самых листьев в которых расположены либо

значения (Lit), либо переменные (Var). Нам было бы удобно иметь возможность пользоваться окружением

из любого узла дерева. В этом нам поможет тип Reader.

Представим что у нас есть значение типа Env и функция, которая позволяет читать значения переменных

по имени:

value :: Env -> String -> Int

Теперь определим функцию eval:

eval :: Exp -> Reader Env Int

eval (Lit n)

= pure n

eval (Neg n)

= liftA

negate $ eval n

eval (Add a b)

= liftA2 (+) (eval a) (eval b)

eval (Mul a b)

= liftA2 (*) (eval a) (eval b)

eval (Var name) = Reader $ \env -> value env name

Определение сильно изменилось, оно стало не таким наглядным. Теперь значение eval стало специаль-

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

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

немного по другому:

eval :: Exp -> Reader Env Int

eval (Lit n)

= pure n

eval (Neg n)

= negateA $ eval n

eval (Add a b)

= eval a ‘addA‘ eval b

eval (Mul a b)

= eval a ‘mulA‘ eval b

eval (Var name) = Reader $ \env -> value env name

addA

= liftA2 (+)

mulA

= liftA2 (*)

negateA

= liftA negate

Тип Map

Для того чтобы закончить определение функции eval нам нужно определить тип Env и функцию value.

Для этого мы воспользуемся типом Map, он предназначен для хранения значений по ключу.

Этот тип живёт в стандартном модуле Data.Map. Посмотрим на его описание:

data Map k a = ..

Первый параметр типа k это ключ, а второй это значение. Мы можем создать значение типа Map из списка

пар ключ значение с помощью функции fromList.

Посмотрим на основные функции:

-- Создаём значения типа Map

-- создаём

empty :: Map k a

-- пустой Map

fromList :: Ord k => [(k, a)] -> Map k a

-- по списку (ключ, значение)

-- Узнаём значение по ключу

(! )

:: Ord k => Map k a -> k -> a

Отложенное вычисление выражений | 111

lookup

:: Ord k => k -> Map k a -> Maybe a

-- Добавляем элементы

insert :: Ord k => k -> a -> Map k a -> Map k a

-- Удаляем элементы

delete :: Ord k => k -> Map k a -> Map k a

Обратите внимание на ограничение Ord k в этих функциях, ключ должен быть экземпляром класса Ord.

Посмотрим как эти функции работают:

*Exp> :m +Data.Map

*Exp Data.Map> :m -Exp

Data.Map> let v = fromList [(1, ”Hello”), (2, ”Bye”)]

Data.Map> v ! 1

”Hello”

Data.Map> v ! 3

”*** Exception: Map.find: element not in the map

Data.Map> lookup 3 v

Nothing

Data.Map> let v1 = insert 3 ” Yo” v

Data.Map> v1 ! 3

Yo

Функция lookup является стабильным аналогом функции ! . В том смысле, что она определена с помощью

Maybe. Она не приведёт к падению программы, если для данного ключа не найдётся значение.

Теперь мы можем определить функцию value:

import qualified Data.Map as M(Map, lookup, fromList)

...

type Env = M.Map String Int

value :: Env -> String -> Int

value env name = maybe errorMsg $ M. lookup env name

where errorMsg = error $ ”value is undefined for ” ++ name

Обычно функции из модуля Data.Map включаются с директивой qualified, поскольку имена многих

функций из этого модуля совпадают с именами из модуля Prelude. Теперь все определения из модуля

Data.Map пишутся с приставкой M. .

Создадим вспомогательную функцию, которая упростит вычисление выражений:

runExp :: Exp -> [(String, Int)] -> Int

runExp a env = runReader (eval a) $ M. fromList env

Сохраним определение новых функций в модуле Exp. И посмотрим что у нас получилось:

*Exp> let env a b = [(”1”, a), (”2”, b)]

*Exp> let exp = 2 * (n 1 + n 2) - n 1

*Exp> runExp exp (env 1 2)

5

*Exp> runExp exp (env 10 5)

20

Так мы можем пользоваться функциями с окружением для того, чтобы читать значения из общего ис-

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

монадами:

eval :: Env -> Exp -> Int

eval env x = case x of

Lit n

-> n

Neg n

-> negate $ eval’ n

Add a b

-> eval’ a + eval’ b

Mul a b

-> eval’ a + eval’ b

Var name

-> value env name

where eval’ = eval env

112 | Глава 7: Функторы и монады: примеры

7.4 Накопление результата

Рассмотрим по-подробнее тип Writer. Он выполняет задачу обратную к типу Reader. Когда мы пользова-

лись типом Reader, мы могли в любом месте функции извлекать данные из окружения. Теперь же мы будем

не извлекать данные из окружения, а записывать их.

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

прибавляем к накопителю результата единицу за каждый конструктор Add или Mul. Тип сообщений будет

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

Напомню, что тип накопителя должен быть экземпляром класса Monoid:

class Monoid a where

mempty

:: a

mappend :: a -> a -> a

mconcat :: [a] -> a

mconcat = foldr mappend mempty

Но для чисел возможно несколько вариантов, которые удовлетворяют свойствам. Для сложения:

instance Num a => Monoid a where

mempty

= 0

mappend = (+)

И умножения:

instance Num a => Monoid a where

mempty

= 1

mappend = (*)

Для нашей задачи подойдёт первый вариант, но не исключена возможность того, что для другой зада-

чи нам понадобится второй. Но тогда мы уже не сможем определить такой экземпляр. Для решения этой

проблемы в модуле Data.Monoid определено два типа обёртки:

newtype Sum

a = Sum

{ getSum

:: a }

newtype Prod a = Prod { getProd :: a }

В этом определении есть два новых элемента. Первый это ключевое слово newtype, а второй это фигурные

скобки. Что всё это значит?

Тип-обёртка newtype

Ключевое слово newtype вводит новый тип-обёртку. Тип-обёртка может иметь только один конструктор,

у которого лишь одни аргумент. Запись:

newtype Sum a = Sum a

Это тоже самое, что и

data Sum a = Sum a

Единственное отличие заключается в том, что в случае newtype вычислитель не видит разницы между

Sum a и a. Её видит лишь компилятор. Это означает, что на разворачивание и заворачивание такого значения

в тип обёртку не тратится никаких усилий. Такие типы подходят для решения двух задач:

• Более точная проверка типов.

Например у нас есть типы, которые описывают физические величины, все они являются числами, но у

них также есть и размерности. Мы можем написать:

type Velocity

= Double

type Time

= Double

type Length

= Double

velocity :: Length -> Time -> Velocity

velocity leng time = leng / time

Накопление результата | 113

В этом случае мы спокойно можем подставить на место времени путь и наоборот. Но с помощью типов

обёрток мы можем исключить эти случаи:

newtype Velocity

= Velocity

Double

newtype Time

= Time

Double

newtype Length

= Length

Double

velocity :: Length -> Time -> Velocity

velocity (Length leng) (Time time) = Velocity $ leng / time

В этом случае мы проводим проверку по размерностям, компилятор не допустит смешивания данных.

• Определение нескольких экземпляров одного класса для одного типа. Этот случай мы как раз и рас-

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

=> a.

Сделаем две обёртки!

newtype Sum

a = Sum

a

newtype Prod a = Prod a

Тогда мы можем определить два экземпляра для двух разных типов:

Один для Sum:

instance Num a => Monoid (Sum a) where

mempty

= Sum 0

mappend (Sum a) (Sum b) = Sum (a + b)

А другой для Prod:

instance Num a => Monoid (Prod a) where

mempty

= Prod 1

mappend (Prod a) (Prod b) = Prod (a * b)

Записи

Вторая новинка заключалась в фигурных скобках. С помощью фигурных скобок в Haskell обозначаются

записи (records). Запись это произведение типа, но с выделенными именами для полей.

Например мы можем сделать тип для описания паспорта:

data Passport

= Person {

surname

:: String,

-- Фамилия

givenName

:: String,

-- Имя

nationality

:: String,

-- Национальность

dateOfBirth

:: Date,

-- Дата рождения

sex

:: Bool,

-- Пол

placeOfBirth

:: String,

-- Место рождения

authority

:: String,

-- Место выдачи документа

dateOfIssue

:: Date,

-- Дата выдачи

dateOfExpiry

:: Date

-- Дата окончания срока

} deriving (Eq, Show)

--

действия

data Date

= Date {

day

:: Int,

month

:: Int,

year

:: Int

} deriving (Show, Eq)

В фигурных скобках через запятую мы указываем поля. Поле состоит из имени и типа. Теперь нам до-

ступны две операции:

• Чтение полей

hello :: Passport -> String

hello p = ”Hello, ” ++ givenName p ++ ”!”

114 | Глава 7: Функторы и монады: примеры

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

человека и обращаемся к нему по имени. Для того, чтобы узнать его имя мы подсмотрели в паспорт, в

поле givenName.

• Обновление полей. Для обновления полей мы пользуемся таким синтаксисом:

value { fieldName1 = newValue1, fieldName2 = newValue2, ... }

Мы присваиваем в значении value полю с именем fieldName новое значение newFieldValue. К примеру

продлим срок действия паспорта на десять лет:

prolongate :: Passport -> Passport

prolongate p = p{ dateOfExpiry = newDate }

where newDate = oldDate { year = year oldDate + 10 }

oldDate = dateOfExpiry p

Вернёмся к типам Sum и Prod:

newtype Sum

a = Sum

{ getSum

:: a }

newtype Prod a = Prod { getProd :: a }

Этой записью мы определили два типа-обёртки. У нас есть две функции, которые заворачивают обычное

значение, это Sum и Prod. С помощью записей мы тут же в определении типа определили функции которые

разворачивают значения, это getSum и getProd.

Вспомним определение для типа State:

data State s a = State (s -> (a, s))

runState :: State s a -> (s -> (a, s))

runState (State f) = f

Было бы гораздо лучше определить его так:

newtype State s a = State{ runState :: s -> (a, s) }

Накопление чисел

Но вернёмся к нашей задаче. Мы будем накапливать сумму в значении типа Sum. Поскольку нас интере-

сует лишь значение накопителя, наша функция будет возвращать значение единичного типа ().

countBiFuns :: Exp -> Int

countBiFuns = getSum . execWriter . countBiFuns’

countBiFuns’ :: Exp -> Writer (Sum Int) ()

countBiFuns’ x = case x of

Add a b -> tell (Sum 1) *> bi a b

Mul a b -> tell (Sum 1) *> bi a b

Neg a

-> un a

_

-> pure ()

where bi a b = countBiFuns’ a *> countBiFuns’ b

un

= countBiFuns’

tell :: Monoid a => a -> Writer a ()

tell a = Writer ((), a)

execWriter :: Writer msg a -> msg

execWriter (Writer (a, msg)) = msg

Первая функция countBiFuns извлекает значение из типов Writer и Sum. А вторая функция countBiFuns’

вычисляет значение.

Мы определили две вспомогательные функции tell, которая записывает сообщение в накопитель и

execWriter, которая возвращает лишь сообщение. Это стандартные для Writer функции.

Посмотрим как работает эта функция:

*Exp> countBiFuns (n 2)

0

*Exp> countBiFuns (n 2 + n 1 + 2 + 3)

3

Накопление результата | 115

Накопление логических значений

В модуле Data.Monoid определены два типа для накопления логических значений. Это типы All и Any. С

помощью типа All мы можем проверить выполняется ли некоторое свойство для всех значений. А с помощью

типа Any мы можем узнать, что существует хотя бы один элемент, для которых это свойство выполнено.

Посмотрим на определение экземпляров класса Monoid для этих типов:

newtype All = All { getAll :: Bool }

instance Monoid All where

mempty = All True

All x ‘mappend‘ All y = All (x && y)

В типе All мы накапливаем значения с помощью логического “и”. Нейтральным элементом является кон-

структор True. Итоговое значение накопителя будет равно True только в том случае, если все накапливаемые

сообщения были равны True.

В типе Any всё наоборот:

instance Monoid Any where

mempty = Any False

Any x ‘mappend‘ Any y = Any (x || y)

Посмотрим как работают эти типы. Составим функцию, которая проверяет отсутствие оператора минус

в выражении:

noNeg :: Exp -> Bool

noNeg = not . getAny . execWriter . anyNeg

anyNeg :: Exp -> Writer Any ()

anyNeg x = case x of

Neg _

-> tell (Any True)

Add a b -> bi a b

Mul a b -> bi a b

_

-> pure ()

where bi a b = anyNeg a *> anyNeg b

Функция anyNeg проверяет есть ли в выражении хотя бы один конструктор Neg. В функции noNeg мы

извлекаем результат и берём его отрицание, чтобы убедиться в том что в выражении не встретилось ни

одного конструктора Neg.

*Exp> noNeg (n 2 + n 1 + 2 + 3)

True

*Exp> noNeg (n 2 - n 1 + 2 + 3)

False

Накопление списков

Экземпляр класса Monoid определён и для списков. Предположим у нас есть дерево, в каждом узле кото-

рого находятся числа, давайте соберём все числа больше 5, но меньше 10. Деревья мы возьмём из модуля

Data.Tree:

data Tree a

= Node

{ rootLabel :: a

-- значение метки

, subForest :: Forest a

-- ноль или несколько дочерних деревьев

}

type Forest a = [Tree a]

Интересный тип. Тип Tree определён через Forest, а Forest определён через Tree. По этому типу мы

видим, что каждый узел содержит некоторое значение типа a, и список дочерних деревьев.

Составим дерево:

*Exp> :m Data.Tree

Prelude Data.Tree> let t a = Node a []

Prelude Data.Tree> let list a = Node a []

Prelude Data.Tree> let bi v a b = Node v [a, b]

Prelude Data.Tree> let un v a

= Node v [a]

Prelude Data.Tree>

Prelude Data.Tree> let tree1 = bi 10 (un 2 $ un 6 $ list 7) (list 5)

Prelude Data.Tree> let tree2 = bi 12 tree1 (bi 8 tree1 tree1)

116 | Глава 7: Функторы и монады: примеры

Теперь составим функцию, которая будет обходить дерево, и собирать числа из заданного диапазона:

type Diap a = (a, a)

inDiap :: Ord a => Diap a -> Tree a -> [a]

inDiap d = execWriter . inDiap’ d

inDiap’ :: Ord a => Diap a -> Tree a -> Writer [a] ()

inDiap’ d (Node v xs) = pick d v *> mapM_ (inDiap’ d) xs

where pick (a, b) v

| (a <= v) && (v <= b)

= tell [v]

| otherwise

= pure ()

Как и раньше у нас две функции, одна выполняет вычисления, другая извлекает результат из Writer. В

функции pick мы проверяем число на принадлежность интервалу, если это так мы добавляем число к резуль-

тату, а если нет пропускаем его, добавляя нейтральный элемент (в функции pure). Обратите внимание на то

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

пользуется, если результат функции не важен, а важны те действия, которые происходят при преобразовании

списка. В нашем случае это накопление результата. Посмотрим на определение этой функции:

mapM_ :: Monad m => (a -> m b) ->

[a] -> m ()

mapM_ f = sequence_ . map f

sequence_ :: Monad m => [m a] -> m ()

sequence_ = foldr (>> ) (return ())

Основное отличие состоит в функции sequence_. Раньше мы собирали значения в список, а теперь отбра-

сываем их с помощью константной функции >> . В конце мы возвращаем значение единичного типа ().

Теперь сохраним в модуле Tree определение функции и вспомогательные функции создания деревьев

un, bi, и list и посмотрим как наша функция работает:

*Tree> inDiap (4, 10) tree2

[10,6,7,5,8,10,6,7,5,10,6,7,5]

*Tree> inDiap (5, 8) tree2

[6,7,5,8,6,7,5,6,7,5]

*Tree> inDiap (0, 3) tree2

[2,2,2]

7.5 Монада изменяемых значений ST

Возможно читатели, для которых “родным” является один из императивных языков, немного заскучали

по изменяемым значениям. Мы говорили, что в Haskell ничего не изменяется, мы даём всё более и более

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

Но есть алгоритмы, которые очень элегантно описываются в терминах изменяемых значений. Примером

такого алгоритма может быть быстрая сортировка. Задача состоит в перестановке элементов массива так,

чтобы на выходе любой последующий элемент массива был больше предыдущего (для списков эту задачу

решают функции sort и sortBy).

Само по себе явление обновления значения является побочным эффектом. Оно ломает представление о

статичности мира, у нас появляются фазы: до обновления и после обновления. Но представьте, что обнов-

ление происходит локально, мы постоянно меняем только одно значение, при этом за время обновления ни

одна другая переменная не может пользоваться промежуточными значениями и обновления происходят с

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

мять, и при построении результата начинает обновлять значение внутри этой памяти (с помощью чистых

функций) и считать что-то ещё полезное на основе этих обновлений, как только вычисления закончатся, па-

мять стирается, и возвращается значение. Будет ли такая функция чистой? Интуиция подсказывает, что да.

Это было доказано, но для реализации этого требуется небольшой трюк на уровне типов. Получается, что

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

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

Для симуляции обновления значения в Haskell нам нужно решить две проблемы. Как упорядочить обнов-

ление значения? И как локализовать его? В императивных языках порядок вычисления выражений строго

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

числитель читает код как ленту и выполняет выражение за выражением. В Haskell всё совсем по-другому. Мы

можем писать функции в любом порядке, также в любом порядке мы можем объявлять локальные перемен-

ные в where или let-выражениях. Компилятор определяет порядок редукции синонимов по функциональным

Монада изменяемых значений ST | 117

зависимостям. Синоним f не будет раскрыт раньше синонима g только в том случае, если результат g тре-

буется в f. Но с обновлением значения этот вариант не пройдёт, посмотрим на выражение:

fun :: Int -> Int

fun arg =

let mem = new arg

x

= read mem

y

= x + 1

??

= write mem y

z

= read mem

in z

Предполагается, что в этой функции мы получаем значение arg, выделяем память mem c помощью спе-

циальной функции new, которая принимает начальное значение, которое будет храниться в памяти. Затем

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

мяти, сохранив значение в переменной z, и в самом конце возвращаем ответ. Налицо две проблемы: z не

зависит от y, поэтому мы можем считать значение z в любой момент после инициализации памяти и вторая

проблема: что должна возвращать функция write?

Для того чтобы упорядочить эти вычисления мы воспользуемся типом State. Каждое выражение будет

принимать фиктивное состояние и возвращать его. Тогда функция fun запишется так:

fun :: Int -> State s Int

fun arg = State $ \s0 ->

let (mem, s1)

= runState (new arg)

s0

((),

s2)

= runState (write mem arg)

s1

(x,

s3)

= runState (read mem)

s2

y

= x + 1

((),

s4)

= runState (write mem y)

s3

(z,

s5)

= runState (read mem)

s4

in (z, s5)

new

:: a -> State s (Mem a)

write

:: Mem a -> a -> State s ()

read

:: Mem a -> State s a

Тип Mem параметризован типом значения, которое хранится в памяти. В этом варианте мы не можем

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

записать это выражение гораздо короче с помощью методов класса Monad, но мне хотелось подчеркнуть как

передача состояния навязывает порядок вычисления. Функция write теперь возвращает пустой кортеж. Но

порядок не теряется за счёт состояния. Пустой кортеж намекает на то, что единственное назначение функции

write – это обновление состояния.

Однако этого не достаточно. Мы хотим, чтобы обновление значения было скрыто от пользователя в чистой

функции. Мы хотим, чтобы тип функции fun не содержал типа State. Для этого нам откуда-то нужно взять

начальное значение состояния. Мы можем решить эту проблему, зафиксировав тип s. Пусть это будет тип

FakeState, скрытый от пользователя.

module Mutable(

Mutable, Mem, purge,

new, read, write)

where

newtype Mutable a = Mutable (State FakeState a)

data FakeState = FakeState

purge :: Mutable a -> a

purge (Mutable a) = fst $ runState a FakeState

new

:: a -> Mutable (Mem a)

read

:: Mem a -> Mutable a

write

:: Mem a -> a -> Mutable ()

Мы предоставим пользователю лишь тип Mutable без конструктора и функцию purge, которая “очища-

ет” значение от побочных эффектов и примитивные функции для работы с памятью. Также мы определим

экземпляры классов типа State для Mutable, сделать это будет совсем не трудно, ведь Mutable – это просто

118 | Глава 7: Функторы и монады: примеры

обёртка. С помощью этих экземпляров пользователь сможет комбинировать вычисления, которые связаны с

изменением памяти. Пока вроде всё хорошо, но обеспечиваем ли мы локальность изменения значений? Нам

важно, чтобы, один раз начав работать с памятью типа Mem, мы не смогли бы нигде воспользоваться этой па-

мятью после выполнения функции purge. Оказывается, что мы можем разрушить локальность. Посмотрите

на пример:

let mem = purge allocate

in

purge (read mem)

Мы возвращаем из функции purge ссылку на память и спокойно пользуемся ею в другой ветке Mutable-

вычислений. Можно ли этого избежать? Оказывается, что можно. Причём решение весьма элегантно. Мы

можем построить типы Mem и Mutable так, чтобы ссылке на память не удалось просочиться через функцию

purge. Для этого мы вернёмся к общему типу State c двумя параметрами. Причём первый первый параметр

мы прицепим и к Mem:

data

Mem

s a = ..

newtype Mutable s a = ..

new

:: a -> Mutable s (Mem s a)

write

:: Mem s a -> a -> Mutable s ()

read

:: Mem s a -> Mutable s a

Теперь при создании типы Mem и Mutable связаны общим параметром s. Посмотрим на тип функции purge

purge :: (forall s. Mutable s a) -> a

Она имеет необычный тип. Слово forall означает “для любых”. Это слово называют квантором всеобщ-

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

фиктивного состояния. Как дополнительный forall может нам помочь? Функция purge забывает тип фик-

тивного состояния s из типа Mutable, но в случае типа Mem, этот параметр продолжает своё путешествие по

программе в типе значения v :: Mem s a. По типу v компилятор может сказать, что существует такое s,

для которого значение v имеет смысл (правильно типизировано). Но оно не любое! Функцию purge с трю-

ком интересует не некоторый тип, а все возможные типы s, поэтому пример не пройдёт проверку типов.

Компилятор будет следить за “чистотой” наших обновлений.

При таком подходе остаётся вопрос: откуда мы возьмём начальное значение, ведь теперь у нас нет типа

FakeState? В Haskell специально для этого типа было сделано исключение. Мы возьмём его из воздуха. Это

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

ваться. Поскольку у нас нет конструктора Mutable мы никогда не сможем добраться до внутренней функции

типа State и извлечь состояние. Состояние скрыто за интерфейсом класса Monad и отбрасывается в функции

purge.

Тип ST

Выше я пользовался вымышленными типами для упрощения объяснений, на самом деле в Haskell за об-

новление значений отвечает тип ST (сокращение от state transformer). Он живёт в модуле Control.Monad.ST.

Из документации видно, что у него два параметра, и нет конструкторов:

data ST s a

Это наш тип Mutable, теперь посмотрим на тип Mem. Он называется ST-ссылкой и определён в модуле

Data.STRef (сокращение от ST reference). Посмотрим на основные функции:

newSTRef

:: a -> ST s (STRef s a)

readSTRef

:: STRef s a -> ST s a

writeSTRef

:: STRef s a -> a -> ST s ()

Такие функции иногда называют смышлёными конструкторами (smart constructors) они позволяют строить

значение, но скрывают от пользователя реализацию за счёт скрытия конструкторов типа (модуль экспорти-

рует лишь имя типа STRef).

Для иллюстрации этих функций реализуем одну вспомогательную функцию из модуля Data.STRef, функ-

цию обновления значения по ссылке:

modifySTRef :: STRef s a -> (a -> a) -> ST s ()

modifySTRef ref f = writeSTRef . f =<< readSTRef ref

Мы воспользовались тем, что ST является экземпляром Monad. Также как и для State для ST определены

экземпляры классов Functor, Applicative и Monad. Какое совпадение! Посмотрим на функцию purge:

runST :: (forall s. ST s a) -> a

Монада изменяемых значений ST | 119

Императивные циклы

Реализуем for цикл из языка C:

Result s;

for (i = 0 ; i < n; i++)

update(i, s);

return s;

У нас есть стартовое значение счётчика и результата, функция обновления счётчика, предикат останова и

функция обновления результата. Мы инициализируем счётчик и затем обновляем счётчик и состояние до тех

пор пока предикат счётчика не станет ложным. Напишем чистую функцию, которая реализует этот процесс. В

этой функции мы воспользуемся специальным синтаксическим сахаром, который называется do-нотация, не

пугайтесь это всё ещё Haskell, для понимания этого примера загляните в раздел “сахар для монад” главы~17.

module Loop where

import Control.Monad

import Data.STRef

import Control.Monad.ST

forLoop ::

i -> (i -> Bool) -> (i -> i) -> (i -> s -> s) -> s -> s

forLoop i0 pred next update s0 = runST $ do

refI <- newSTRef i0

refS <- newSTRef s0

iter refI refS

readSTRef refS

where iter refI refS = do

i <- readSTRef refI

s <- readSTRef refS

when (pred i) $ do

writeSTRef refI $ next i

writeSTRef refS $ update i s

iter refI refS

Впрочем код выше можно понять если читать его как обычный императивный код. Выражения do-блока

выполняются последовательно, одно за другим. Сначала мы инициализируем два изменяемых значения, для

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

pred. Функция when – это стандартная функция из модуля Control.Monad. Она проверяет предикат, и если

он возвращает True выполняет серию действий, в которых мы записываем обновлённые значения. Обратите

внимание на то, что связка when-do это не специальная конструкция языка. Как было сказано when – это

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

do начинает блок действий (границы блока определяются по отступам), который будет интерпретироваться

как одно действие. В настоящем императивном цикле в обновлении и предикате счётчика может участвовать

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

типе счётчика. Решим типичную задачу, посчитаем числа от одного до десяти:

*Loop> forLoop 1 (<=10) succ (+) 0

55

Посчитаем факториал:

*Loop> forLoop 1 (<=10) succ (*) 1

3628800

*Loop> forLoop 1 (<=100) succ (*) 1

9332621544394415268169923885626670049071596826

4381621468592963895217599993229915608941463976

1565182862536979208272237582511852109168640000

00000000000000000000

Теперь напишем while-цикл:

120 | Глава 7: Функторы и монады: примеры

Result s;

while (pred(s))

update(s);

return s;

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

до тех пор пока предикат не станет ложным.

whileLoop :: (s -> Bool) -> (s -> s) -> s -> s

whileLoop pred update s0 = runST $ do

ref <- newSTRef s0

iter ref

readSTRef ref

where iter ref = do

s <- readSTRef ref

when (pred s) $ do

writeSTRef ref $ update s

iter ref

Посчитаем сумму чисел через while-цикл:

*Loop> whileLoop ((> 0) . fst) (\(n, s) -> (pred n, n + s)) (10, 0)

(0,55)

Первый элемент пары играет роль счётчика, а во втором мы накапливаем результат.

Быстрая сортировка

Реализуем императивный алгоритм быстрой сортировки. Алгоритм быстрой сортировки хорош не только

тем, что он работает очень быстро, но и минимальным расходом памяти. Сортировка проводится в самом

массиве, с помощью обмена элементов местами. Но для этого нам понадобятся изменяемые массивы. Этот

тип определён в модуле Data.Array.ST. В Haskell есть несколько типов изменяемых массивов (как впрочем и

неизменяемых), это связано с различными нюансами размещения элементов в массивах, о которых мы пока

умолчим. Для предостваления общего интерфейса к различным массивам был определён класс:

class (HasBounds a, Monad m) => MArray a e m where

newArray

:: Ix i => (i, i) -> e -> m (a i e)

newArray_ :: Ix i => (i, i) -> m (a i e)

MArray – это сокращение от mutable (изменяемый) array. Метод newArray создёт массив типа a, который

завёрнут в тип-монаду m. Первый аргумент указывает на диапазон значений индексов массива, а вторым

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

в массив элемент undefined.

Посмотрим на вспомогательные классы:

class Ord a => Ix a where

range :: (a, a) -> [a]

index :: (a, a) -> a -> Int

inRange :: (a, a) -> a -> Bool

rangeSize :: (a, a) -> Int

class HasBounds a where

bounds :: Ix i => a i e -> (i, i)

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

и типу вы догадаетесь о назначении методов (можете свериться с интерпретатором на типах Int или (Int,

Int)). Класс HasBounds обозначает массивы размер, которых фиксирован. Но вернёмся к массивам. Мы можем

не только выделять память под массив, но и читать элементы и обновлять их:

readArray

:: (MArray a e m, Ix i) => a i e -> i -> m e

writeArray :: (MArray a e m, Ix i) => a i e -> i -> e -> m ()

В случае ST-ссылок у нас была функция runST. Она возвращала значение из памяти, но что будет возвра-

щать аналогичная функция для массива? Посмотрим на неё:

Монада изменяемых значений ST | 121

freeze :: (Ix i, MArray a e m, IArray b e) => a i e -> m (b i e)

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

IArray обозначает неизменяемые (immutable) массивы. Функцией freeze мы превращаем изменяемый мас-

сив в неизменяемый, но завёрнутый в специальный тип-монаду. В нашем случае этим типом будет ST. В

модуле Data.Array.ST определена специальная версия этой функции:

runSTArray :: Ix i => (forall s . ST s (STArray s i e)) -> Array i e

Здесь Array – это обычный неизменяемый массив. Он живёт в модуле Data.Array мы можем строить

массивы из списков значений, преобразовывать их разными способами, превращать в обратно в списки и

многое другое. Об о всём этом можно узнать из документации к модулю. Обратите на появление слова

forall и в этой функции. Оно несёт тот же смысл, что и в функции runST.

Для тренировки напишем функцию, которая меняет местами два элемента массива:

module Qsort where

import Data.STRef

import Control.Monad.ST

import Data.Array

import Data.Array.ST

import Data.Array.MArray

swapElems :: Ix i => i -> i -> STArray s i e -> ST s ()

swapElems i j arr = do

vi <- readArray arr i

vj <- readArray arr j

writeArray arr i vj

writeArray arr j vi

Протестируем на небольшом массиве:

test :: Int -> Int -> [a] -> [a]

test i j xs = elems $ runSTArray $ do

arr <- newListArray (0, length xs - 1) xs

swapElems i j arr

return arr

Тир функции test ничем не выдаёт её содержание. Вроде функция как функция:

test :: Int -> Int -> [a] -> [a]

Посмотрим на то, как она работает:

*Qsort> test 0 3 [0,1,2,3,4]

[3,1,2,0,4]

*Qsort> test 0 4 [0,1,2,3,4]

[4,1,2,3,0]

Теперь перейдём к сортировке. Суть метода в том, что мы выбираем один элемент массива, называемый

осью (pivot) и переставляем остальные элементы массива так, чтобы все элементы меньше осевого были сле-

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

тех, что находятся слева и справа от осевого элемента и так пока все элементы не отсортируются. В алго-

ритме очень хитрая процедура перестановки элементов, наша задача переставить элементы в массиве, то

есть не пользуясь никакими дополнительными структурами данных. Я не буду говорить как это делается,

просто выпишу код, а вы можете почитать об этом где-нибудь, в любом случае из кода будет понятно как это

происходит:

qsort :: Ord a => [a] -> [a]

qsort xs = elems $ runSTArray $ do

arr <- newListArray (left, right) xs

qsortST left right arr

return arr

where left

= 0

122 | Глава 7: Функторы и монады: примеры

right = length xs - 1

qsortST :: Ord a => Int -> Int -> STArray s Int a -> ST s ()

qsortST left right arr = do

when (left <= right) $ do

swapArray left (div (left + right) 2) arr

vLeft <- readArray arr left

(last, _) <- forLoop (left + 1) (<= right) succ

(update vLeft) (return (left, arr))

swapArray left last arr

qsortST left (last - 1) arr

qsortST (last + 1) right arr

where update vLeft i st = do

(last, arr) <- st

vi <- readArray arr i

if (vi < vLeft)

then do

swapArray (succ last) i arr

return (succ last, arr)

else do

return (last, arr)

Это далеко не самый быстрый вариант быстрой сортировки, но самый простой. Мы просто учимся обра-

щаться с изменяемыми массивами. Протестируем:

*Qsort> qsort ”abracadabra”

”aaaaabbcdrr”

*Qsort> let x = 1000000

*Qsort> last $ qsort [x, pred x .. 0]

-- двадцать лет спустя

1000000

7.6 Краткое содержание

Мы посмотрели на примерах как применяются типы State, Reader и Writer. Также мы познакомились

с монадой изменяемых значений ST. Она позволяет писать в имеративном стиле на Haskell. Мы узнали два

новых элемента пострения типов:

• Типы-обёртки, которые определяются через ключевое слово newtype.

• Записи, они являются произведением типов с именованными полями.

Также мы узнали несколько полезных типов:

Map – хранение значений по ключу (из модуля Data.Map).

Tree – деревья (из модуля Data.Tree).

Array – массивы (из модуля Data.Array).

• Типы для накопления результата (из модуля Data.Monoid).

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

гументов (a -> b -> c) как (a -> (-> ) b c). Тогда тип (-> ) b будет типом с одним параметром, как раз

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

типа Reader. Первый аргумент стрелочного типа b играет роль окружения.

7.7 Упражнения

• Напишите с помощью типа Random функцию игры в кости, два игрока бросают по очереди кости (два

кубика с шестью гранями, грани пронумерованы от 1 до 6). Они бросают кубики 10 раз выигрывает тот,

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

игры: суммарные баллы игроков.

Краткое содержание | 123

• Напишите с помощью типа Random функцию, которая будет создавать случайные деревья заданной

глубины. Значение в узле является случайным числом от 0 до 100, также число дочерних деревьев в

каждом узле случайно, оно изменяется от 0 до 10.

• Опишите в виде конечного автомата поведение амёбы. Амёба может двигаться на плоскости по четырём

направлениям. Если она чувствует свет в определённой стороне, то она ползёт туда. Если по-близости

нет света, она ползает в произвольном направлении. Амёба улавливает интенсивность света, если по

всем четырём сторонам интенсивность одинаковая, она стоит на месте и греется.

• Казалось бы, зачем нам сохранять вычисления в выражениях, почему бы нам просто не вычислить их

сразу? Если у нас есть описание выражения мы можем применить различные техники оптимизации, ко-

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

на аргумент, мы можем выразить это так:

instance Num Exp where

negate (Neg a)

= a

negate x

= Neg x

...

...

Так мы сократили вычисления на две операции. Возможны и более сложные техники оптимизации.

Мы можем учесть ноль и единицу при сложении и умножении или дистрибутивность сложения отно-

сительно умножения.

В этом упражнении вам предлагается провести подобную оптимизацию для логических значений. У

нас есть абстрактное синтаксическое дерево:

data Log

= True

| False

| Not Log

| Or

Log Log

| And Log Log

Напишите функцию, которая оптимизирует выражение Log. Эта функция приводит Log к конъюнктив-

ной нормальной форме (КНФ). Дерево в КНФ обладает такими свойствами: все узлы с Or находятся

ближе к корню чем узлы с And и все узлы с And находятся ближе к корню чем узлы с Not. В КНФ выра-

жения имеют вид:

(True AndNot False AndTrue) ‘OrTrue Or‘ (True AndFalse)

(True AndTrue AndFalse) ‘OrTrue

Как бы мы не шли от корня к листу сначала нам будут встречаться только операции Or, затем только

операции And, затем только Not.

КНФ замечательна тем, что её вычисление может пройти досрочно. КНФ можно представить так:

data Or’

a = Or’

[a]

data And’ a = And’ [a]

data Not’ a = Not’

a

data Lit

= True’ | False’

type CNF = Or’ (And’ (Not’ Lit))

Сначала идёт список выражений разделённых конструктором Or (вычислять весь список не нужно, нам

нужно найти первый элемент, который вернёт True). Затем идёт список выражений, разделённых And

(опять же его не надо вычислять целиком, нам нужно найти первое выражение, которое вернёт False).

В самом конце стоят отрицания.

В нашем случае приведение к КНФ состоит из двух этапов:

Сначала построим выражение, в котором все конструкторы Or и And стоят ближе к корню чем

конструктор Not. Для этого необходимо воспользоваться такими правилами:

-- удаление двойного отрицания

Not (Not a)

==> a

-- правила де Моргана

Not (And a b) ==> Or

(Not a) (Not b)

Not (Or

a b) ==> And (Not a) (Not b)

124 | Глава 7: Функторы и монады: примеры

Делаем так чтобы все конструкторы Or были бы ближе к корню чем конструкторы And. Для этого

мы воспользуемся правилом дистрибутивности:

And a (Or b c)

==> Or (And a b) (And a c)

При этом мы будем учитывать коммутативность And и Or:

And a b

== And b a

Or

a b

== Or

b a

• Когда вы закончите определение функции:

transform :: Log -> CNF

Напишите функцию, которая будет сравнивать вычисление исходного выражения напрямую и вычис-

ление через КНФ. Эта функция будет принимать исходное значение типа Log и будет возвращать два

числа, число операций необходимых для вычисления выражения:

evalCount :: Log -> (Int, Int)

evalCount a = (evalCountLog a, evalCountCNF a)

evalCountLog :: Log -> Int

evalCountLog a = ...

evalCountCNF :: Log -> Int

evalCountCNF a = ...

При написании этих функций воспользуйтесь функциями-накопителями.

• В модуле Data.Monoid определён специальный тип с помощью которого можно накапливать функции.

Только функции должны быть специального типа. Они должны принимать и возвращать значения од-

ного типа. Такие функции называют эндоморфизмами.

Посмотрим на их определение:

newtype Endo a = Endo { appEndo :: a -> a }

instance Monoid (Endo a) where

mempty = Endo id

Endo f ‘mappend‘ Endo g = Endo (f . g)

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

является функция композиции. Попробуйте переписать примеры из главы накопление чисел с помощью

этого типа.

• Реализуйте с помощью монады ST какой-нибудь алгоритм в императивном стиле. Например алгоритм

поиска корней уравнения методом деления пополам. Если функция f непрерывна и в двух точках a и b

( a < b) значения функции имеют разные знаки, то это говорит о том, что где-то на отрезке [ a, b] урав-

нение f( x) = 0 имеет решение. Мы можем найти его так. Посмотрим какой знак у значения функции в

середине отрезка. Если значение равно нулю, то нам повезло и мы нашли решение, если нет, то из двух

концов отрезка выбрем тот, у которого знак значения функции 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

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

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

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

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

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

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

вычисления с помощью какой-нибудь другой функции. Тогда наша программа примет вид:

program =

liftA2 algorithm2 readInit

(liftA2 algorithm1 readInit (readConfig ”file”))

>>= print

-- функции с побочными эффектами

readInit

:: IO Int

readConfig :: String -> IO Config

print

:: Show a => 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, поскольку нас интересует не

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

Закодируем два примера из первого раздела. В первом мы читаем один символ и печатаем его дважды:

Prelude> :m Control.Applicative

Prelude Control.Applicative> let res = (\c -> c:c:[]) <$> getChar >>= print

Prelude Control.Applicative> res

q”qq”

Мы сначала вызываем функцию getChar удваиваем результат функцией \c -> c:c:[] и затем выводим

на экран.

Во втором примере мы дважды запрашиваем символ с клавиатуры а затем печатаем их:

Prelude Control.Applicative> let res = liftA2 (\a b -> a:b:[]) getChar getChar >>= print

Prelude Control.Applicative> res

qw”qw”

8.3 Как пишутся программы

Мы уже умеем читать с клавиатуры и выводить значения на экран. Давайте научимся писать самостоя-

тельные программы. Программа обозначается специальным именем:

main :: IO ()

Если модуль называется Main или в нём нет директивы module ... where и в модуле есть функция main

:: IO (), то после компиляции будет сделан исполняемый файл. Его можно запускать независимо от ghci.

Просто нажимаем дважды мышкой или вызываем из командной строки.

Напишем программу Hello world. Единственное, что она делает это выводит на экран приветствие:

main :: IO ()

main = print ”Hello World!”

Теперь сохраним эти строчки в файле Hello. hs, перейдём в директорию файла и скомпилируем файл:

ghc --make Hello

Появились объектный и интерфейсный файлы, а также появился третий бинарный файл. Это либо Hello

без расширения (в Linux) или Hello. exe (в Windows). Запустим этот файл:

$ ./Hello

”Hello World!”

Получилось! Это наша первая программа. Теперь напишем программу, которая принимает три символа

с клавиатуры и выводит их в обратном порядке:

import Control.Applicative

f :: Char -> Char -> Char -> String

f a b c = reverse $ [a,b,c]

main :: IO ()

main = print =<< f <$> getChar <*> getChar <*> getChar

Сохраним в файле ReverseIO. hs и скомпилируем:

ghc --make ReverseIO -o rev3

Дополнительным флагом -o мы попросили компилятор чтобы он сохранил исполняемый файл под име-

нем rev3. Теперь запустим в командной строке:

$ ./rev3

qwe

”ewq”

Как пишутся программы | 129

Набираем три символа и нажимаем ввод. И программа переворачивает ответ. Обратите внимание на то,

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

двойные кавычки. Для того чтобы выводить строку существует функция putStr. Заменим print на putStr,

перекомпилируем и посмотрим что получится:

$ ghc --make ReverseIOstr -o rev3str

[1 of 1] Compiling Main

( ReverseIOstr.hs, ReverseIOstr.o )

Linking rev3str ...

$ ./rev3str

123

321$

Видно, что после вывода не произошёл перенос каретки, терминал приглашает нас к вводу команды сразу

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

кроме бинарного файла появились ещё два файла с расширениями . hi и . o. Первый файл называется ин-

терфейсным он описывает какие в модуле определения, а второй файл называется объектным. Он содержит

скомпилированный код модуля.

Стоит отметить команду runhaskell. Она запускает программу без создания дополнительных файлов.

Но в этом случае выполнение программы будет происходить медленнее.

8.4 Типичные задачи IO

Вывод на экран

Нам уже встретилось несколько функций вывода на экран. Это функции: print (вывод значения из эк-

земпляра класса Show), putStr (вывод строки) и putStrLn (вывод строки с переносом). Каждый раз когда мы

набираем какое-нибудь выражение в строке интерпретатора и нажимаем Enter, интерпретатор применяет к

выражению функцию print и мы видим его на экране.

Из простейших функций вывода на экран осталось не рассмотренной лишь функция putChar, но я думаю

вы без труда догадаетесь по типу и имени чем она занимается:

putChar :: Char -> IO ()

Функции вывода на экран также можно вызывать в интерпретаторе:

Prelude> putStr ”Hello” >> putChar ’ ’ >> putStrLn ”World!”

Hello World!

Обратите внимание на применение постоянной функции для монад >> . В этом выражении нас интересует

не результат, а те побочные эффекты, которые выполняются при композиции специальных функций. Также

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

Ввод пользователя

Мы уже умеем принимать от пользователя буквы. Это делается функцией getChar. Функцией getLine мы

можем прочитать целую строчку. Строка читается до тех пор пока мы не нажмём Enter.

Prelude> fmap reverse $ getLine

Hello-hello!

”!olleh-olleH”

Есть ещё одна функция для чтения строк, она называется getContents. Основное отличие от getLine

заключается в том, что содержание не читается сразу, а откладывается на потом, когда содержание дей-

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

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

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

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

на потом и будет заниматься им лишь тогда, когда оно понадобится в вычислениях. Это может существенно

снизить расход памяти. Мы читаем файл в 2Гб моментально (мы делаем вид, что читаем его). А на самом

деле сохраняем себе задачу на будущее: читать ввод, когда придёт пора.

130 | Глава 8: IO

Чтение и запись файлов

Для чтения и записи файлов есть три простые функции:

Загрузка...