ДУШКИН Роман Викторович
[email protected] http://roman-dushkin.narod.ru/
ФП 02005-03 01 Джефф Ньюберн (Jeff Newbern)
Взам. инв. № Инв. № дубл.
Подп. и дата
ВСЁ О МОНАДАХ
[email protected]
Инв. № подл.
Подп. и дата
http://www.nomaware.com/monads/html/
2006
Копирова
Формат
АННОТАЦИЯ This tutorial aims to explain the concept of a monad and its application to functional programming in a way that is easy to understand and useful to beginning and intermediate Haskell programmers. Familiarity with the Haskell language is assumed, but no prior experience with monads is required. The tutorial covers a lot of material and the later sections require a thorough understanding of the earlier material. Many code examples are provided along the way to demonstrate monadic programming. It is not advisable to attempt to absorb all of the material in a single reading. The tutorial is arranged in three parts. The first part provides a basic understanding of the role of monads in functional programming, how monads operate, and how they are declared and used in Haskell. The second part covers each standard monad in Haskell, giving the definition of the monad and discussing the use of the monad. The third part covers advanced material relating to monad transformers and real-world issues encountered when programming with monads.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Ключевые слова: монада, стандартные монады, стратегия вычислений.
ФП 02005-03 01 ИзмЛист № докум. Разраб. Душкин Р. Пров. Н. контр. Утв.
Подп. Дата
Лит. Функциональное программирование Копирова
Лист Листов 2 82
Формат
ФП 02005-03 01
СОДЕРЖАНИЕ 1. ПОНЯТИЕ МОНАДЫ..............................................................................................................7 1.1. Введение .............................................................................................................................7 1.1.1. Что такое «монада»?...............................................................................................................7 1.1.2. Зачем пытаться понять монады? ...........................................................................................7
1.2. Знакомство с монадами.....................................................................................................8 1.2.1. 1.2.2. 1.2.3. 1.2.4. 1.2.5.
Конструкторы типов...............................................................................................................8 Из чего состоит монада? ........................................................................................................9 Пример...................................................................................................................................10 Список — тоже монада ........................................................................................................11 Резюме ...................................................................................................................................12
1.3. Работа с классами — классы типов в языке Haskell ....................................................12 1.3.1. 1.3.2. 1.3.3. 1.3.4.
Класс Monad ..........................................................................................................................13 Продолжение примера .........................................................................................................13 Нотация do.............................................................................................................................14 Резюме ...................................................................................................................................15
1.4.1. 1.4.2. 1.4.3. 1.4.4. 1.4.5. 1.4.6.
Три основных закона............................................................................................................16 В случае ошибки ...................................................................................................................16 Монады, не возвращающие значение .................................................................................17 Нулевой элемент и операция «+»........................................................................................18 Резюме ...................................................................................................................................19 Упражнения...........................................................................................................................20
2. СПИСОК СТАНДАРТНЫХ МОНАД ..................................................................................30
2.3. Монада Maybe ..................................................................................................................32
Инв. № подл.
Взам. инв. № Инв. № дубл.
1.5. Поддержка монад в языке Haskell..................................................................................21
Подп. и дата
Подп. и дата
1.4. Законы монад ...................................................................................................................16
1.5.1. В модуле Prelude ...................................................................................................................21 1.5.2. В модуле Monad ....................................................................................................................23 1.5.3. Резюме ...................................................................................................................................29
2.1. Введение ...........................................................................................................................30 2.2. Монада Identity.................................................................................................................31 2.2.1. 2.2.2. 2.2.3. 2.2.4.
Обзор......................................................................................................................................31 Мотивация .............................................................................................................................31 Определение..........................................................................................................................31 Пример...................................................................................................................................32
2.3.1. Обзор......................................................................................................................................32 2.3.2. Мотивация .............................................................................................................................32 2.3.3. Определение..........................................................................................................................32
Лист
ФП 02005-03 01 ИзмЛист № докум.
3
Подп. Дата Копирова
Формат
ФП 02005-03 01
2.3.4. Пример...................................................................................................................................33
2.4. Монада Error ....................................................................................................................33 2.4.1. 2.4.2. 2.4.3. 2.4.4.
Обзор......................................................................................................................................33 Мотивация .............................................................................................................................34 Определение..........................................................................................................................34 Пример...................................................................................................................................35
2.5. Монада List .......................................................................................................................36 2.5.1. 2.5.2. 2.5.3. 2.5.4.
Обзор......................................................................................................................................36 Мотивация .............................................................................................................................36 Определение..........................................................................................................................36 Пример...................................................................................................................................37
2.6. Монада IО.........................................................................................................................38 2.6.1. 2.6.2. 2.6.3. 2.6.4.
Обзор......................................................................................................................................38 Мотивация .............................................................................................................................38 Определение..........................................................................................................................38 Пример...................................................................................................................................40
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
2.7. Монада State .....................................................................................................................41 2.7.1. 2.7.2. 2.7.3. 2.7.4.
Обзор......................................................................................................................................41 Мотивация .............................................................................................................................41 Определение..........................................................................................................................41 Пример...................................................................................................................................42
2.8. Монада Reader .................................................................................................................43 2.8.1. 2.8.2. 2.8.3. 2.8.4.
Обзор......................................................................................................................................43 Мотивация .............................................................................................................................43 Определение..........................................................................................................................44 Пример...................................................................................................................................44
2.9. Монада Writer...................................................................................................................46 2.9.1. 2.9.2. 2.9.3. 2.9.4.
Обзор......................................................................................................................................46 Мотивация .............................................................................................................................46 Определение..........................................................................................................................46 Пример...................................................................................................................................48
2.10. 2.10.1. 2.10.2. 2.10.3. 2.10.4.
Монада Continuation ................................................................................................50 Обзор ................................................................................................................................50 Мотивация .......................................................................................................................50 Определение ....................................................................................................................51 Пример .............................................................................................................................52
3. МОНАДЫ В РЕАЛЬНОМ МИРЕ .........................................................................................53 3.1. Introduction........................................................................................................................53 3.2. Combining monads the hard way ......................................................................................53
Инв. № подл.
3.2.1. Nested Monads .......................................................................................................................53
Лист
ФП 02005-03 01 ИзмЛист № докум.
4
Подп. Дата Копирова
Формат
ФП 02005-03 01
3.2.2. Combined Monads..................................................................................................................54
3.3. Monad transformers...........................................................................................................56 3.3.1. Transformer type constructors................................................................................................56 3.3.2. Lifting .....................................................................................................................................57
3.4. Standard monad transformers ............................................................................................59 3.4.1. The MonadTrans and MonadIO classes .................................................................................59 3.4.2. Transformer versions of standard monads..............................................................................59
3.5. Anatomy of a monad transformer......................................................................................60 3.5.1. Combined monad definition ...................................................................................................60 3.5.2. Defining the lifting function ...................................................................................................61 3.5.3. Functors ..................................................................................................................................61
3.6. More examples with monad transformers .........................................................................61 3.6.1. WriterT with IO ......................................................................................................................62 3.6.2. ReaderT with IO .....................................................................................................................63 3.6.3. StateT with List ......................................................................................................................63
3.7. Managing the transformer stack ........................................................................................67
Взам. инв. № Инв. № дубл.
Подп. и дата
3.7.1. Selecting the correct order ......................................................................................................67 3.7.2. An example with multiple transformers .................................................................................67 3.7.3. Heavy lifting ...........................................................................................................................69
3.8. Continuing Exploration .....................................................................................................72 ПРИЛОЖЕНИЕ A .......................................................................................................................73 ПРИЛОЖЕНИЕ B........................................................................................................................77 Example 1 .................................................................................................................................77 Example 2 .................................................................................................................................77 Example 3 .................................................................................................................................77 Example 4 .................................................................................................................................77 Example 5 .................................................................................................................................77 Example 6 .................................................................................................................................78 Example 7 .................................................................................................................................78 Example 8 .................................................................................................................................78 Example 9 .................................................................................................................................78
Инв. № подл.
Подп. и дата
Example 10 ...............................................................................................................................78 Example 11 ...............................................................................................................................79 Example 12 ...............................................................................................................................79 Example 13 ...............................................................................................................................79 Example 14 ...............................................................................................................................79
Лист
ФП 02005-03 01 ИзмЛист № докум.
5
Подп. Дата Копирова
Формат
ФП 02005-03 01
Example 15 ...............................................................................................................................79 Example 16 ...............................................................................................................................80 Example 17 ...............................................................................................................................80 Example 18 ...............................................................................................................................80 Example 19 ...............................................................................................................................80 Example 20 ...............................................................................................................................80 Example 21 ...............................................................................................................................80 Example 22 ...............................................................................................................................81 Example 23 ...............................................................................................................................81 Example 24 ...............................................................................................................................81 Example 25 ...............................................................................................................................81
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Example 26 ...............................................................................................................................81
Лист
ФП 02005-03 01 ИзмЛист № докум.
6
Подп. Дата Копирова
Формат
ФП 02005-03 01
1. ПОНЯТИЕ МОНАДЫ 1.1. Введение 1.1.1. Что такое «монада»? Монада — это способ структурировать вычисления с точки зрения значений и последовательностей вычислений, использующих эти значения. Монады позволяют программисту строить вычисления, используя последовательные модули, которые сами могут быть последовательностями вычислений. Монада определяет, каким образом вычисления комбинируются для создания новых вычислений, тем самым освобождая программиста от необходимости кодировать комбинацию вручную каждый раз, когда она потребуется. Можно считать монаду стратегией комбинирования вычислений в более сложные вычисления. Например, в стандартной поставке языка Haskell имеется тип Maybe:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
data Maybe a = Nothing | Just a
Этот тип представляет собой тип вычислений, которые не всегда могут выдать результат. Тип Maybe предлагает алгоритм комбинирования вычислений, которые возвращают значения типа Maybe: если комбинированное вычисление состоит из вычисления B, которое зависит от результата вычисления А, тогда комбинированное вычисление должно выдавать Nothing в случае, если А или B возвращают Nothing, и комбинированное вычисление должно выдавать результат вычисления B, применённого к результату вычисления А, в случае если А и В завершились успешно. Существуют также монады для построения вычислений, выполняющих операции ввода/вывода, имеющих состояния или возможность возврата множества результатов и т.д. Различных типов монад так же много, как и стратегий комбинирования вычислений, но некоторые из них настолько хороши и удобны, что были включены в стандартные библиотеки Haskell 98. Эти монады подробно описаны во второй части данного руководства.
1.1.2. Зачем пытаться понять монады? Огромное число различных пособий по изучению монад в сети интернет подтверждают проблематичность осваивания людьми данного понятия. Это объясняется абстрактной природой монад и тем фактом, что они используются в нескольких различных качествах, которые могут внести неясность в определение и назначение монад.
Лист
ФП 02005-03 01 ИзмЛист № докум.
7
Подп. Дата Копирова
Формат
ФП 02005-03 01
В языке Haskell монады играют центральную роль в системе ввода/вывода. Не обязательно разбираться в монадах для использования ввода/вывода в Haskell, но понимание монады IO оптимизирует код и расширяет возможности программиста. Для программиста монады — удобный инструмент для структурирования функциональных программ. Существует 3 свойства монад, которые делают их особенно полезными: 1) Модульность — монады позволяют комбинировать вычисления из более простых вычислений и отделять алгоритм комбинирования от фактического выполнения вычислений.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
2) Гибкость — монады позволяют функциональным программам быть гораздо более адаптивными, чем аналогичные программы, написанные без использования монад. Это происходит благодаря тому, что монады помещают стратегию вычислений в одном-единственном месте программы вместо того, чтобы распределять его по всему коду. 3) Изолированность — монады могут быть использованы для создания императивных вычислительных структур, которые остаются изолированными от основного кода функциональной программы. Это полезно для использования побочных эффектов (таких как ввод/вывод) и состояний (которые нарушают прозрачность ссылок) в таком чистом функциональном языке, как Haskell. К каждой из этих особенностей вернёмся позднее при рассмотрении конкретных монад.
1.2. Знакомство с монадами Мы будем использовать конструктор типов Maybe на протяжении этой главы, поэтому прежде, чем идти дальше, необходимо ознакомиться с определением и использованием типа Maybe.
1.2.1. Конструкторы типов Чтобы понять монады в языке Haskell, необходимо хорошо ориентироваться в конструкторах типов. Конструктор типов — это параметризованное определение типа, используемое с полиморфными типами. Применяя конструктор типов к одному или более конкретным типам, можно сконструировать новый конкретный тип в языке Haskell. В определении Maybe: data Maybe a = Nothing | Just a
Монада Maybe — это конструктор типов, а Nothing и Just — конструкторы данных. Можно задать значение данных, применив конструктор данных Just к значению:
Лист
ФП 02005-03 01 ИзмЛист № докум.
8
Подп. Дата Копирова
Формат
ФП 02005-03 01
country = Just "China"
Аналогично можно создать тип, применив конструктор типов Maybe к типу: lookupAge :: DB -> String -> Maybe Int
Полиморфные типы похожи на контейнеры, которые могут содержать значения многих различных типов. Таким образом, Maybe Int можно считать контейнером типа Maybe, содержащим значение типа Int (или Nothing), а Maybe String — контейнером типа Maybe, содержащим значение типа String (или Nothing). В языке Haskell мы также можем сделать тип контейнера полиморфным, поэтому можно написать «m a» чтобы определить контейнер некоторого типа, содержащий значение некоторого типа! Мы часто используем переменные с конструкторами типов для описания абстрактных особенностей вычисления. Например, полиморфный тип Maybe a — это тип всех вычислений, результатом которых может быть значение или Nothing. Таким образом, можно говорить о свойствах контейнера, не упоминая о том, что контейнер может содержать.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Получение сообщений, подобных «kind errors», от компилятора во время работы с монадами означает некорректное использование конструкторов типов.
1.2.2. Из чего состоит монада? В языке Haskell монада определяется конструктором типов (назовем его m), функцией, формирующей значения данного типа (a -> m a), и функцией, комбинирующей это монадическое значение с вычислениями, которые создают значения данного типа для формирования нового значения данного типа (m a -> (a -> m b) -> m b). Заметьте, что контейнер не меняется, в то время как тип содержимого контейнера может изменяться. Говоря о монадах в целом, принято называть конструктор типов монады m. Функцию, вычисляющую значения данного типа, обычно называют return, а третья функция известна как bind, но записывается как «>>=». Сигнатура функций такова: -- тип монады m data m a = ... -- функция return return :: a -> m a -- bind - функция, связывающая экземпляр монады m a с вычислением, -- которое создает другой экземпляр монады m b из a для создания нового -- экземпляра монады m b (>>=) :: m a -> (a -> m b) -> m b
Лист
ФП 02005-03 01 ИзмЛист № докум.
9
Подп. Дата Копирова
Формат
ФП 02005-03 01
Грубо говоря, конструктор типов монады определяет тип вычисления, функция return создает примитивные значения данного типа вычисления и >>= объединяет вычисления данного типа для проведения более сложных вычислений данного типа. Используя аналогию с контейнерами, можно сказать, что конструктор типов m — это контейнер, который может содержать различные значения. Тип m — это контейнер, содержащий значение типа a. Функция return заносит значение в контейнер монады. Функция >>= берёт значение из контейнера монады и передаёт его функции для создания контейнера монады, содержащего новое значение, возможно, другого типа. Функция известна как bind (связывание), потому что она связывает значение в контейнере монады с первым аргументом функции. При добавлении логических операций в связывающую функцию монада может выполнять определённый алгоритм комбинирования вычислений в монаде. Всё станет понятнее после следующего примера, но если вы чувствуете некоторое замешательство, для начала ознакомьтесь с физической аналогией.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
1.2.3. Пример Предположим, что мы пишем программу, прослеживающую проведение экспериментов по клонированию овец. Конечно, нам захочется знать генетическую историю всех наших овец, поэтому нам понадобятся функции mother и father. Но если овцы клонированы, у них не всегда могут быть и отец, и мать! В нашей реализации на языке Haskell мы опишем возможность отсутствия матери или отца с помощью конструктора типов Maybe: type Sheep = ... father :: Sheep -> Maybe Sheep father = ... mother :: Sheep -> Maybe Sheep mother = ...
Определение функции для нахождения дедушек немного сложнее, потому что необходимо учитывать возможность отсутствия родителя: maternalGrandfather :: Sheep -> Maybe Sheep maternalGrandfather s = case (mother s) of Nothing -> Nothing Just m -> father m
Аналогично для бабушек. Задача становится ещё сложнее, если мы захотим найти прадедов:
Инв. № подл.
mothersPaternalGrandfather :: Sheep -> Maybe Sheep
Лист
ФП 02005-03 01 ИзмЛист № докум.
10
Подп. Дата Копирова
Формат
ФП 02005-03 01
mothersPaternalGrandfather s = case (mother s) of Nothing -> Nothing Just m -> case (father m) of Nothing -> Nothing Just gf -> father gf
Такое описание не только уродливо и непонятно, но и слишком громоздко. Очевидно, что значение Nothing в любой точке вычисления приведёт к Nothing в конечном результате, и гораздо удобнее выполнить данное преобразование один раз и удалить из программы подробные описания проверок case. Данные преобразования упростят создание, чтение и изменение кода программы. Для реализации этих преобразований определим следующий комбинатор: Код содержится в файле example1.hs
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- comb - комбинатор для упорядочивания операций, производимых Maybe comb :: Maybe a -> (a -> Maybe b) -> Maybe b comb Nothing _ = Nothing comb (Just x) f = f x -- теперь можно использовать `comb` для формирования сложных -- последовательностей mothersPaternalGrandfather :: Sheep -> Maybe Sheep mothersPaternalGrandfather s = (Just s) `comb` mother `comb` father `comb` father
Комбинатор отлично работает! Теперь код программы гораздо аккуратнее, с ним легче работать. Заметьте также, что функция comb полностью полиморфна — она никаким образом не специализирована для типа Sheep. Фактически, комбинатор описывает общую стратегию комбинирования вычислений, которые могут не вернуть значение. Таким образом, можно применять данный комбинатор и к другим вычислениям, не всегда возвращающим значение, таким как запросы к базам данных или поиск слов в словарях. Примечательно, что обычный здравый смысл привёл нас к созданию монады без осознания этого. Конструктор типов Maybe, функция Just (работает как return) и наш комбинатор (работает как >>=) вместе составляют простую монаду для формирования вычислений, которые могут не вернуть значение. Для того, чтобы использовать монаду, остается только согласовать её с встроенным в стандартную библиотеку Haskell набором монад. Этот вопрос является предметом обсуждения в следующей главе.
1.2.4. Список — тоже монада Как мы уже видели, конструктор типов Maybe — это монада для формирования вычислений, которые могут не вернуть значение. Возможно, вы удивитесь, узнав, что
Лист
ФП 02005-03 01 ИзмЛист № докум.
11
Подп. Дата Копирова
Формат
ФП 02005-03 01
другой стандартный конструктор типов в Haskell, [] (для конструирования списков), тоже является монадой. Монада списка позволяет сформировать вычисления, которые дают в результате 0, 1 или более значений. Функция return для списков просто создаёт список единичной длины (return x = [x]). Операция связывания для списков создаёт новый список, который содержит результаты применения функции ко всем значениям в первоначальном списке (l >>= f = concatMap f l). Один из случаев использования функций, возвращающих списки, это представление неоднозначных вычислений, имеющих 0, 1 или более возможных результатов. При проведении неоднозначных вычислений неоднозначность может разрешаться в единственный допустимый результат или вообще не иметь допустимого результата. При этом набор возможных вычислительных состояний представляется списком. Таким образом, списковая монада воплощает собой способ представления одновременных вычислений всех возможных альтернатив неоднозначного вычисления.
Взам. инв. № Инв. № дубл.
Подп. и дата
Примеры данного использования списковой монады, а также сравнительные примеры использования монады Maybe будут представлены далее. Но сначала рассмотрим, как наиболее используемые монады определены в языке Haskell.
1.2.5. Резюме Мы выяснили, что монада состоит из конструктора типов, функции, называемой return, и комбинаторной функции, называемой bind или >>=. Эти три элемента инкапсулируют стратегию комбинирования вычислений для составления более сложных вычислений. С помощью конструктора типов Maybe мы определили простую монаду, которая может использоваться для комбинирования вычислений, не всегда возвращающих значение. Заключив стратегию комбинирования вычислений в монаду, мы добились той степени модульности и гибкости, которая недостижима при обычном объединении вычислений. Также мы увидели, что другой стандартный конструктор типов в Haskell, [], является монадой. Списковая монада инкапсулирует алгоритм комбинирования вычислений, которые могут вернуть 0, 1 или множество значений.
Инв. № подл.
Подп. и дата
1.3. Работа с классами — классы типов в языке Haskell Обсуждение в этой главе включает в себя систему классов типов в языке Haskell. Если вы не знакомы с классами в языке Haskell, вам необходимо предварительно изучить их.
Лист
ФП 02005-03 01 ИзмЛист № докум.
12
Подп. Дата Копирова
Формат
ФП 02005-03 01
1.3.1. Класс Monad В языке Haskell существует стандартный класс Monad, который определяет названия и характеристики двух монадических функций return и >>=. Нет строгой необходимости объявлять ваши монады экземплярами класса Monad, но это приветствуется. Haskell располагает специальными средствами поддержки экземпляров класса Monad, поэтому, если ваши монады будут экземплярами класса Monad, это позволит вам использовать эти средства для написания более чистого и красивого кода. Также объявление монад экземплярами класса Monad несёт важную информацию людям, читающим ваш код. Это легко и имеет массу преимуществ, так что пользуйтесь! Стандартное определение класса Monad в Haskell выглядит примерно так: class Monad m where (>>=) :: m a -> (a -> m b) -> m b return :: a -> m a
1.3.2. Продолжение примера
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Возвращаясь к предыдущему примеру, рассмотрим, как конструктор типов Maybe вписывается в систему монад Haskell в качестве экземпляра класса Monad. Напомню, что наша монада Maybe использует конструктор данных Just в качестве функции return, и мы построили простейший комбинатор, исполняющий роль связывающей функции. Мы можем более явно задать Maybe как монаду, объявив её экземпляром класса Monad: instance Monad Maybe where Nothing >>= f = Nothing (Just x) >>= f = f x return = Just
Определив Maybe экземпляром класса Monad, мы можем использовать стандартные операторы для построения сложных вычислений: -- мы можем использовать монадические операции для построения сложных -- последовательностей maternalGrandfather :: Sheep -> Maybe Sheep maternalGrandfather s = (return s) >>= mother >>= father fathersMaternalGrandmother :: Sheep -> Maybe Sheep fathersMaternalGrandmother s = (return s) >>= father >>= mother >>= mother
Монада Maybe определена как экземпляр класса Monad в модуле Prelude языка Haskell, поэтому вам нет необходимости этим заниматься. Другая монада, с которой мы уже познакомились, конструктор списков, также определена как экземпляр класса Monad в стандартной библиотеке. При написании функций, работающих с монадами,
Лист
ФП 02005-03 01 ИзмЛист № докум.
13
Подп. Дата Копирова
Формат
ФП 02005-03 01
постарайтесь использовать класс Monad вместо конкретного экземпляра класса. Функция типа doSomething :: (Monad m) => a -> m b
гораздо более гибкая, чем doSomething :: a -> Maybe b
Первая из упомянутых функций может использоваться со многими типами монад для получения различного результата в зависимости от алгоритма, заключенного в монаде, в то время как последняя ограничивается алгоритмом монады Maybe.
1.3.3. Нотация do
Подп. и дата
Кроме использования стандартных монадических функций, существует другое преимущество принадлежности вашей монады классу Monad — это поддержка языком Haskell нотации do. Любой экземпляр класса Monad может использоваться в блоке do в Haskell. Короче говоря, нотация do позволяет проводить монадические вычисления, используя псевдо-императивные конструкции с именованными переменными. Результат монадического вычисления может быть «присвоен» переменной с помощью оператора левой стрелки <-. Затем использование этой переменной в последующем монадическом вычислении автоматически производит связывание. Тип выражения справа от стрелки — монадический тип m a. Выражение справа от стрелки — это образец, сопоставляемый со значением внутри монады. Например, (x:xs) сопоставим Maybe [1, 2, 3].
Подп. и дата
Взам. инв. № Инв. № дубл.
Вот пример нотации do с использованием монады Maybe: Код содержится в example2.hs -- мы можем также использовать нотацию do для построения сложных -- последовательностей mothersPaternalGrandfather :: Sheep -> Maybe Sheep mothersPaternalGrandfather s = do m <- mother s gf <- father m father gf
Сравните этот вариант с fathersMaternalGrandmother, определённым выше без использования нотации do. Блок do в данном примере записан определённым образом для определения границ блока. Haskell также позволяет использовать фигурные скобки и точки с запятой при определении блока: mothersPaternalGrandfather s = do { m <- mother s; gf <- father m; father gf
Инв. № подл.
}
Лист
ФП 02005-03 01 ИзмЛист № докум.
14
Подп. Дата Копирова
Формат
ФП 02005-03 01
Заметьте, что нотация do имеет сходство с императивным языком программирования, в котором вычисление формируется из определённой последовательности простейших вычислений. В этом отношении монады предлагают возможность создания императивных вычислений в составе функциональной программы. Эта тема будет рассмотрена подробнее при обсуждении побочных эффектов и монады IO позднее. Нотация do — всего лишь синтаксический приём. Нет ничего, что можно сделать с помощью нотации do, и нельзя сделать с помощью стандартных монадических операторов. Но нотация do более удобна в некоторых случаях, особенно при длинной последовательности монадических вычислений. Необходимо понимать и стандартную монадическую связывающую запись, и нотацию do, и уметь применять их в соответствующем случае. Фактически, при приведении нотации do к стандартным монадическим операторам каждое выражение, сопоставленное с образцом, x <- expr1, становится expr1 >>= \x ->
и каждое выражение без означивания, expr2, становится
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
expr2 >>= \_ ->
Все блоки do должны заканчиваться монадическим выражением, клоз let допускается в начале блока do (но клозы let в блоках do не используют ключевое слово in). Определение, описанное выше, может быть преобразовано в следующий код: mothersPaternalGrandfather s =
mother s >>= \m -> father m >>= \gf -> father gf
Теперь становится понятным, почему связывающий оператор так называется. Он используется буквально для связывания значения внутри монады с аргументом последующего λ-выражения.
1.3.4. Резюме Язык Haskell располагает встроенными средствами поддержки монад. Для использования этих средств вам необходимо объявить конструктор типов вашей монады экземпляром класса Monad и предоставить описания функций return и >>= (называемой «связыванием») вашей монады. Монада, являющаяся экземпляром класса Monad, может быть использована с нотацией do, которая является синтаксическим приёмом, обеспечивающим простую императивную запись для описания вычислений с монадами.
Лист
ФП 02005-03 01 ИзмЛист № докум.
15
Подп. Дата Копирова
Формат
ФП 02005-03 01
1.4. Законы монад До этого момента учебное пособие избегало технических подробностей, но есть несколько технических моментов, касающихся монад, которые требуют разъяснения. Монадические операции должны придерживаться законов, известных как «аксиомы монад». Соблюдение данных законов не проверяется компилятором языка Haskell, поэтому обеспечение этого является задачей программиста. Класс Monad в Haskell включает также несколько функций, выходящих за пределы минимального определения, с которыми мы ещё не знакомы. Наконец, многие монады подчиняются дополнительным законам, не входящим в состав стандартных законов монад, и существует дополнительный класс в Haskell для поддержки этих монад.
Подп. и дата
1.4.1. Три основных закона Концепция монады берёт начало в направлении математики, называемом теорией категорий. Хотя нет необходимости в знании теории категорий для создания и использования монад, нам необходимо придерживаться некоторых математических формализмов. Для создания монады недостаточно задать экземпляр класса Monad с корректными характеристиками типов. Для того чтобы монада была составлена правильно, необходимо, чтобы функции return и >>= работали вместе согласно трём правилам: 1) (return x) >>= f == f x 2) m >>= return == m
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
3) (m >>= f) >>= g == m >>= (\x -> f x >>= g) Первое правило требует, чтобы функция return была эквивалентна слева по отношению к >>=. Второе правило требует, чтобы функция return была эквивалентна справа по отношению к >>=. Третье правило представляет собой закон ассоциативности для >>=. Соблюдение этих законов обеспечит непротиворечивость семантики нотации do, использующей монаду. Любой конструктор типов с операторами return и bind, удовлетворяющими трём законам монад, это монада. Компилятор языка Haskell не проверяет соблюдение данных законов для каждого экземпляра класса Monad. Обеспечение соответствия создаваемых монад данным законам — задача программиста.
1.4.2. В случае ошибки Ранее было приведено лишь минимальное определение класса Monad. Полное определение данного класса на самом деле включает ещё две дополнительные функции: fail и >>.
Лист
ФП 02005-03 01 ИзмЛист № докум.
16
Подп. Дата Копирова
Формат
ФП 02005-03 01
Реализация функции fail по умолчанию выглядит так: fail s = error s
Необходимость изменения данного описания функции fail возникает лишь в случае вашего желания обеспечить другой способ обработки ошибки или включить обработку ошибки в вычислительный алгоритм вашей монады. Например, в монаде Maybe так определена функция fail: fail _ = Nothing
Таким образом, функция fail возвращает экземпляр монады Maybe с определённой характеристикой, когда она связывается с другими функциями монады Maybe. Функция fail не является обязательной частью математического определения монады, но она включена в стандартное определение класса Monad из-за роли, которую она исполняет нотации do языка Haskell. Функция fail вызывается каждый раз при неудаче сопоставления с образцом в блоке do:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
fn fn idx
:: Int -> Maybe [Int] = do let l = [Just [1, 2, 3], Nothing, Just [], Just [7..20]] (x:xs) <- l !! idx -- ошибка при сопоставлении с образцом -- вызовет функцию fail return xs
В данном примере fn 0 имеет значение Just [2, 3], но fn 1 и fn 2 имеют значение Nothing. Функция >> — оператор, используемый для связывания монадического вычисления, которое не требует результата предыдущего в последовательности вычисления. Её определение с помощью функции >>= выглядит так: (>>) :: m a -> m b -> m b m >> k = m >>= (\_ -> k)
1.4.3. Монады, не возвращающие значение Вы могли заметить, что из монады, являющейся экземпляром стандартного класса Monad, нельзя извлечь значение. Это не проблема. Ничто не мешает автору монады использовать специальные функции. Например, значения могут быть извлечены из монады с помощью сравнения с образцом Just x или использования функции fromJust. Не требуя подобных функций, класс Monad языка Haskell позволяет создавать монады без обратной связи. В такую монаду значение можно передать через функцию return (а иногда и через функцию fail), а также можно выполнить вычисления внутри монады, используя функции связывания >>= и >>. Однако из такой монады нет возможности извлечь значения.
Лист
ФП 02005-03 01 ИзмЛист № докум.
17
Подп. Дата Копирова
Формат
ФП 02005-03 01
Монада IO — один из примеров монад без обратной связи в языке Haskell. Так как вы не можете выйти из монады IO, невозможно написать функцию, выполняющую вычисление внутри монады IO и при этом не имеющей в своём результате конструктора типов IO. Это значит, что любая функция, результирующий тип которой не содержит конструктор типов IO, гарантированно не использует монаду IO. Другие монады, такие как List и Maybe, возвращают значения. Поэтому возможно написание функций, которые используют эти монады, но при этом возвращают не монадические значения.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Удивительная черта монад без обратной связи состоит в том, что они могут поддерживать побочные эффекты в своих монадических операциях, предотвращая при этом разрушение функциональных свойств немонадических частей программы. Рассмотрим простой пример чтения символа, введенного пользователем. Нельзя написать функцию readChar :: Char, потому что она должна каждый раз при вызове возвращать различный символ, в зависимости от того, что ввёл пользователь. Так как Haskell является чистым функциональным языком, то все функции в нём каждый раз при вызове с одними и теми же аргументами должны возвращать одно и то же значение. Зато можно написать функцию ввода/вывода getChar :: IO Char в монаде IO, потому что она может быть использована лишь в последовательности внутри монады без обратной связи. Нет возможности избавиться от конструктора типов IO в сигнатуре какой-либо функции, использующей его, поэтому конструктор типов IO работает как своего рода тег, определяющий функции, которые выполняют операции ввода/вывода. Более того, такие функции полезны лишь внутри монады IO. Таким образом, монада без обратной связи создаёт изолированный вычислительный процесс, в котором правила чистого функционального языка могут не соблюдаться. Другой стандартный приём при определении монад — это представление монадических значений в качестве функций. Тогда при необходимости получения значения монадического вычисления «выполняется» результирующая монада и выдаёт результат.
1.4.4. Нулевой элемент и операция «+» Кроме трёх основных законов, описанных выше, некоторые монады подчиняются дополнительным правилам. У монад есть специальное значение mzero и оператор mplus, который подчиняется четырём дополнительным законам: 1) mzero >>= f == mzero 2) m >>= (\x -> mzero) == mzero 3) mzero `mplus` m == m 4) m `mplus` mzero == m
Лист
ФП 02005-03 01 ИзмЛист № докум.
18
Подп. Дата Копирова
Формат
ФП 02005-03 01
Легко запомнить законы для mzero и mplus, если сопоставить mzero с 0, mplus с +, и >>= с ∗ в обычной математике. Монады, включающие в себя нулевой элемент и операцию «+», можно объявить экземплярами класса MonadPlus в языке Haskell: class (Monad m) => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a
Продолжая использовать монаду Maybe в качестве примера, можно сказать, что она является экземпляром класса MonadPlus: instance MonadPlus Maybe where mzero = Nothing Nothing `mplus` x = x x `mplus` _ = x
В данном примере Nothing определяется как нулевое значение, а объединение двух значений Maybe даёт в результате то из этих значений, которое не является Nothing. Если оба входных значения равны Nothing, тогда результатом mplus является также Nothing.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
В монаде списка также есть нулевой элемент и операция «+». Нулевым элементом mzero является пустой список, оператором mplus — оператор конкатенации списков ++. Оператор mplus используется для комбинирования монадических значений из различных вычислений в единственное монадическое значение. В контексте нашего примера клонирования овец можно использовать оператор mplus монады Maybe для определения функции, parent s = (mother s) `mplus` (father s), которая вернёт родителя, если такой есть, или значение Nothing, если у овцы вообще нет родителей. Для овцы с двумя родителями функция mplus вернёт одного из них, в зависимости от точного определения mplus в монаде Maybe.
1.4.5. Резюме Экземпляры класса Monad должны подчиняться так называемым законам монад, которые описывают алгебраические свойства монад. Три основных закона определяют, что функция return эквивалентна справа и слева, и связывающий оператор ассоциативен. Невыполнение этих требований приведет к тому, что монады будут неправильно функционировать, и может вызвать неявные проблемы при использовании нотации do. Помимо функций return и >>=, класс Monad определяет другую функцию, fail. Включение функции fail в монаду не является обязательным, но она удобна в использовании и включена в класс Monad, потому что используется нотацией do языка Haskell.
Лист
ФП 02005-03 01 ИзмЛист № докум.
19
Подп. Дата Копирова
Формат
ФП 02005-03 01
Некоторые монады кроме основных трёх законов подчиняются ещё и дополнительным правилам. Одним из важнейших классов таких монад являются монады с нулевым элементом (mzero) и операцией «+» (mplus). Для монад, в которых есть элемент mzero и оператор mplus, используется класс MonadPlus.
1.4.6. Упражнения Этот раздел содержит несколько простых упражнений для шлифования навыков читателя по созданию монад, а также для укрепления понимания понятия функции и использования монад списка и Maybe перед более глубоким изучением монадического программирования. Упражнения построены на предыдущем примере клонирования овец, с которым читатель уже должен быть знаком.
Упражнение 1: Нотация do Перепишите функции maternalGrandfather, fathersMaternalGrandmother, и mothersPaternalGrandfather из примера 2, используя монадические операторы return и >>=, без помощи нотации do.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Упражнение 2: комбинирование монадических значений Напишите функции parent и grandparent с сигнатурой Sheep -> Maybe Sheep. Они должны возвращать одну овцу, отобранную от остальных в соответствии с описанием, или значение Nothing, если такой овцы нет. Подсказка: в данном случае удобно воспользоваться оператором mplus.
Упражнение 3: использование монады списка Напишите функции parent и grandparent с сигнатурой Sheep -> [Sheep]. Они должны возвращать всех овец, соответствующих описанию, или пустой список, если таких овец нет. Подсказка: в данном случае удобно использовать оператор mplus из монады списка. Также можно воспользоваться функцией maybeToList, определенной в модуле Maybe, для конвертации значения из типа Maybe в список.
Упражнение 4: Использование ограничения класса Monad Монады обеспечивают модульность и многократное использование кода, инкапсулируя вычислительные алгоритмы в модули, которые могут использоваться для формирования множества различных вычислений. Монады также способствуют модульности, позволяя вам изменять монаду, в которой происходит вычисление, для получения различных вариантов вычислений. Это достигается написанием полиморфных функций в конструкторе типов монады, используя (Monad m) =>, (MonadPlus m) =>, и другие ограничения классов.
Лист
ФП 02005-03 01 ИзмЛист № докум.
20
Подп. Дата Копирова
Формат
ФП 02005-03 01
Напишите функции parent и grandparent с сигнатурой (MonadPlus m) => Sheep > m Sheep. Их удобно использовать в монадах списка и Maybe. Чем различается поведение функций при использовании их с монадой списка и монадой Maybe?
1.5. Поддержка монад в языке Haskell Встроенные в Haskell средства поддержки монад определены в стандартном модуле Prelude, который экспортирует стандартные монадические функции, и в модуле Monad, который содержит реже используемые функции. Конкретные монадические типы содержатся в собственных библиотеках и описаны во второй части данного руководства.
1.5.1. В модуле Prelude Модуль Prelude содержит описание класса Monad и нескольких вспомогательных функций для работы с монадическими типами данных.
1.5.1.1. Класс Monad
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Мы уже встречались с определением класса Monad: class Monad m where (>>=) :: m a -> (>>) :: m a -> return :: a -> m fail :: String
(a -> m b) -> m b m b -> m b a -> m a
-- Минимальное описание: -- (>>=), return m >> k = m >>= \_ -> k fail s = error s
1.5.1.2. Функции установления последовательности Функция sequence берёт список монадических вычислений, по очереди активирует каждое из них и возвращает список результатов. Если какое-либо из вычислений завершается ошибкой, то и вся функция выдает ошибку: sequence sequence
:: =
Monad m => [m a] -> m [a] foldr mcons (return []) where mcons p q = p >>= \x -> q >>= \y -> return (x:y)
Функция sequence_ (обратите внимание на символ нижней черты) работает так же, как и sequence, но не возвращает список результатов. Она используется в случае, если важны только побочные эффекты монадических вычислений. sequence_ sequence_
:: =
Monad m => [m a] -> m () foldr (>>) (return ())
Лист
ФП 02005-03 01 ИзмЛист № докум.
21
Подп. Дата Копирова
Формат
ФП 02005-03 01
1.5.1.3. Отображающие функции Функция mapM применяет монадическое вычисление ко всем элементам списка и возвращает список результатов. Она определена с помощью функции map и функции sequence, описанной выше: mapM mapM f as
:: =
Monad m => (a -> m b) -> [a] -> m [b] sequence (map f as)
Для данной функции также существует версия с символом нижней черты, mapM_, которая определяется с помощью функции sequence_. Функция mapM_ работает так же, как и mapM, но не возвращает список результатов. Она используется в случае, если важны только побочные эффекты монадических вычислений. mapM_ mapM_ f as
:: =
Monad m => (a -> m b) -> [a] -> m () sequence_ (map f as)
Являясь простым примером использования отображающих функций, функция putString для монады IO может быть определена следующим образом:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
putString putString
:: s
[Char] -> IO () = mapM_ putChar s
Функция mapM может использоваться внутри блока do примерно так же, как функция map обычно используется со списками. Это распространенная ситуация — версия функции для использования внутри монады (т.е. рассчитанная на связывание) имеет сигнатуру, схожую с не монадической версией, но возвращаемые функцией значения находятся внутри монады: -- сравните монадическую и не монадическую сигнатуры map :: (a -> b) -> [a] -> [b] mapM :: Monad m => (a -> m b) -> [a] -> m [b]
1.5.1.4. Функция обратного связывания (=<<) В модуле Prelude также определена функция связывания, которая берёт аргументы в обратном порядке, нежели стандартная связывающая функция. Так как стандартная связывающая функция обозначается «>>=», обратная связывающая функция обозначается «=<<». Она полезна в случае, если связывающий оператор используется в качестве члена более высокого порядка и удобнее представить аргументы в обратном порядке. Её определение очень простое: (=<<) :: Monad m => (a -> m b) -> m a -> m b f =<< x = x >>= f
Лист
ФП 02005-03 01 ИзмЛист № докум.
22
Подп. Дата Копирова
Формат
ФП 02005-03 01
1.5.2. В модуле Monad Модуль Monad в стандартных библиотеках Haskell 98 экспортирует некоторые средства для более сложных монадических операций. Для использования этих средств нужно импортировать модуль Monad в вашу программу на языке Haskell. В данной главе описаны не все функции, представленные в модуле Monad, поэтому приветствуется более подробное самостоятельное изучение данного модуля.
1.5.2.1. Класс MonadPlus В модуле Monad определен класс MonadPlus для монад с нулевым элементом и операцией «+»: class Monad m => MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a
1.5.2.2. Монадические версии списковых функций
Подп. и дата
Однако проще понять принцип работы функции foldM, если рассмотреть её определение, записанное с помощью блока do:
Инв. № подл.
Подп. и дата
Функция foldM — это монадическая версия функции foldl. Определение функции foldM:
Взам. инв. № Инв. № дубл.
В модуле Monad определено несколько функций, обобщающих стандартные функции работы со списками на монады. Функции mapM экспортируются модулем Prelude и описаны выше.
foldM foldM f a [] foldM f a (x:xs)
:: = =
(Monad m) => (a -> b -> m a) -> a -> [b] -> m a return a f a x >>= \y -> foldM f y xs
-- данный код не является допустимым в языке Haskell; -- это всего лишь иллюстрация. foldM f a1 [x1, x2, ..., xn] = do a2 <- f a1 x1 a3 <- f a2 x2 ... f an xn
Связывание справа налево достигается с помощью обращения входного списка перед вызовом функции foldM. Мы можем воспользоваться функцией foldM при создании более мощной функции для составления запросов в нашем примере клонирования овец: Код содержится в файле example3.hs -- traceFamily - общая функция для поиска родственников traceFamily :: Sheep -> [ (Sheep -> Maybe Sheep) ] -> Maybe Sheep
Лист
ФП 02005-03 01 ИзмЛист № докум.
23
Подп. Дата Копирова
Формат
ФП 02005-03 01
traceFamily s l
=
foldM getParent s l where getParent s f = f s
-- используя traceFamily, можно легко составлять сложные запросы mothersPaternalGrandfather s = traceFamily s [mother, father, father] paternalGrandmother s = traceFamily s [father, mother]
Функция traceFamily использует foldM для создания простого способа просмотра родового дерева на любую глубину и в любом направлении. Фактически, проще написать traceFamily s [father, mother], чем использовать функцию paternalGrandmother! Использование функции foldM в блоке do более характерно: Код содержится в файле example4.hs -- Dict - просто конечное отображение из строк в строки type Dict = FiniteMap String String
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- вспомогательная функция, используемая с foldl addEntry :: Dict -> Entry -> Dict addEntry d e = addToFM d (key e) (value e) -- вспомогательная функция, используемая с foldM внутри монады IO addDataFromFile :: Dict -> Handle -> IO Dict addDataFromFile dict hdl = do contents <- hGetContents hdl entries <- return (map read (lines contents)) return (foldl (addEntry) dict entries) -- эта программа формирует словарь из записей файлов, названных в -- командной строке, и затем печатает его в виде ассоциированного -- списка main :: IO () main = do files <- getArgs handles <- mapM openForReading files dict <- foldM addDataFromFile emptyFM handles print (fmToList dict)
Функция filterM работает внутри монады как списковая функция filter. Она берёт предикатную функцию, которая возвращает в монаду значение типа Boolean, и список значений. Функция filterM возвращает в монаду список тех значений, для которых значение предиката равно True. filterM filterM p [] filterM p (x:xs)
:: = =
Monad m => (a -> m Bool) -> [a] -> m [a] return [] do b <- p x ys <- filterM p xs return (if b then (x:ys) else ys)
Лист
ФП 02005-03 01 ИзмЛист № докум.
24
Подп. Дата Копирова
Формат
ФП 02005-03 01
Вот пример того, как функция filterM может быть использована внутри монады IO для выделения из списка директорий: Код содержится в файле example5.hs import Monad import Directory import System -- Обратите внимание: doesDirectoryExist имеет тип FilePath -> IO Bool -- эта программа печатает только те директории, которые названы -- в командной строке main :: IO () main = do names <- getArgs dirs <- filterM doesDirectoryExist names mapM_ putStrLn dirs
Взам. инв. № Инв. № дубл.
Подп. и дата
Функция zipWithM — это монадическая версия списковой функции zipWith. Функция zipWithM_ работает аналогично, но не возвращает результат. Она используется в случае, если важны только побочные эффекты монадических вычислений. zipWithM zipWithM f xs ys
:: =
(Monad m) => (a -> b -> m c) -> [a] -> [b] -> m [c] sequence (zipWith f xs ys)
zipWithM_ zipWithM_ f xs ys
:: =
(Monad m) => (a -> b -> m c) -> [a] -> [b] -> m () sequence_ (zipWith f xs ys)
1.5.2.3. Условные монадические вычисления Существует две функции, обеспечивающие условное выполнение монадических вычислений. Функция when берёт аргумент типа Boolean и монадическое вычисление с модулем типа () и производит вычисление только в том случае, если значение аргумента типа Boolean равно True. Функция unless работает аналогично, но она производит вычисление, пока значение типа Boolean не равно True. when when p s
:: =
(Monad m) => Bool -> m () -> m () if p then s else return ()
unless unless p s
:: =
(Monad m) => Bool -> m () -> m () when (not p) s
Инв. № подл.
Подп. и дата
1.5.2.4. Функция ap и втягивающие функции Втягивание — это монадическая операция конвертации немонадической функции в эквивалентную функцию, которая оперирует монадическими значениями. Говорят, что функция «втянута в монаду» операторами втягивания. Втянутая функция используется для оперирования монадическими значениями вне блока do, а также для сокращения кода внутри блока do.
Лист
ФП 02005-03 01 ИзмЛист № докум.
25
Подп. Дата Копирова
Формат
ФП 02005-03 01
liftM — это простейший втягивающий оператор, который втягивает в монаду функцию одного аргумента. liftM liftM f
:: =
(Monad m) => (a -> b) -> (m a -> m b) \a -> do { a' <- a; return (f a') }
Втягивающие операторы также определены и для функций с большим количеством аргументов. Например, liftM2 втягивает функции двух аргументов: liftM2 :: liftM2 f =
(Monad m) => (a -> b -> c) -> (m a -> m b -> m c) \a b -> do { a' <- a; b' <- b; return (f a' b') }
Аналогично определяются функции большего количества аргументов. Функции до liftM5 определены в модуле Monad. Для того чтобы увидеть, каким образом втягивающие операторы сокращают код, рассмотрим вычисление в монаде типа Maybe с использованием функции swapNames :: String -> String. Можно сделать так:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
getName :: String -> Maybe String getName name = do let db = [("John", "Smith, John"), ("Mike", "Caine, Michael")] tempName <- lookup name db return (swapNames tempName)
Но с помощью функции liftM мы можем использовать liftM swapNames как функцию типа Maybe String -> Maybe String: Код содержится в файле example6.hs getName :: String -> Maybe String getName name = do let db = [("John", "Smith, John"), ("Mike", "Caine, Michael")] liftM swapNames (lookup name db)
Разница ещё более заметна при использовании втягивающих функций с большим количеством аргументов. Втягивающие функции позволяют создание очень компактных конструкций с использованием функций более высоких порядков. Для понимания следующего примера вам, возможно, потребуется ознакомиться с монадическими функциями для монады списка (в частности >>=). Представьте, как вы смогли бы реализовать данную функцию без втягивания оператора: Код содержится в файле example7.hs ------
allCombinations возвращает список, содержащий результат применения бинарного оператора ко всем комбинациям элементов данных списков Например, результатом allCombinations (+) [[0, 1], [1, 2, 3]] будет [0 + 1, 0 + 2, 0 + 3, 1 + 1, 1 + 2, 1 + 3], или [1, 2, 3, 2, 3, 4]
Лист
ФП 02005-03 01 ИзмЛист № докум.
26
Подп. Дата Копирова
Формат
ФП 02005-03 01
-- и результатом allCombinations (*) [[0, 1], [1, 2], [3, 5]] будет -- [0 * 1 * 3, 0 * 1 * 5, 0 * 2 * 3, 0 * 2 * 5, 1 * 1 * 3, 1 * 1 * 5, -- 1 * 2 * 3, 1 * 2 * 5], или [0, 0, 0, 0, 3, 5, 6, 10] allCombinations :: (a -> a -> a) -> [[a]] -> [a] allCombinations fn [] = [] allCombinations fn (l:ls) = foldl (liftM2 fn) l ls
Существует функция ap, которую иногда удобнее использовать, нежели втягивающие функции. Функция представляет собой функциональный аппликативный оператор ($), втянутый в монаду: ap ap
:: =
(Monad m) => m (a -> b) -> m a -> m b liftM2 ($)
Обратите внимание, что liftM2 f x y эквивалентно return f `ap` x `ap` y, и так далее для функций с бо́льшим количеством аргументов. Функция ap удобна для работы с функциями более высоких порядков и монадами.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Результат работы функции ap зависит от алгоритма монады, в которой она используется. Поэтому, например, [( * 2), ( + 3)] `ap` [0, 1, 2] эквивалентно [0, 2, 4, 3, 4, 5] и (Just ( * 2)) `ap` (Just 3) эквивалентно Just 6. Вот простой пример того, насколько удобна функция ap при проведении вычислений более высоких порядков: Код содержится в файле example8.hs -- lookup the commands and fold ap into the command list to -- compute a result. main :: IO () main = do let fns = [("double", (2 * )), ("halve", ( `div` 2)), ("square", (\x -> x * x)), ("negate", negate), ("incr", ( + 1)), ("decr", ( + (-1)))] args <- getArgs let val = read (args !! 0) cmds = map ((flip lookup) fns) (words (args!!1)) print $ foldl (flip ap) (Just val) cmds
1.5.2.5. Функции, используемые с MonadPlus В модуле Monad определены две функции, которые используются с монадами, имеющими нулевой элемент и операцию «+». Первая из них — это функция msum, аналогичная функции sum, определённой для списка чисел. Функция msum оперирует списками монадических значений и применяет оператор mplus к списку, используя mzero как начальное значение:
Инв. № подл.
msum
::
MonadPlus m => [m a] -> m a
Лист
ФП 02005-03 01 ИзмЛист № докум.
27
Подп. Дата Копирова
Формат
ФП 02005-03 01
msum xs
=
foldr mplus mzero xs
В монаде списка функция msum эквивалентна функции concat. В монаде Maybe функция msum возвращает первое в списке значение, не равное Nothing. Аналогично, поведение функции msum в других монадах зависит от природы определений их нулевых элементов (mzero) и функций «+» (mplus). Функция msum позволяет более лаконично определить многие рекурсивные функции. Например, в монаде Maybe можно написать следующее: Код содержится в файле example9.hs type Variable type Value type EnvironmentStack
= = =
String String [[(Variable, Value)]]
-- Функция lookupVar извлекает значение переменной из стека. -- она использует функцию msum в монаде Maybe для получения -- первого значения, не равного Nothing lookupVar :: Variable -> EnvironmentStack -> Maybe Value lookupVar var stack = msum $ map (lookup var) stack
Взам. инв. № Инв. № дубл.
Подп. и дата
вместо этого: lookupVar lookupVar var [] lookupVar var (e:es)
:: = =
Variable -> EnvironmentStack -> Maybe Value Nothing let val = lookup var e in maybe (lookupVar var es) Just val
Вторая функция, используемая с монадами, имеющими нулевой элемент и функцию «+», это guard: guard guard p
:: =
MonadPlus m => Bool -> m () if p then return () else mzero
Ключ к пониманию этой функции заключается в законе монад с нулевым элементом и функцией «+», который гласит: mzero >>= f == mzero. При включении функции guard в последовательность монадических операций любое вычисление, охранное выражение в котором принимает значение False, возвращает mzero. Это аналогично тому, как в случае работы со списками действует охранное выражение l == []. Следующий пример демонстрирует использование функции guard в монаде Maybe:
Инв. № подл.
Подп. и дата
Код содержится в файле example10.hs data Record type DB
= =
Rec {name :: String, age :: Int} deriving Show [Record]
-- Функция getYoungerThan возвращает все записи о людях -- младше определенного возраста. -- она использует функцию guard для исключения записей о людях,
Лист
ФП 02005-03 01 ИзмЛист № докум.
28
Подп. Дата Копирова
Формат
ФП 02005-03 01
-- возраст которых равен или больше заданного предела. -- Этот пример создан чисто в демонстрационных целях. -- В реальной жизни было бы проще -- воспользоваться фильтром. Функция guard становится полезнее, если -- критерии отбора более сложные. getYoungerThan :: Int -> DB -> [Record] getYoungerThan limit db = mapMaybe (\r -> do guard (age r < limit) return r) db
1.5.3. Резюме
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Стандартные библиотеки языка Haskell содержат некоторые функции для работы с монадами. Класс Monad и наиболее часто используемые функции определены в модуле Prelude. Класс MonadPlus и реже используемые (но вс` же очень полезные!) функции определены в модуле Monad. Множество других типов в библиотеках языка Haskell объявлены в соответствующих модулях экземплярами классов Monad и MonadPlus.
Лист
ФП 02005-03 01 ИзмЛист № докум.
29
Подп. Дата Копирова
Формат
ФП 02005-03 01
2. СПИСОК СТАНДАРТНЫХ МОНАД 2.1. Введение Монады, описанные во 2-ой части, включают смесь стандартных типов Haskell, которые являются монадами, так же как и классы монад из библиотеки шаблонных монад Энди Гилла. Библиотека шаблонных монад включена в иерархические библиотеки Glasgow Haskell Compiler под названием «Управление.Монада». Некоторая документация для этих монад взята из великолепного Haskell Wiki. В дополнении к описанным здесь монадам, монады находятся в многих других местах в языке Haskell, таких как, например, унарный объединитель Parsec, делающий грамматический разбор библиотеки. Эти монады выходят за пределы рассмотрения этой статьи, но сами по себе они тщательно изучены. Например, вы можете увидеть часть библиотеки Parsec, посмотрев в исходный код примера 16. Таблица 1. Список рассматриваемых монад
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Монада
Тип вычисления
Объединённая стратегия для >>=
Identity
N/A — используется преобразователями монад.
Связанная функция применена к введённой величине.
Maybe
Вычисления, которые могут не возвращать результат.
Nothing на входе даёт Nothing на выходе. Just x на входе использует x как значение, введённое в
Вычисления, которые могут изменять или отбрасывать исключения.
Неудача записывает информацию, описывающую неудачу. Связь передаёт информацию о неудаче без выполнения связанной функции или использует удачные значения для ввода в связанную функцию.
Неопределённые вычисления, которые могут возвращать сложные возможные результаты.
Записывает связанную функцию во вводимый список и проводит процедуру конкатенации над результирующими списками, чтобы получить список всевозможных результатов при всевозможных введённых величинах.
Вычисления, которые выполняют ввод/вывод.
Последовательное выполнение действий ввода/вывода в порядке связывания.
Вычисления, которые поддерживают состояние.
Связанная функция приложена к введённой величине, чтобы создать переходную функцию состояния, которая применяется к вводимому состоянию.
Reader
Вычисления, которые считываются из разделённой среды.
Связанная функция применена к величине вводимых данных, использующих ту же среду.
Writer
Вычисления, которые записывают данные в дополнении к вычисляемым величинам.
Записанные данные поддерживаются отдельно от величин. Связанная функция применяется к вводимой величине и всё, что она записывает, прибавляется к записанному потоку данных.
Error
[] (List)
IO State
связанную функцию.
Лист
ФП 02005-03 01 ИзмЛист № докум.
30
Подп. Дата Копирова
Формат
ФП 02005-03 01
Монада
Тип вычисления
Cont
Вычисления, которые могут быть остановлены и возобновлены.
Объединённая стратегия для >>= Связанная функция включена в возобновленную цепочку.
2.2. Монада Identity 2.2.1. Обзор Таблица 2. Свойства монады Identity Тип вычисления:
Простое функциональное применение.
Стратегия связывания:
Связанная функция применяется к введённой величине. Identity x >>= f == Identity (f x)
Полезна для:
Монады могут быть получены из преобразователей монад, примененных к монаде Identity.
Нуль и плюс:
Нет. Identity a
Пример:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
2.2.2. Мотивация Монада Identity является монадой, которая не олицетворяет какую-либо вычислительную стратегию. Она просто применяет граничную функцию к вводимым величинам без какой-либо модификации. С точки зрения вычисления, нет причины использовать монаду Identity вместо наиболее простого способа применения функции к её аргументам. Цель монады Identity — фундаментальная роль в теории преобразователей монад (описанная в части 3). Любой преобразователь монад, применённый к монаде Identity, создаёт непреобразованную версию этой монады.
2.2.3. Определение newtype Identity a = Identity { runIdentity :: a } instance Monad Identity where return a = (Identity x) >>= f =
Identity a f x
-- i.e. return = id -- i.e. x >>= f = f x
Метка runIdentity использована в определении типа, поскольку из этого следует стиль определения монады, который явно представляет величины монады в виде вычислений. В этом стиле унарное вычисление построено с использованием унарных операторов, и затем величина вычисления извлечена с использованием функции runIdentity. Поскольку монада Identity не выполняет какого-либо вычисления, её определение является тривиальным. Для лучшего примера этого стиля монады смотрите монаду State.
Лист
ФП 02005-03 01 ИзмЛист № докум.
31
Подп. Дата Копирова
Формат
ФП 02005-03 01
2.2.4. Пример Типичное использование монады Identity состоит в получении монады от преобразователя монады. -- получение монады State, используя преобразователь монады StateT type State s a = StateT s Identity a
2.3. Монада Maybe 2.3.1. Обзор
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Таблица 3. Свойства монады Maybe Тип вычисления:
Вычисления, которые могут возвращать Nothing.
Стратегия связывания:
Величины Nothing обходят связанную функцию, а другие величины используются, как введённые значения в связанную функцию.
Полезна для:
Построение вычислений из последовательностей функций, которые могут возвращать Nothing. Хорошими примерами являются сложные запросы к базе данных или словарные поиски.
Нуль и плюс:
Nothing — это нуль. Операция «плюс» возвращает первое ненулевое значение(не Nothing) или значение Nothing, если оба введённых значения были Nothing. Maybe a
Пример:
2.3.2. Мотивация Монада Maybe олицетворяет стратегию объединения цепи вычислений, которая может возвращать Nothing путём раннего завершения цепочки, если при любом шаге на выходе получается Nothing. Это полезно, когда вычисление влечёт за собой последовательность шагов, которые зависят друг от друга, и в которых некоторые шаги не могут возвращать значение. Если Вы когда-либо начинаете писать код подобно этому: case ... of Nothing -> Nothing Just x -> case ... of Nothing -> Nothing Just y -> ...
вам необходимо будет рассмотреть унарные свойства монады Maybe для улучшения кода.
2.3.3. Определение
Инв. № подл.
data Maybe a = Nothing | Just a
Лист
ФП 02005-03 01 ИзмЛист № докум.
32
Подп. Дата Копирова
Формат
ФП 02005-03 01
instance Monad Maybe where return = fail = Nothing >>= f = (Just x) >>= f =
Just Nothing Nothing f x
instance MonadPlus Maybe where mzero = Nothing Nothing `mplus` x = x x `mplus` _ = x
2.3.4. Пример Общий пример состоит в объединении словарных поисков. Даны словарь, который отображает полные имена, чтобы пересылать по адресам, другой словарь, который отображает прозвища, чтобы пересылать по адресам, и третий, который отображает почтовые адреса в пересылочные предпочтения; вы могли бы создать функцию, которая находит предпочтения человека, основываясь на полном имени или на прозвище.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Код, имеющийся в example11.hs data MailPref data MailSystem
= =
HTML | Plain ...
getMailPrefs getMailPrefs sys name let
:: MailSystem -> String -> Maybe MailPref = do nameDB = fullNameDB sys nickDB = nickNameDB sys prefDB = prefsDB sys addr <- (lookup name nameDB) `mplus` (lookup name nickDB) lookup addr prefDB
2.4. Монада Error 2.4.1. Обзор Таблица 4. Свойства монады Error Тип вычисления:
Вычисления, которые могут изменять или отбрасывать исключения.
Стратегия связывания:
Неудача записывает информацию о причине или размещении неудачи. Неудачные значения обходят стороной связанную функцию, а другие величины используются как входные данные в связанную функцию.
Полезна для:
Построение вычислений из последовательности функций, которые могут терпеть неудачу или использование исключения, имеющего дело со структурой обработки ошибок.
Лист
ФП 02005-03 01 ИзмЛист № докум.
33
Подп. Дата Копирова
Формат
ФП 02005-03 01
Нуль и плюс:
Нуль представлен пустой ошибкой, а операция «плюс» исполняет роль второго аргумента, если первый терпит неудачу. Either String a
Пример:
2.4.2. Мотивация Монада Error (также её называют монадой Exception) олицетворяет стратегию объединённых вычислений, которые могут отбрасывать исключения с помощью обхода связанной функции от точки, где отброшено исключение, к точке, которая им управляет. Класс MonadError работает над типом информации об ошибке и конструктором типа монады. Распространено использование Either String, как конструктора типа монады для монады Error, где описания ошибки имеют форму строк. В этом случае и многих других распространённых случаях результирующая монада уже определена как пример класса MonadError. Вы можете также определить ваш собственный тип ошибки и/или использовать конструктор типа монады, отличные от Either String или Either IOError. В этих случаях вам придется точно определить экземпляры классов Error и/или MonadError.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
2.4.3. Определение Описание класса MonadError ниже использует мультипараметрические типовые классы и funDeps, которые являются языковыми расширениями, не найденными в стандартном Haskell 98. Вам не нужно понимать их, чтобы увидеть преимущество класса MonadError. class Error a where noMsg :: strMsg ::
a String -> a
class (Monad m) => MonadError e m | m -> e where throwError :: e -> m a catchError :: m a -> (e -> m a) -> m a
Функция throwError используется в унарных вычислениях, чтобы начать обработку исключений. Функция catchError связывает управляющую функцию, чтобы управлять предыдущими ошибками и возвращаться к нормальному выполнению. Общая идиома: do { action1; action2; action3 } `catchError` handler
где функции action могут вызвать throwError. Заметьте, что handler и создатель блоков должны иметь одинаковый возвращаемый тип. Определение типа конструктора Either e, как примера класса MonadError, является правильным. Опишем следующие условия: Left используется для ошибочных значений, а Right используется для правильных значений.
Инв. № подл.
instance MonadError (Either e) where
Лист
ФП 02005-03 01 ИзмЛист № докум.
34
Подп. Дата Копирова
Формат
ФП 02005-03 01
throwError (Left e) `catchError` handler a `catchError` _
= = =
Left handler e a
2.4.4. Пример Это пример, который демонстрирует использование обычного типа данных Error с использованием механизмов исключений throwError и catchError. В примере пытаются разобрать шестнадцатеричные числа и отбросить исключения, если встречается неправильный символ. Мы используем обычный тип данных Error, чтобы записать размещение разбирающейся ошибки. Исключения отыскиваются путём вызванной функции и сопровождаемым печатью информативным сообщением об ошибке. Код, имеющийся в example12.hs -- Это тип нашего представления разбираемой ошибки. data ParseError = Err {location::Int, reason::String}
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- Мы делаем её экземпляром класса Error instance Error ParseError where noMsg = Err 0 "Parse Error" strMsg s = Err 0 s -- Для нашего конструктора типа монады мы используем Either ParseError, -- которая выдает неудачный результат при использовании Left ParseError или -- удачный результат типа а, используя Right a. type ParseMonad = Either ParseError -- parseHexDigit пытается превратить единственную шестнадцатиричную цифру в -- целое в монаде ParseMonad и отбросить ошибку с неправильным символом parseHexDigit :: Char -> Int -> ParseMonad Integer parseHexDigit c idx = if isHexDigit c then return (toInteger (digitToInt c)) else throwError (Err idx ("Invalid character '" ++ [c] ++ "'")) -- parseHex переводит строку, состоящую из шестнадцатиричных чисел в -- целое в монаде ParseMonad. Разбираемая ошибка из parseHexDigit -- вызовет исключительный возврат из parseHex. parseHex :: String -> ParseMonad Integer parseHex s = parseHex' s 0 1 where parseHex' [] val _ = return val parseHex' (c:cs) val idx = do d <- parseHexDigit c idx parseHex' cs ((val * 16) + d) (idx + 1) -- toString преобразовывает целое в строку в монадеParseMonad toString :: Integer -> ParseMonad String
Лист
ФП 02005-03 01 ИзмЛист № докум.
35
Подп. Дата Копирова
Формат
ФП 02005-03 01
toString n
=
return $ show n
-- Преобразование строки, состоящей из шестнадцатиричного представления -- числа в строку, содержащую десятичное представление этого числа. -- Разбираемая ошибка во введённой строке будет создавать -- описание сообщения об ошибке, как строку вывода. convert :: String -> String convert s = let (Right str) = do {n <- parseHex s; toString n} `catchError` printError in str where printError e = return $ "At index " ++ (show (location e)) ++ ":" ++ (reason e)
2.5. Монада List 2.5.1. Обзор
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Таблица 5. Свойства монады List Тип вычисления:
Вычисления, которые могут возвращать 0, 1, или более возможных результатов.
Стратегия связывания:
Связанная функция применена ко всем возможным величинам во входном списке, и результирующие списки конкатенированы, чтобы произвести список всех возможных результатов.
Полезна для:
Построение вычислений из последовательностей недетерминированных операций. Общим примером являются неоднозначные грамматики синтаксического анализа.
Нуль и плюс:
[] — нуль и ++ — операция «+». [a]
Пример:
2.5.2. Мотивация Монада List включает стратегию объединения цепи недетерминированных вычислений путём приложения операций ко всем возможным величинам на каждом шаге. Это полезно, когда вычисления имеют дело с неоднозначностью. В этом случае допускаются все возможности, которые нужно изучить для разрешения неоднозначности.
2.5.3. Определение instance Monad [] where m >>= f = concatMap f m return x = [x] fail s = [] instance MonadPlus [] where mzero = []
Лист
ФП 02005-03 01 ИзмЛист № докум.
36
Подп. Дата Копирова
Формат
ФП 02005-03 01
mplus
=
(++)
2.5.4. Пример Канонический пример использования монады List — для синтаксического анализа неоднозначных грамматик. Пример ниже показывает просто небольшой пример синтаксического преобразования данных в шестнадцатеричные величины, десятичные величины и слова, содержащие только буквенно-числовые символы. Заметьте, что шестнадцатеричные цифры перекрывают как десятичные цифры так и буквенно-числовые символы, приводящие к неоднозначной грамматике. «Мёртвый» — это как правильная шестнадцатеричная величина, так и слово, например, «10» — как десятичная величина 10, так и шестнадцатеричная величина 16. Код, имеющийся в example13.hs
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- мы можем выполнить грамматический разбор трёх разных типов условий data Parsed = Digit Integer | Hex Integer | Word String deriving Show -- попытки добавить символ к грамматически разобранному представлению -- шестнадцатеричного числа parseHexDigit :: Parsed -> Char -> [Parsed] parseHexDigit (Hex n) c = if isHexDigit c then return (Hex ((n*16) + (toInteger (digitToInt c)))) else mzero parseHexDigit _ _ = mzero -- попытки добавить символ к грамматически разобранному представлению -- десятичного числа parseDigit :: Parsed -> Char -> [Parsed] parseDigit (Digit n) c = if isDigit c then return (Digit ((n*10) + (toInteger (digitToInt c)))) else mzero parseDigit _ _ = mzero -- попытки добавить символ к грамматически разобранному представлению слова parseWord :: Parsed -> Char -> [Parsed] parseWord (Word s) c = if isAlpha c then return (Word (s ++ [c])) else mzero parseWord _ _ = mzero
Лист
ФП 02005-03 01 ИзмЛист № докум.
37
Подп. Дата Копирова
Формат
ФП 02005-03 01
-- пытаемся выполнять грамматический разбор цифры как шестнадцатеричной -- величины, десятичной величины и слова; результатом является список -- возможных разборов parse :: Parsed -> Char -> [Parsed] parse p c = (parseHexDigit p c) `mplus` (parseDigit p c) `mplus` (parseWord p c) -- выполняет грамматический разбор целой -- разобранных величин parseArg :: String -> [Parsed] parseArg s = do init <- (return (return (return foldM parse init
строки и возвращает список возможных
(Hex 0)) `mplus` (Digit 0)) `mplus` (Word "")) s
2.6. Монада IО 2.6.1. Обзор
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Таблица 6. Свойства монады IO Тип вычисления:
Вычисления, которые выполняют ввод/вывод.
Стратегия связывания:
Действия ввода/вывода выполнены в том порядке, в котором они связаны. Неудачи отбрасывают ошибки ввода/вывода, которые могут быть отслежены и подданы управлению.
Полезна для:
Выполнение ввода/вывода в пределах программы на языке Haskell.
Нуль и плюс:
Нет. IО a
Пример:
2.6.2. Мотивация Ввод/вывод несовместим с чистым функциональным языком, так как он не является прозрачным и не может избежать побочных эффектов. Монада IO решает эту проблему, ограничивая вычисления, которые выполняют ввод/вывод в пределах монады IO.
2.6.3. Определение Определение монады IO является платформо-зависимым. Никакие конструкторы данных не экспортированы и никакие функции не предусмотрены, чтобы удалять данные из монады IO. Это делает монаду IO односторонней монадой и существенной в обеспечении безопасности функциональных программ с помощью изоляции побочных эффектов и непрозрачных действий в пределах необходимого стиля вычислений монады IO.
Лист
ФП 02005-03 01 ИзмЛист № докум.
38
Подп. Дата Копирова
Формат
ФП 02005-03 01
В этой статье мы ссылались на унарные величины, как на вычисления. Тем не менее, величины в монаде IO часто называют действиями ввода/вывода, и мы используем эту терминологию здесь. В языке Haskell основная функция верхнего уровня должна иметь тип IO (), чтобы программы являлись естественно структурированными на верхнем уровне в виде необходимой последовательности действий ввода/вывода и вызывали функциональный стиль кода. Функции, экспортированные из модуля IO, не выполняют операций ввода/вывода сами. Они возвращают действия ввода/вывода, которые описывают операцию ввода/вывода, которая должна выполниться. Действия ввода/вывода объединены в пределах монады IO (в чисто функциональном стиле), чтобы создавать более сложные действия ввода/вывода, заканчивающиеся конечным действием ввода/вывода, которое является основной величиной программы.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Стандартный модуль Prelude и модуль IO определяют много функций, которые могут быть использованы в пределах монады IO, и любой программист на языке Haskell несомненно будет знаком с некоторыми из них. Эта статья только обсудит унарные аспекты монады IO; не полный ряд функций способен выполнять действия ввода/вывода. Конструктор типа IO — это экземпляр класса Monad и класса MonadError, где ошибки имеют тип IOError. Недостаток определён для того, чтобы отбросить ошибку, созданную из аргумента строки. В пределах монады IO вы можете использовать исключительные механизмы из модуля Управления.Монада.Ошибка в библиотеке шаблона монад, если вы импортируете модуль. Те же механизмы имеют альтернативные имена, экспортированные модулем IO:ioError и catch. instance Monad IO where return a = ... -- функция из a -> IO a m >>= k = ... -- выполняет действие ввода/вывода m и привязывает -- величину на k-ый ввод fail s = ioError (userError s) data IOError
=
...
ioError ioError
:: =
IOError -> IO a ...
userError userError
:: =
String -> IOError ...
catch catch
:: =
IO a -> (IOError -> IO a) -> IO a ...
try try f
:: =
IO a -> IO (Either IOError a) catch (do r <- f return (Right r)) (return . Left)
Лист
ФП 02005-03 01 ИзмЛист № докум.
39
Подп. Дата Копирова
Формат
ФП 02005-03 01
Монада IO включена в состав библиотеки шаблонов монады как пример класса MonadError. instance Error IOError where ... instance MonadError IO where throwError = ioError catchError = catch
Модуль IO экспортирует специальную функцию, вызывающую попытку выполнения действий ввода/вывода, и возвращает результат Right, если действие успешно, или Left IOError, если была найдена ошибка ввода/вывода.
2.6.4. Пример Этот пример показывает частичную реализацию команды tr, которая копирует стандартный входной поток в стандартный выходной поток с символьными переводами, контролируемыми командной строкой аргументов. Это демонстрирует использование исключения, обрабатывающего механизмы класса MonadError с монадой IO.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Код, имеющийся в example14.hs import import import import
Monad System IO Control.Monad.Error
-- переводится символ из set1 в соответствующий символ в set2 translate :: String -> String -> Char -> Char translate [] _ c = c translate (x:xs) [] c = if x == c then ' ' else translate xs [] c translate (x:xs) [y] c = if x == c then y else translate xs [y] c translate (x:xs) (y:ys) c = if x == c then y else translate xs ys c -- переводится целая строка translateString translateString set1 set2 str usage :: usage e = putStrLn putStrLn putStrLn
:: =
String -> String -> String -> String map (translate set1 set2) str
IOError -> IO () do "Usage: ex14 set1 set2" "Переводит символы из set1 на stdin в соответствующие" "символы из set2 и записывает перевод на stdout."
-- переводит stdin в stdout на основаннии командных аргументов main :: IO () main = (do
Лист
ФП 02005-03 01 ИзмЛист № докум.
40
Подп. Дата Копирова
Формат
ФП 02005-03 01
[set1,set2] <- getArgs contents <- hGetContents stdin putStr $ translateString set1 set2 contents) `catchError` usage
2.7. Монада State 2.7.1. Обзор Таблица 7. Свойства монады State Тип вычисления:
Вычисления, которые поддерживают состояние.
Стратегия связывания:
Связь управляет параметром состояния через последовательность связанных функций, поэтому одни и те же величины состояния никогда не используются дважды, создавая иллюзию коррекции на месте.
Полезна для:
Построение вычислений из последовательностей операций, которые требуют разделенного состояния.
Нуль и плюс:
Нет. State st a
Подп. и дата
Пример:
2.7.2. Мотивация Чистый функциональный язык не может корректировать величины на месте, поскольку это нарушает прозрачность. Общая идиома для имитации таких вычислений состояния — «управлять» параметром состояния через последовательность функций:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
data MyType
=
makeRandomValue makeRandomValue g
MT Int Bool Char Int deriving Show :: =
StdGen -> (MyType, StdGen) let (n, g1) = randomR (1,100) g (b, g2) = random g1 (c, g3) = randomR ('a', 'z') g2 (m, g4) = randomR (-n, n) g3 in (MT n b c m, g4)
Этот метод работает, но такой код может быть подвержен к появлению ошибок, быть беспорядочным и трудным к поддержке. Монада State держит управление параметром состояния вне операции связывания, одновременно делая код легче для записи, легче для чтения и легче для модификации.
2.7.3. Определение Показанное здесь определение использует классы многопараметрического типа и funDeps, которые не являются стандартными в языке Haskell 98. Нет необходимости полностью понимать эти детали, чтобы использовать монаду State.
Лист
ФП 02005-03 01 ИзмЛист № докум.
41
Подп. Дата Копирова
Формат
ФП 02005-03 01
newtype State s a
= State { runState :: (s -> (a,s)) }
instance Monad (State s) where return a = State $ \s -> (a,s) (State x) >>= f = State $ \s -> let (v,s') = x s in runState (f v) s'
Величины в монаде State представлены как переходные функции из начального состояния в пару (величина, newState), и новое определение типа предусмотрено для описания этой конструкции: State s — тип величины типа a вне монады State с состоянием типа s. Конструктор типа State s является экземпляром класса Monad. Возвращаемая функция просто создает функцию перехода состояния, которая устанавливает величину, но оставляет состояние неизменным. Связующий оператор создает функцию перехода состояния, которая прилагает свой правосторонний аргумент к величине и новому состоянию из своего левого аргумента. class MonadState m s | m -> s where get :: m s put :: s -> m ()
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
instance MonadState (State s) s where get = State $ \s -> (s,s) put s = State $ \_ -> ((),s)
Класс MonadState обеспечивает стандартный, но очень простой интерфейс для монад State. Полученная функция извлекает состояние, записывая его как величину. Заданная функция устанавливает состояние монады и не выдает величину. Есть много дополнительных функциональных обеспечений, которые выполняют более сложные вычисления, построенные на получении и задании. Наиболее полезный — получения, которые извлекают функцию состояния. Другие указаны в документации для библиотеки монады State.
2.7.4. Пример Простое применение монады State состоит в управлении произвольным состоянием генератора через многочисленные вызовы функции генерации. Код, используемый в example15.hs data MyType
=
MT Int Bool Char Int deriving Show
-- Используя монаду State, мы можем определить функцию, которая возвращает -- произвольную величину и в то же время корректирует произвольный генератор -- состояния getAny :: (Random a) => State StdGen a getAny = do g <- get (x, g') <- return $ random g put g'
Лист
ФП 02005-03 01 ИзмЛист № докум.
42
Подп. Дата Копирова
Формат
ФП 02005-03 01
return x -- подобен getAny, но ограничивает произвольную возвращенную величину getOne :: (Random a) => (a,a) -> State StdGen a getOne bounds = do g <- get (x, g') <- return $ randomR bounds g put g' return x -- Используя монаду State с состоянием StdGen, мы можем сформировать -- произвольные сложные типы без управления произвольным состоянием -- генератора makeRandomValueST :: StdGen -> (MyType, StdGen) makeRandomValueST = runState (do n <- getOne (1, 100) b <- getAny c <- getOne ('a', 'z') m <- getOne (-n, n) return (MT n b c m))
2.8. Монада Reader
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
2.8.1. Обзор Таблица 8. Свойства монады Reader Тип вычисления:
Вычисления, которые считывают значения из разделённой среды.
Стратегия связывания:
Величины монады являются функциями от среды к величине. Связанная функция приложена к связанной величине, и обе имеют доступ к разделённой среде.
Полезна для:
Поддерживание переменных связей или другой разделённой среды.
Нуль и плюс:
Нет. Reader [(String, Value)] a
Пример:
2.8.2. Мотивация Некоторые проблемы программирования требуют вычислений в пределах разделённой среды (как, например, установка переменных связей). Эти вычисления обычно считывают величины из среды и иногда выполняют подвычисления в модифицированной среде (например, с новыми или затеняющими связями), но они не требуют полной общности монады State. Монада Reader разработана специально для этих типов вычислений, и это часто является наиболее ясным и лёгким механизмом, чем использование монады State.
Лист
ФП 02005-03 01 ИзмЛист № докум.
43
Подп. Дата Копирова
Формат
ФП 02005-03 01
2.8.3. Определение Показанное здесь определение использует классы многопараметрического типа и funDeps, которые не являются стандартными в Haskell 98. Нет необходимости полностью понимать эти детали, чтобы использовать монаду Reader. newtype Reader e a
=
Reader { runReader :: (e -> a) }
instance Monad (Reader e) where return a = Reader $ \e -> a (Reader r) >>= f = Reader $ \e -> f (r e) e
Величины в монаде Reader — это функции от среды к величине. Чтобы извлечь конечную величину из вычисления в монаде Reader, необходимо просто приложить считыватель runReader к величине среды.
class MonadReader e m | m -> e where ask :: m e local :: (e -> e) -> m a -> m a instance MonadReader (Reader e) where ask = Reader id local f c = Reader $ \e -> runReader c (f e) (MonadReader e m) => (e -> a) -> m a ask >>= return . sel
Класс MonadReader выделяет множество подходящих функций, которые являются очень удобными при работе с монадой Reader. Запрашивающая функция извлекает среду, а локальная функция выполняет вычисление в модифицированной среде. Запрашивающая функция является удобной функцией, которая извлекает функцию текущей среды, и обычно используется с селекторной функцией или функцией поиска.
Рассмотрим проблему приписывания значений шаблонам, которые содержат переменные подстановки и включенные шаблоны. Используя монаду Reader, мы можем поддержать среду всех известных шаблонов и всех известных переменных связей. Затем, столкнувшись с переменной подстановкой, мы можем использовать запрашивающую функцию, чтобы найти величину переменной. Когда в шаблон включены новые определения переменных, мы можем использовать локальную функцию для разрешения
Инв. № подл.
Взам. инв. № Инв. № дубл.
asks :: asks sel =
Подп. и дата
Подп. и дата
Возвращаемая функция создаёт считыватель, который игнорирует среду и создает заданную величину. Связанный оператор создаёт считыватель, который использует среду, чтобы извлекать величину её левой границы, а затем прилагать связанную функцию к этой величине в той же среде.
2.8.4. Пример
Лист
ФП 02005-03 01 ИзмЛист № докум.
44
Подп. Дата Копирова
Формат
ФП 02005-03 01
шаблона в модифицированной среде, которая содержит дополнительные переменные связи. Код, используемый в example16.hs -- Это абстрактное синтаксическое представление шаблона -- Текст Переменная Назначение Включение Состав data Template = T String | V Template | Q Template | I Template [Definition] | C [Template] data Definition = D Template Template -- Наша среда состоит из ассоциативного списка именованных шаблонов и -- ассоциативного списка поименованных переменных величин data Environment = Env {templates::[(String,Template)], variables::[(String,String)]}
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- поиск переменной в среды lookupVar :: lookupVar name env = --поиск шаблона в среде lookupTemplate lookupTemplate name env
String -> Environment -> Maybe String lookup name (variables env)
:: String -> Environment -> Maybe Template = lookup name (templates env)
-- добавляет список разрешенных определений к среде addDefs :: [(String,String)] -> Environment -> Environment addDefs defs env = env {variables = defs ++ (variables env)} -- решает определения и создает пару (имя, величина) resolveDef :: Definition -> Reader Environment (String,String) resolveDef (D t d) = do name <- resolve t value <- resolve d return (name, value) -- переводит шаблон в строку resolve :: Template -> Reader Environment (String) resolve (T s) = return s resolve (V t) = do varName <- resolve t varValue <- asks (lookupVar varName) return $ maybe "" id varValue resolve (Q t) = do tmplName <- resolve t body <- asks (lookupTemplate tmplName) return $ maybe "" show body resolve (I t ds) = do tmplName <- resolve t body <- asks (lookupTemplate tmplName)
Лист
ФП 02005-03 01 ИзмЛист № докум.
45
Подп. Дата Копирова
Формат
ФП 02005-03 01
case body of Just t' -> do defs <- mapM resolveDef ds local (addDefs defs) (resolve t') Nothing -> return "" resolve (C ts) = (liftM concat) (mapM resolve ts)
Чтобы использовать монаду Reader для перевода шаблона t в строку, вам просто нужно выполнить runReader (resolve t) env.
2.9. Монада Writer 2.9.1. Обзор
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Таблица 9. Свойства монады Writer Тип вычисления:
Вычисления, которые создают поток данных в дополнении к вычисленным величинам.
Стратегия связывания:
Величина монады Writer — пара (вычислительная величина, регистрационная величина). Связь заменяет вычислительную величину результатом применения связанной функции к предшествующей величине и добавляет любые регистрационные данные от вычисления до существующих регистрационных данных.
Полезна для:
Регистрация, или другие вычисления, которые производят выход «на стороне».
Нуль и плюс:
Нет. Writer [String] a
Пример:
2.9.2. Мотивация Желательно использовать для вычисления генерации выхода «на стороне». Регистрация и слежение — наиболее общие примеры, в которых данные сгенерированы в процессе вычисления, которое мы хотим сохранить, но это не первичный результат вычисления. Явное управление регистрацией или отслеживанием данных может загромоздить код и вызвать тонкие дефекты, как, например, пропущенные регистрационные данные. Монада Writer обеспечивает чистый путь для управления выходом, не загромождая основное вычисление.
2.9.3. Определение Показанное здесь определение использует классы многопараметрического типа и funDeps, которые не являются стандартными в Haskell 98. Нет необходимости полностью понимать эти детали, чтобы использовать монаду Writer.
Лист
ФП 02005-03 01 ИзмЛист № докум.
46
Подп. Дата Копирова
Формат
ФП 02005-03 01
Для того, чтобы полностью понимать это определение, вам нужно знать о классе Monoid в языке Haskell, который представляет собой математическую структуру, представляющую моноид так же, как и класс Monad представляет структуру монады. Хорошей новостью является то, что моноиды проще, чем монады. Моноид — это множество объектов, единственный элемент тождества и ассоциативный бинарный оператор над множеством объектов. Моноид должен подчиняться некоторым математическим законам, таким, при которых приложение оператора к любым величинам из установленных даёт другую величину при установке, и всякий раз, когда один операнд оператора является элементом тождества, результат равняется другому операнду. Вы можете обратить внимание, что эти законы являются такими же, как и законы, управляющие mzero и mplus для примеров MonadPlus. Это происходит потому, что монады с нулём и плюсом являются монадами, которые также являются моноидами!
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Одним из примеров математических моноидов являются натуральные числа с элементом тождества 0 и двоичный оператор для сложения, а также натуральные числа с элементом тождества 1 и двоичный оператор для умножения. В языке Haskell моноид состоит из типа, элемента тождества и двоичного оператора. Язык Haskell определяет класс Monoid (в Data.Monoid), чтобы обеспечивать стандартную конвенцию для работы с моноидами: элемент тождества назван mempty, а оператор назван mappend. Наиболее общий использованный стандартный моноид в языке Haskell — это список, но функции типа (а -> a) также формируют моноид. Вы должны быть внимательными при использовании списка как моноиды для Writer, так как там может быть штраф при исполнении, связанный с операцией mappend, так как выход увеличивается. В этом случае структура данных, которая поддерживает быстрые операции добавления, должна быть более подходящим выбором. newtype Writer w a
=
Writer { runWriter :: (a,w) }
instance (Monoid w) => Monad (Writer w) where return a = Writer (a,mempty) (Writer (a,w)) >>= f = let (a', w') = runWriter $ f a in Writer (a',w `mappend` w')
Монада Writer поддерживает пару (величина, протокол), где регистрационный тип должен быть моноидом. Возвращаемая функция просто возвращает величину вместе с пустым протоколом. Связь выполняет связанную функцию, использующую текущую величину как ввод, и добавляет любой регистрационный выход в существующий протокол. class (Monoid pass listen tell
w, Monad m) => MonadWriter w m | m -> w where :: m (a,w -> w) -> m a :: m a -> m (a,w) :: w -> m ()
Лист
ФП 02005-03 01 ИзмЛист № докум.
47
Подп. Дата Копирова
Формат
ФП 02005-03 01
instance (Monoid w) => MonadWriter (Writer w) where pass (Writer ((a, f), w)) = Writer (a, f w) listen (Writer (a, w)) = Writer ((a, w), w) tell s = Writer ((), s) listens listens f m
:: =
(MonadWriter w m) => (w -> w) -> m a -> m (a, w) do (a, w) <- m; return (a, f w)
censor censor f m
:: =
(MonadWriter w m) => (w -> w) -> m a -> m a pass $ do a <- m; return (a, f)
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Класс MonadWriter выделяет множество удобных функций для работы с монадами Writer. Самый простой и наиболее полезный способ — сообщение, которое добавляет одно или более данных к протоколу. Прослушивающая функция переделывает Writer, которая возвращает величину a и создает выход w в Writer, которая создает величину (a, w) и все еще создает выход w. Это допускает вычисление, чтобы «слушать» протокол на выходе, сгенерированный Writer. Передаваемая функция немного усложнена. Она преобразовывает Writer, которая производит величину (a, f) и выход w в Writer, которая производит величину a и выход f w. Это отчасти громоздко, поэтому нормально использован функциональный цензор помощника. Функция цензора берет функцию и Writer и производит новую Writer, чей выход — тот же, но чей регистрационный вход был модифицирован функцией. Прослушивающая функция кроме того управляет тем, что регистрационная часть величины модифицируется поставленной функцией.
2.9.4. Пример В этом примере, мы представляем себе то же простое применение, которое фильтрует пакеты, основываясь на базе правил, сочетающих в себе исходные и предопределённые множества и полезность пакетов. Первичной работой является фильтрование пакетов, но нам должно это нравиться, чтобы создать протокол деятельности. Код, имеющийся в example17.hs -- это - формат нашего входного протокола data Entry = Log {count::Int, msg::String} deriving Eq -- добавляем сообщение к протоколу logMsg :: String -> Writer [Entry] () logMsg s = tell [Log 1 s] -- управление одним пакетом filterOne :: [Rule] -> Packet -> Writer [Entry] (Maybe Packet)
Лист
ФП 02005-03 01 ИзмЛист № докум.
48
Подп. Дата Копирова
Формат
ФП 02005-03 01
filterOne rules packet = do rule <- return (match rules packet) case rule of Nothing -> do logMsg ("DROPPING UNMATCHED PACKET: " ++ (show packet)) return Nothing (Just r) -> do when (logIt r) (logMsg ("MATCH: " ++ (show r) ++ " <=> " ++ (show packet))) case r of (Rule Accept _ _) -> return (Just packet) (Rule Reject _ _) -> return Nothing
Это было довольно просто, но что если мы хотим объединить двойные последовательные регистрационные данные? Ни одна из существующих функций не позволяет нам модифицировать выход с предшествующих этапов вычисления, но мы можем использовать хитрость «задержанная регистрация», чтобы добавлять регистрационный вход только после того, как мы получим новый вход, который не соответствует ни одному до него.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Код, имеющийся в example17.hs -- слияние идентичных данных в конце протокола. Эта функция использует -- [Entry] как тип протокола и тип результата. Когда два идентичных сообщения -- объеденены, результатом является сообщением с увеличенным счётчиком. Когда -- два других сообщения объединены, сначала регистрируется первое сообщение, -- а второе возвращается как результат. mergeEntries :: [Entry] -> [Entry] -> Writer [Entry] [Entry] mergeEntries [] x = return x mergeEntries x [] = return x mergeEntries [e1] [e2] = let (Log n msg) = e1 (Log n' msg') = e2 in if msg == msg' then return [(Log (n+n') msg)] else do tell [e1] return [e2] -- Эта с виду сложная функция на самом деле красива и проста. -- Она отображает функцию над списком величин, чтобы получать список Writer, -- тогда выполняется каждый автор и объединяются результаты. Результат -- функции - автор, чья величина является списком всех величин из авторов -- и чей регистрационный выход является результатом свертки оператора слияния -- в личные регистрационные данные (использование инициала как начального -- протокола величины). groupSame :: (Monoid a) => a ->
Лист
ФП 02005-03 01 ИзмЛист № докум.
49
Подп. Дата Копирова
Формат
ФП 02005-03 01
(a -> a -> Writer a a) -> [b] -> (b -> Writer a c) -> Writer a [c] groupSame initial merge [] _ = do
tell initial return []
groupSame initial merge (x:xs) fn = do (result,output) <- return (runWriter (fn x)) new <- merge initial output rest <- groupSame new merge xs fn return (result:rest) -- эти фильтры - список пакетов, производящих отфильтрованный список пакета и -- протокол деятельности, в котором объединяются последовательные сообщения filterAll :: [Rule] -> [Packet] -> Writer [Entry] [Packet] filterAll rules packets = do tell [Log 1 "STARTING PACKET FILTER"] out <- groupSame [] mergeEntries packets (filterOne rules) tell [Log 1 "STOPPING PACKET FILTER"] return (catMaybes out)
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
2.10. Монада Continuation 2.10.1. Обзор Таблица 10. Свойства монады Continuation Тип вычисления:
Вычисления, которые могут быть остановлены и возобновлены.
Стратегия связывания:
Привязывание функции к унарному значению создаёт новое возобновление, которое использует функцию, как возобновление унарного вычисления.
Полезна для:
Сложные управляющие структуры, обработка ошибок и создание сопрограмм.
Нуль и плюс:
Нет. Cont r a
Пример:
2.10.2. Мотивация Злоупотребление монадой Continuation может привести к созданию кода, который будет невозможно понять и поддержать. Прежде чем использовать монаду Continuation, будьте уверены, что у вас есть твёрдое понимание выносимого возобновления стиля (CPS), и что возобновления представляют собой лучшее решение вашей определённой задачи. Многие алгоритмы, которые требуют возобновлений в других языках, не требуют их в языке Haskell, благодаря ленивой семантике языка Haskell.
Лист
ФП 02005-03 01 ИзмЛист № докум.
50
Подп. Дата Копирова
Формат
ФП 02005-03 01
Возобновления представляют будущие вычисления, как функцию от промежуточного результата к конечному результату. В выносимом возобновлении стиле вычисления построены из последовательностей вложенных возобновлений, заканчивающихся конечным возобновлением (часто идентификатором), которое создаёт конечный результат. С тех пор как возобновления являются функциями, представляющими будущее вычисления, обработка функций возобновления может достичь сложных обработок будущего вычисления, как, например, прерывание вычисления в середине, прерывание части вычислений, возобновление вычислений и чередование выполнения вычислений. Монада Continuation приспосабливает CPS к структуре монады.
2.10.3. Определение -- r - это конечный результирующий тип всего вычисления newtype Cont r a = Cont { runCont :: ((a -> r) -> r) }
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
instance Monad (Cont r) where return a = Cont $ \k -> k a -- return a = \k -> k a (Cont c) >>= f = Cont $ \k -> c (\a -> runCont (f a) k) -- c >>= f = \k -> c (\a -> f a k)
Монада Continuation представляет вычисления в выносимом возобновления стиле. Cont r a — это CPS вычисление, которое создает промежуточный результат типа а в пределах CPS вычисления, чей конечный результирующий тип — r. Возвращаемая функция просто создаёт возобновление, которое передаёт значение. Оператор >>= добавляет связанную функцию к возобновленной цепочке. class (Monad m) => MonadCont m where callCC :: ((a -> m b) -> m a) -> m a instance MonadCont (Cont r) where callCC f = Cont $ \k -> runCont (f (\a -> Cont $ \_ -> k a)) k
Класс MonadCont создает функцию callCC, которая создает возобновленный механизм выхода для использования монадами Continuation. Возобновления выхода позволяют вам прервать текущее вычисление и возвратить непосредственно значение. Они достигают похожего результата с throwError и catchError в монаде Error. Функция сallCC вызывает функцию с таким текущим возобновлением, как её аргумент (отсюда имя). Стандартная идиома, используемая callCC — это создание λ-выражений для того, чтобы назвать возобновление. Затем вызов названного возобновления везде в пределах его области будет избавлять от вычисления, даже если находится на много слоёв глубже во вложенных вычислениях.
Лист
ФП 02005-03 01 ИзмЛист № докум.
51
Подп. Дата Копирова
Формат
ФП 02005-03 01
В добавлении к механизму вывода, предусмотренному callCC, монада Continuation может быть использована для выполнения других, более мощных обработок возобновления. Эти другие механизмы имеют довольно специализированное использование, однако, и злоупотребление ими может легко создать зверски затемнённый код; поэтому они не будут охвачены здесь.
2.10.4. Пример Этот пример показывает фрагмент того, как работает возобновление выхода. Функция в примере использует возобновления выхода для выполнения сложных преобразований над целыми числами.
-----------
Мы используем монаду Continuation, чтобы выполнить “выходы” из блоков кода. Эта функция осуществляет сложную управляющую структуру для обработки чисел: Ввод (n) Вывод Показательный Список ========= ====== ========== 0-9 n ничего 10-199 число цифр в (n/2) цифры из (n/2) 200-19999 n цифры из (n/2) 20000-1999999 (n/2) назад ничего >= 2000000 сумма цифр из (n/2) цифры из (n/2)
fun :: Int -> String fun n = (`runCont` id) $ do str <- callCC $ \exit1 -> do -- определяет "exit1" when (n < 10) (exit1 (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do -- определяет "exit2" when ((length ns) < 3) (exit2 (length ns)) when ((length ns) < 5) (exit2 n) when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 (dropWhile (=='0') ns') -- освобождает 2 уровня return $ sum ns return $ "(ns = " ++ (show ns) ++ ") " ++ (show n') return $ "Answer: " ++ str
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Код, имеющийся в example18.hs
Лист
ФП 02005-03 01 ИзмЛист № докум.
52
Подп. Дата Копирова
Формат
ФП 02005-03 01
3. МОНАДЫ В РЕАЛЬНОМ МИРЕ 3.1. Introduction Part I has introduced the monad concept and Part II has provided an understanding of a number of common, useful monads in the standard Haskell libraries. This is not enough to put monads into heavy practice, however, because in the real world you often want computations which combine aspects of more than one monad at the same time, such as stateful, nondetermistic computations or computations which make use of continuations and perform I/O. When one computation is a strict subset of the other, it is possible to perform the monad computations separately, unless the sub-computation is performed in a one-way monad.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Often, the computations can't be performed in isolation. In this case, what is needed is a monad that combines the features of the two monads into a single computation. It is inefficient and poor practice to write a new monad instance with the required characteristics each time a new combination is desired. Instead, we would prefer to develop a way to combine the standard monads to produce the needed hybrids. The technique that lets us do exactly that is called monad transformers. Monad transformers are the topic of Part III, and they are explained by revisiting earlier examples to see how monad transformers can be used to add more realistic capabilities to them. It may be helpful to review the earlier examples as they are re-examined.
3.2. Combining monads the hard way Before we investigate the use of monad transformers, we will see how monads can be combined without using transformers. This is a useful excercise to develop insights into the issues that arise when combining monads and provides a baseline from which the advantages of the transformer approach can be measured. We use the code from example 18 (the Continuation monad) to illustrate these issues, so you may want to review it before continuing.
3.2.1. Nested Monads Some computations have a simple enough structure that the monadic computations can be nested, avoiding the need for a combined monad altogether. In Haskell, all computations occur in the IO monad at the top level, so the monad examples we have seen so far all actually use the technique of nested monadic computations. To do this, the computations perform all of their input at the beginning — usually by reading arguments from the command line — then pass the values on to the monadic computations to produce results, and finally perform their output at the end. This structure avoids the issues of combining monads but makes the examples seem contrived at times.
Лист
ФП 02005-03 01 ИзмЛист № докум.
53
Подп. Дата Копирова
Формат
ФП 02005-03 01
The code introduced in example 18 followed the nesting pattern: reading a number from the command line in the IO monad, passing that number to a computation in the Continuation monad to produce a string, and then writing the string back in the IO monad. The computations in the IO monad aren't restricted to reading from the command line and writing strings; they can be arbitrarily complex. Likewise, the inner computation can be arbitrarily complex as well. As long as the inner computation does not depend on the functionality of the outer monad, it can be safely nested within the outer monad, as illustrated in this variation on example 18 which reads the value from stdin instead of using a command line argument:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Code available in example19.hs fun :: IO String fun = do n <- (readLn::IO Int) -- this is an IO monad block return $ (`runCont` id) $ do -- this is a Cont monad block str <- callCC $ \exit1 -> do when (n < 10) (exit1 (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do when ((length ns) < 3) (exit2 (length ns)) when ((length ns) < 5) (exit2 n) when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 (dropWhile (=='0') ns') return $ sum ns return $ "(ns = " ++ (show ns) ++ ") " ++ (show n') return $ "Answer: " ++ str
3.2.2. Combined Monads What about computations with more complicated structure? If the nesting pattern cannot be used, we need a way to combine the attributes of two or more monads in a single computation. This is accomplished by doing computations within a monad in which the values are themselves monadic values in another monad. For example, we might perform computations in the Continuation monad of type Cont (IO String) a if we need to perform I/O within the computation in the Continuation monad. We could use a monad of type State (Either Err a) a to combine the features of the State and Error monads in a single computation. Consider a slight modification to our example in which we perform the same I/O at the beginning, but we may require additional input in the middle of the computation in the Continuation monad. In this case, we will allow the user to specify part of the output value when the input value is within a certain range. Because the I/O depends on part of the computation in the Continuation monad and part of the computation in the Continuation monad depends on the result of the I/O, we cannot use the nested monad pattern.
Лист
ФП 02005-03 01 ИзмЛист № докум.
54
Подп. Дата Копирова
Формат
ФП 02005-03 01
Instead, we make the computation in the Continuation monad use values from the IO monad. What used to be Int and String values are now of type IO Int and IO String. We can't extract values from the IO monad — it's a one-way monad — so we may need to nest little doblocks of the IO monad within the Continuation monad to manipulate the values. We use a helper function toIO to make it clearer when we are creating values in the IO monad nested within the Continuation monad. Code available in example20.hs toIO :: a -> IO a toIO x = return x
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
fun :: IO String fun = do n <- (readLn::IO Int) convert n
-- this is an IO monad block
convert :: Int -> IO String convert n = (`runCont` id) $ do -- this is a Cont monad block str <- callCC $ \exit1 -> do -- str has type IO String when (n < 10) (exit1 $ toIO (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do -- n' has type IO Int when ((length ns) < 3) (exit2 (toIO (length ns))) when ((length ns) < 5) (exit2 $ do putStrLn "Enter a number:" x <- (readLn::IO Int) return x) when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 $ toIO (dropWhile (=='0') ns') return (toIO (sum ns)) return $ do num <- n' -- this is an IO monad block return $ "(ns = " ++ (show ns) ++ ") " ++ (show num) return $ do s <- str -- this is an IO monad block return $ "Answer: " ++ s
Even this trivial example has gotten confusing and ugly when we tried to combine different monads in the same computation. It works, but it isn't pretty. Comparing the code side-by-side shows the degree to which the manual monad combination strategy pollutes the code. Nested monads from example 19 fun = do n <- (readLn::IO Int) return $ (`runCont` id) $ do str <- callCC $ \exit1 -> do when (n < 10) (exit1 (show n))
Лист
ФП 02005-03 01 ИзмЛист № докум.
55
Подп. Дата Копирова
Формат
ФП 02005-03 01
let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do when ((length ns) < 3) (exit2 (length ns)) when ((length ns) < 5) (exit2 n) when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 (dropWhile (=='0') ns') return $ sum ns return $ "(ns = " ++ (show ns) ++ ") " ++ (show n') return $ "Answer: " ++ str
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Manually combined monads from example 20 convert n = (`runCont` id) $ do str <- callCC $ \exit1 -> do when (n < 10) (exit1 $ toIO (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do when ((length ns) < 3) (exit2 (toIO (length ns))) when ((length ns) < 5) (exit2 $ do putStrLn "Enter a number:" x <- (readLn::IO Int) return x) when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 $ toIO (dropWhile (=='0') ns') return (toIO (sum ns)) return $ do num <- n' return $ "(ns = " ++ (show ns) ++ ") " ++ (show num) return $ do s <- str return $ "Answer: " ++ s
3.3. Monad transformers Monad transformers are special variants of standard monads that facilitate the combining of monads. Their type constructors are parameterized over a monad type constructor, and they produce combined monadic types.
3.3.1. Transformer type constructors Type constructors play a fundamental role in Haskell's monad support. Recall that Reader r a is the type of values of type a within a Reader monad with environment of type r. The type constructor Reader r is an instance of the Monad class, and the runReader::(r->a) function performs a computation in the Reader monad and returns the result of type a.
Лист
ФП 02005-03 01 ИзмЛист № докум.
56
Подп. Дата Копирова
Формат
ФП 02005-03 01
A transformer version of the Reader monad, called ReaderT, exists which adds a monad type constructor as an addition parameter. ReaderT r m a is the type of values of the combined monad in which Reader is the base monad and m is the inner monad. ReaderT r m is an instance of the monad class, and the runReaderT::(r -> m a) function performs a computation in the combined monad and returns a result of type m a. Using the transformer versions of the monads, we can produce combined monads very simply. ReaderT r IO is a combined Reader+IO monad. We can also generate the nontransformer version of a monad from the transformer version by applying it to the Identity monad. So ReaderT r Identity is the same monad as Reader r. If your code produces kind errors during compilation, it means that you are not using the type cosntructors properly. Make sure that you have supplied the correct number of parameters to the type constructors and that you have not left out any parenthesis in complex type expressions.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
3.3.2. Lifting When using combined monads created by the monad transformers, we avoid having to explicitly manage the inner monad types, resulting in clearer, simpler code. Instead of creating additional do-blocks within the computation to manipulate values in the inner monad type, we can use lifting operations to bring functions from the inner monad into the combined monad. Recall the liftM family of functions which are used to lift non-monadic functions into a monad. Each monad transformer provides a lift function that is used to lift a monadic computation into a combined monad. Many transformers also provide a liftIO function, which is a version of lift that is optimized for lifting computations in the IO monad. To see this in action, we will continue to develop our previous example in the Continuation monad. Code available in example21.hs fun :: IO String fun = (`runContT` return) $ do n <- liftIO (readLn::IO Int) str <- callCC $ \exit1 -> do -- define "exit1" when (n < 10) (exit1 (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do -- define "exit2" when ((length ns) < 3) (exit2 (length ns)) when ((length ns) < 5) $ do liftIO $ putStrLn "Enter a number:" x <- liftIO (readLn::IO Int) exit2 x when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 (dropWhile (=='0') ns') -escape 2 levels return $ sum ns
Лист
ФП 02005-03 01 ИзмЛист № докум.
57
Подп. Дата Копирова
Формат
ФП 02005-03 01
return $ "(ns = " ++ (show ns) ++ ") " ++ (show n') return $ "Answer: " ++ str
Compare this function using ContT, the transformer version of Cont, with the original version to see how unobtrusive the changes become when using the monad transformer. Nested monads from example 19 fun = do n <- (readLn::IO Int) return $ (`runCont` id) $ do str <- callCC $ \exit1 -> do when (n < 10) (exit1 (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do when ((length ns) < 3) (exit2 (length ns)) when ((length ns) < 5) (exit2 n) when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 (dropWhile (=='0') ns') return $ sum ns return $ "(ns = " ++ (show ns) ++ ") " ++ (show n') return $ "Answer: " ++ str
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Monads combined with a transformer from example 21 fun = (`runContT` return) $ do n <- liftIO (readLn::IO Int) str <- callCC $ \exit1 -> do when (n < 10) (exit1 (show n)) let ns = map digitToInt (show (n `div` 2)) n' <- callCC $ \exit2 -> do when ((length ns) < 3) (exit2 (length ns)) when ((length ns) < 5) $ do liftIO $ putStrLn "Enter a number:" x <- liftIO (readLn::IO Int) exit2 x when ((length ns) < 7) $ do let ns' = map intToDigit (reverse ns) exit1 (dropWhile (=='0') ns') return $ sum ns return $ "(ns = " ++ (show ns) ++ ") " ++ (show n') return $ "Answer: " ++ str
The impact of adding the I/O in the middle of the computation is narrowly confined when using the monad transformer. Contrast this with the changes required to achieve the same result using a manually combined monad.
Лист
ФП 02005-03 01 ИзмЛист № докум.
58
Подп. Дата Копирова
Формат
ФП 02005-03 01
3.4. Standard monad transformers Haskell's base libraries provide support for monad transformers in the form of classes which represent monad transformers and special transformer versions of standard monads.
3.4.1. The MonadTrans and MonadIO classes The MonadTrans class is defined in Control.Monad.Trans and provides the single function lift. The lift function lifts a monadic computation in the inner monad into the combined monad. class MonadTrans t where lift :: (Monad m) => m a -> t m a
Monads which provide optimized support for lifting IO operations are defined as members of the MonadIO class, which defines the liftIO function. class (Monad m) => MonadIO m where liftIO :: IO a -> m a
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
3.4.2. Transformer versions of standard monads The standard monads of the monad template library all have transformer versions which are defined consistently with their non-transformer versions. However, it is not the case the all monad transformers apply the same transformation. We have seen that the ContT transformer turns continuations of the form (a->r)->r into continuations of the form (a->m r)->m r. The StateT transformer is different. It turns state transformer functions of the form s->(a,s) into state transformer functions of the form s->m (a,s). In general, there is no magic formula to create a transformer version of a monad — the form of each transformer depends on what makes sense in the context of its non-transformer type. Standard Monad
Transformer Version
Original Type
Combined Type
Error
ErrorT
Either e a
m (Either e a)
State
StateT
s -> (a,s)
s -> m (a,s)
Reader
ReaderT
r -> a
r -> m a
Writer
WriterT
(a,w)
m (a,w)
Cont
ContT
(a -> r) -> r
(a -> m r) -> m r
Лист
ФП 02005-03 01 ИзмЛист № докум.
59
Подп. Дата Копирова
Формат
ФП 02005-03 01
Order is important when combining monads. StateT s (Error e) is different than ErrorT e (State s). The first produces a combined type of s -> Error e (a,s), in which the computation can either return a new state or generate an error. The second combination produces a combined type of s -> (Error e a,s), in which the computation always returns a new state, and the value can be an error or a normal value.
3.5. Anatomy of a monad transformer In this section, we will take a detailed look at the implementation of one of the more interesting transformers in the standard library, StateT. Studying this transformer will build insight into the transformer mechanism that you can call upon when using monad transformers in your code. You might want to review the section on the State monad before continuing.
3.5.1. Combined monad definition Just as the State monad was built upon the definition newtype State s a = State { runState :: (s -> (a,s)) }
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
the StateT transformer is built upon the definition newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }
State s is an instance of both the Monad class and the MonadState s class, so StateT s m should also be members of the Monad and MonadState s classes. Furthermore, if m is an instance of MonadPlus, StateT s m should also be a member of MonadPlus. To define StateT s m as a Monad instance: newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) } instance (Monad m) => Monad (StateT s m) where return a = StateT $ \s -> return (a,s) (StateT x) >>= f = StateT $ \s -> do (v,s') <- x s get new value, state (StateT x') <- return $ f v apply bound function to get new state transformation fn x' s' apply the state transformation fn to the new state
----
Compare this to the definition for State s. Our definition of return makes use of the return function of the inner monad, and the binding operator uses a do-block to perform a computation in the inner monad. We also want to declare all combined monads that use the StateT transformer to be instaces of the MonadState class, so we will have to give definitions for get and put:
Инв. № подл.
instance (Monad m) => MonadState s (StateT s m) where
Лист
ФП 02005-03 01 ИзмЛист № докум.
60
Подп. Дата Копирова
Формат
ФП 02005-03 01
get = StateT $ \s -> return (s,s) put s = StateT $ \_ -> return ((),s)
Finally, we want to declare all combined monads in which StateT is used with an instance of MonadPlus to be instances of MonadPlus: instance (MonadPlus m) => MonadPlus (StateT s m) where mzero = StateT $ \s -> mzero (StateT x1) `mplus` (StateT x2) = StateT $ \s -> (x1 s) `mplus` (x2 s)
3.5.2. Defining the lifting function The final step to make our monad transformer fully integrated with Haskell's monad classes is to make StateT s an instance of the MonadTrans class by providing a lift function:
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
instance MonadTrans (StateT s) where lift c = StateT $ \s -> c >>= (\x -> return (x,s))
The lift function creates a StateT state transformation function that binds the computation in the inner monad to a function that packages the result with the input state. The result is that a function that returns a list (i.e., a computation in the List monad) can be lifted into StateT s [], where it becomes a function that returns a StateT (s -> [(a,s)]). That is, the lifted computation produces multiple (value,state) pairs from its input state. The effect of this is to "fork" the computation in StateT, creating a different branch of the computation for each value in the list returned by the lifted function. Of course, applying StateT to a different monad will produce different semantics for the lift function.
3.5.3. Functors We have examined the implementation of one monad transformer above, and it was stated earlier that there was no magic formula to produce transformer versions of monads. Each transformer's implementation will depend on the nature of the computational effects it is adding to the inner monad. Despite this, there is some theoretical foundation to the theory of monad transformers. Certain transformers can be grouped according to how they use the inner monad, and the transformers within each group can be derived using monadic functions and functors. Functors, roughly, are types which support a mapping operation fmap :: (a->b) -> f a -> f b. To learn more about it, check out Mark Jones' influential paper that inspired the Haskell monad template library.
3.6. More examples with monad transformers At this point, you should know everything you need to begin using monads and monad transformers in your programs. The best way to build proficiency is to work on actual code. As
Лист
ФП 02005-03 01 ИзмЛист № докум.
61
Подп. Дата Копирова
Формат
ФП 02005-03 01
your monadic programs become more abitious, you may find it awkward to mix additional transformers into your combined monads. This will be addressed in the next section, but first you should master the basic process of applying a single transformer to a base monad.
3.6.1. WriterT with IO Try adapting the firewall simulator of example 17 to include a timestamp on each log entry (don't worry about merging entries). The necessary changes should look something like this: Code available in example22.hs -- this is the format of our log entries data Entry = Log {timestamp::ClockTime, msg::String} deriving Eq instance Show Entry where show (Log t s) = (show t) ++ " | " ++ s
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- this is the combined monad type type LogWriter a = WriterT [Entry] IO a -- add a message to the log logMsg :: String -> LogWriter () logMsg s = do t <- liftIO getClockTime tell [Log t s] -- this handles one packet filterOne :: [Rule] -> Packet -> LogWriter (Maybe Packet) filterOne rules packet = do rule <- return (match rules packet) case rule of Nothing -> do logMsg ("DROPPING UNMATCHED PACKET: " ++ (show packet)) return Nothing (Just r) -> do when (logIt r) (logMsg ("MATCH: " ++ (show r) ++ " <=> " ++ (show packet))) case r of (Rule Accept _ _) -> return (Just packet) (Rule Reject _ _) -> return Nothing -- this filters a list of packets, producing a filtered packet list -- and a log of the activity filterAll :: [Rule] -> [Packet] -> LogWriter [Packet] filterAll rules packets = do logMsg "STARTING PACKET FILTER" out <- mapM (filterOne rules) packets logMsg "STOPPING PACKET FILTER" return (catMaybes out)
Лист
ФП 02005-03 01 ИзмЛист № докум.
62
Подп. Дата Копирова
Формат
ФП 02005-03 01
-- read the rule data from the file named in the first argument, and the packet data from -- the file named in the second argument, and then print the accepted packets followed by -- a log generated during the computation. main :: IO () main = do args <- getArgs ruleData <- readFile (args!!0) packetData <- readFile (args!!1) let rules = (read ruleData)::[Rule] packets = (read packetData)::[Packet] (out,log) <- runWriterT (filterAll rules packets) putStrLn "ACCEPTED PACKETS" putStr (unlines (map show out)) putStrLn "\n\nFIREWALL LOG" putStr (unlines (map show log))
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
3.6.2. ReaderT with IO If you found that one too easy, move on to a slightly more complex example: convert the template system in example 16 from using a single template file with named templates to treating individual files as templates. One possible solution is shown in example 23, but try to do it without looking first.
3.6.3. StateT with List The previous examples have all been using the IO monad as the inner monad. Here is a more interesting example: combining StateT with the List monad to produce a monad for stateful nondeterministic computations. We will apply this powerful monad combination to the task of solving constraint satisfaction problems (in this case, a logic problem). The idea behind it is to have a number of variables that can take on different values and a number of predicates involving those variables that must be satisfied. The current variable assignments and the predicates make up the state of the computation, and the non-deterministic nature of the List monad allows us to easily test all combinations of variable assignments. We start by laying the groundwork we will need to represent the logic problem, a simple predicate language: Code available in example24.hs -- First, we develop a language to express logic problems type Var = String type Value = String data Predicate = Is Var Value -- var has specific value
Лист
ФП 02005-03 01 ИзмЛист № докум.
63
Подп. Дата Копирова
Формат
ФП 02005-03 01
| Equal Var Var
-- vars have same (unspecified)
value | And Predicate Predicate | Or Predicate Predicate | Not Predicate deriving (Eq, Show)
-- both are true -- at least one is true -- it is not true
type Variables = [(Var,Value)] -- test for a variable NOT equaling a value isNot :: Var -> Value -> Predicate isNot var value = Not (Is var value) -- if a is true, then b must also be true implies :: Predicate -> Predicate -> Predicate implies a b = Not (a `And` (Not b))
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- exclusive or orElse :: Predicate -> Predicate -> Predicate orElse a b = (a `And` (Not b)) `Or` ((Not a) `And` b) -- Check a predicate with the given variable bindings. -- An unbound variable causes a Nothing return value. check :: Predicate -> Variables -> Maybe Bool check (Is var value) vars = do val <- lookup var vars return (val == value) check (Equal v1 v2) vars = do val1 <- lookup v1 vars val2 <- lookup v2 vars return (val1 == val2) check (And p1 p2) vars = liftM2 (&&) (check p1 vars) (check p2 vars) check (Or p1 p2) vars = liftM2 (||) (check p1 vars) (check p2 vars) check (Not p) vars = liftM (not) (check p vars)
The next thing we will need is some code for representing and solving constraint satisfaction problems. This is where we will define our combined monad. Code available in example24.hs -- this is the type of our logic problem data ProblemState = PS {vars::Variables, constraints::[Predicate]} -- this is our monad type for non-determinstic computations with state type NDS a = StateT ProblemState [] a -- lookup a variable getVar :: Var -> NDS (Maybe Value) getVar v = do vs <- gets vars return $ lookup v vs
Лист
ФП 02005-03 01 ИзмЛист № докум.
64
Подп. Дата Копирова
Формат
ФП 02005-03 01
-- set a variable setVar :: Var -> Value -> NDS () setVar v x = do st <- get vs' <- return $ filter ((v/=).fst) (vars st) put $ st {vars=(v,x):vs'} -- Check if the variable assignments satisfy all of the predicates. -- The partial argument determines the value used when a predicate returns -- Nothing because some variable it uses is not set. Setting this to True -- allows us to accept partial solutions, then we can use a value of -- False at the end to signify that all solutions should be complete. isConsistent :: Bool -> NDS Bool isConsistent partial = do cs <- gets constraints vs <- gets vars let results = map (\p->check p vs) cs return $ and (map (maybe partial id) results)
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- Return only the variable bindings that are complete consistent solutions. getFinalVars :: NDS Variables getFinalVars = do c <- isConsistent False guard c gets vars -- Get the first solution to the problem, by evaluating the solver computation with -- an initial problem state and then returning the first solution in the result list, -- or Nothing if there was no solution. getSolution :: NDS a -> ProblemState -> Maybe a getSolution c i = listToMaybe (evalStateT c i) -- Get a list of all possible solutions to the problem by evaluating the solver -- computation with an initial problem state. getAllSolutions :: NDS a -> ProblemState -> [a] getAllSolutions c i = evalStateT c i
We are ready to apply the predicate language and stateful nondeterministic monad to solving a logic problem. For this example, we will use the well-known Kalotan puzzle which appeared in Mathematical Brain-Teasers, Dover Publications (1976), by J. A. H. Hunter. The Kalotans are a tribe with a peculiar quirk: their males always tell the truth. Their females never make two consecutive true statements, or two consecutive untrue statements. An anthropologist (let's call him Worf) has begun to study them. Worf does not yet know the Kalotan language. One day, he meets a Kalotan (heterosexual) couple and their child Kibi. Worf asks Kibi: ``Are you a boy?'' The kid answers in Kalotan, which of course Worf doesn't understand. Worf turns to the parents (who know English) for explanation. One of them says:
Лист
ФП 02005-03 01 ИзмЛист № докум.
65
Подп. Дата Копирова
Формат
ФП 02005-03 01
"Kibi said: `I am a boy.'" The other adds: "Kibi is a girl. Kibi lied." Solve for the sex of Kibi and the sex of each parent. We will need some additional predicates specific to this puzzle, and to define the universe of allowed variables values: Code available in example24.hs -- if a male says something, it must be true said :: Var -> Predicate -> Predicate said v p = (v `Is` "male") `implies` p -- if a male says two things, they must be true -- if a female says two things, one must be true and one must be false saidBoth :: Var -> Predicate -> Predicate -> Predicate saidBoth v p1 p2 = And ((v `Is` "male") `implies` (p1 `And` p2)) ((v `Is` "female") `implies` (p1 `orElse` p2))
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- lying is saying something is true when it isn't or saying something isn't true when it is lied :: Var -> Predicate -> Predicate lied v p = ((v `said` p) `And` (Not p)) `orElse` ((v `said` (Not p)) `And` p) -- Test consistency over all allowed settings of the variable. tryAllValues :: Var -> NDS () tryAllValues var = do (setVar var "male") `mplus` (setVar var "female") c <- isConsistent True guard c
All that remains to be done is to define the puzzle in the predicate language and get a solution that satisfies all of the predicates: Code available in example24.hs -- Define the problem, try all of the variable assignments and print a solution. main :: IO () main = do let variables = [] constraints = [ Not (Equal "parent1" "parent2"), "parent1" `said` ("child" `said` ("child" `Is` "male")), saidBoth "parent2" ("child" `Is` "female") ("child" `lied` ("child" `Is` "male")) ] problem = PS variables constraints print $ (`getSolution` problem) $ do tryAllValues "parent1" tryAllValues "parent2" tryAllValues "child" getFinalVars
Лист
ФП 02005-03 01 ИзмЛист № докум.
66
Подп. Дата Копирова
Формат
ФП 02005-03 01
Each call to tryAllValues will fork the solution space, assigning the named variable to be "male" in one fork and "female" in the other. The forks which produce inconsistent variable assignments are eliminated (using the guard function). The call to getFinalVars applies guard again to eliminate inconsistent variable assignments and returns the remaining assignments as the value of the computation.
3.7. Managing the transformer stack As the number of monads combined together increases, it becomes increasingly important to manage the stack of monad transformers well.
3.7.1. Selecting the correct order
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
Once you have decided on the monad features you need, you must choose the correct order in which to apply the monad transformers to achieve the results you want. For instance you may know that you want a combined monad that is an instance of MonadError and MonadState, but should you apply StateT to the Error monad or ErrorT to the State monad? The decision depends on the exact semantics you want for your combined monad. Applying StateT to the Error monad gives a state transformer function of type s -> Error e (a,s). Applying ErrorT to the State monad gives a state transformer function of type s -> (Error e a,s). Which order to choose depends on the role of errors in your computation. If an error means no state could be produced, you would apply StateT to Error. If an error means no value could be produced, but the state remains valid, then you would apply ErrorT to State. Choosing the correct order requires understanding the transformation carried out by each monad transformer, and how that transformation affects the semantics of the combined monad.
3.7.2. An example with multiple transformers The following example demonstrates the use of multiple monad transformers. The code uses the StateT monad transformer along with the List monad to produce a combined monad for doing stateful nondeterministic computations. In this case, however, we have added the WriterT monad transformer to perform logging during the computation. The problem we will apply this monad to is the famous N-queens problem: to place N queens on a chess board so that no queen can attack another. The first decision is in what order to apply the monad transformers. StateT s (WriterT w []) yields a type like: s -> [((a,s),w)]. WriterT w (StateT s []) yields a type like: s -> [((a,w),s)]. In this case, there is little difference between the two orders, so we will choose the second arbitrarily.
Лист
ФП 02005-03 01 ИзмЛист № докум.
67
Подп. Дата Копирова
Формат
ФП 02005-03 01
Our combined monad is an instance of both MonadState and MonadWriter, so we can freely mix use of get, put, and tell in our monadic computations. Code available in example25.hs -- this is the type of our problem description data NQueensProblem = NQP {board::Board, ranks::[Rank], files::[File], asc::[Diagonal], desc::[Diagonal]} -- initial state = empty board, all ranks, files, and diagonals free initialState = let fileA = map (\r->Pos A r) [1..8] rank8 = map (\f->Pos f 8) [A .. H] rank1 = map (\f->Pos f 1) [A .. H] asc = map Ascending (nub (fileA ++ rank1)) desc = map Descending (nub (fileA ++ rank8)) in NQP (Board []) [1..8] [A .. H] asc desc
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- this is our combined monad type for this problem type NDS a = WriterT [String] (StateT NQueensProblem []) a -- Get the first solution to the problem, by evaluating the solver computation with -- an initial problem state and then returning the first solution in the result list, -- or Nothing if there was no solution. getSolution :: NDS a -> NQueensProblem -> Maybe (a,[String]) getSolution c i = listToMaybe (evalStateT (runWriterT c) i) -- add a Queen to the board in a specific position addQueen :: Position -> NDS () addQueen p = do (Board b) <- gets board rs <- gets ranks fs <- gets files as <- gets asc ds <- gets desc let b' = (Piece Black Queen, p):b rs' = delete (rank p) rs fs' = delete (file p) fs (a,d) = getDiags p as' = delete a as ds' = delete d ds tell ["Added Queen at " ++ (show p)] put (NQP (Board b') rs' fs' as' ds') -- test if a position is in the set of allowed diagonals inDiags :: Position -> NDS Bool inDiags p = do let (a,d) = getDiags p as <- gets asc
Лист
ФП 02005-03 01 ИзмЛист № докум.
68
Подп. Дата Копирова
Формат
ФП 02005-03 01
ds <- gets desc return $ (elem a as) && (elem d ds) -- add a Queen to the board in all allowed positions addQueens :: NDS () addQueens = do rs <- gets ranks fs <- gets files allowed <- filterM inDiags [Pos f r | f <- fs, r <- rs] tell [show (length allowed) ++ " possible choices"] msum (map addQueen allowed)
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
-- Start with an empty chess board and add the requested number of queens, -- then get the board and print the solution along with the log main :: IO () main = do args <- getArgs let n = read (args!!0) cmds = replicate n addQueens sol = (`getSolution` initialState) $ do sequence_ cmds gets board case sol of Just (b,l) -> do putStr $ show b -- show the solution putStr $ unlines l -- show the log Nothing -> putStrLn "No solution"
The program operates in a similar manner to the previous example, which solved the kalotan puzzle. In this example, however, we do not test for consistency using the guard function. Instead, we only create branches that correspond to allowed queen positions. We use the added logging facility to log the number of possible choices at each step and the position in which the queen was placed.
3.7.3. Heavy lifting There is one subtle problem remaining with our use of multiple monad transformers. Did you notice that all of the computations in the previous example are done in the combined monad, even if they only used features of one monad? The code for these functions in tied unneccessarily to the definition of the combined monad, which decreases their reusability. This is where the lift function from the MonadTrans class comes into its own. The lift function gives us the ability to write our code in a clear, modular, reusable manner and then lift the computations into the combined monad as needed. Instead of writing brittle code like: logString :: String -> StateT MyState (WriterT [String] []) Int logString s = ...
we can write clearer, more flexible code like:
Инв. № подл.
logString :: (MonadWriter [String] m) => String -> m Int
Лист
ФП 02005-03 01 ИзмЛист № докум.
69
Подп. Дата Копирова
Формат
ФП 02005-03 01
logString s = ...
and then lift the logString computation into the combined monad when we use it. You may need to use the compiler flags -fglasgow-exts with GHC or the equivalent flags with your Haskell compiler to use this technique. The issue is that m in the constraint above is a type constructor, not a type, and this is not supported in standard Haskell 98. When using lifting with complex transformer stacks, you may find yourself composing multiple lifts, like lift . lift . lift $ f x. This can become hard to follow, and if the transformer stack changes (perhaps you add ErrorT into the mix) the lifting may need to be changed all over the code. A good practice to prevent this is to declare helper functions with informative names to do the lifting: liftListToState = lift . lift . lift
Then, the code is more informative and if the transformer stack changes, the impact on the lifting code is confined to a small number of these helper functions.
Подп. и дата
The hardest part about lifting is understanding the semantics of lifting computations, since this depends on the details of the inner monad and the transformers in the stack. As a final task, try to understand the different roles that lifting plays in the following example code. Can you predict what the output of the program will be? Code available in example26.hs -- this is our combined monad type for this problem type NDS a = StateT Int (WriterT [String] []) a
Подп. и дата
Взам. инв. № Инв. № дубл.
{- Here is a computation on lists -} -- return the digits of a number as a list getDigits :: Int -> [Int] getDigits n = let s = (show n) in map digitToInt s {- Here are some computations in MonadWriter -} -- write a value to a log and return that value logVal :: (MonadWriter [String] m) => Int -> m Int logVal n = do tell ["logVal: " ++ (show n)] return n -- do a logging computation and return the length of the log it wrote getLogLength :: (MonadWriter [[a]] m) => m b -> m Int getLogLength c = do (_,l) <- listen $ c return (length (concat l))
Инв. № подл.
-- log a string value and return 0
Лист
ФП 02005-03 01 ИзмЛист № докум.
70
Подп. Дата Копирова
Формат
ФП 02005-03 01
logString :: (MonadWriter [String] m) => String -> m Int logString s = do tell ["logString: " ++ s] return 0 {- Here is a computation that requires a WriterT [String] [] -} -- "Fork" the computation and log each list item in a different branch. logEach :: (Show a) => [a] -> WriterT [String] [] a logEach xs = do x <- lift xs tell ["logEach: " ++ (show x)] return x {- Here is a computation in MonadState -} -- increment the state by a specified value addVal :: (MonadState Int m) => Int -> m () addVal n = do x <- get put (x+n)
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
{- Here are some computations in the combined monad -} -- set the state to a given value, and log that value setVal :: Int -> NDS () setVal n = do x <- lift $ logVal n put x -- "Fork" the computation, adding a different digit to the state in each branch. -- Because setVal is used, the new values are logged as well. addDigits :: Int -> NDS () addDigits n = do x <- get y <- lift . lift $ getDigits n setVal (x+y) {- an equivalent construction is: addDigits :: Int -> NDS () addDigits n = do x <- get msum (map (\i->setVal (x+i)) (getDigits n)) -} {- This is an example of a helper function that can be used to put all of the lifting logic in one location and provide more informative names. This has the advantage that if the transformer stack changes in the future (say, to add ErrorT) the changes to the existing lifting logic are confined to a small number of functions. -}
Лист
ФП 02005-03 01 ИзмЛист № докум.
71
Подп. Дата Копирова
Формат
ФП 02005-03 01
liftListToNDS :: [a] -> NDS a liftListToNDS = lift . lift -- perform a series of computations in the combined monad, lifting computations from other -- monads as necessary. main :: IO () main = do mapM_ print $ runWriterT $ (`evalStateT` 0) $ do x <- lift $ getLogLength $ logString "hello" addDigits x x <- lift $ logEach [1,3,5] lift $ logVal x liftListToNDS $ getDigits 287
Once you fully understand how the various lifts in the example work and how lifting promotes code reuse, you are ready for real-world monadic programming. All that is left to do is to hone your skills writing real software. Happy hacking!
Подп. и дата
If you discover any errors — no matter how small — in this document, or if you have suggestions for how it can be improved, please write to the author at
[email protected].
Инв. № подл.
Подп. и дата
This brings us to the end of this tutorial. If you want to continue learning about the mathematical foundations of monads, there are numerous category theory resources on the internet. For more examples of monads and their applications in the real world, you might want to explore the design of the Parsec monadic parser combinator library and/or the QuickCheck testing tool. You may also be interested in arrows, which are similar to monads but more general.
Взам. инв. № Инв. № дубл.
3.8. Continuing Exploration
Лист
ФП 02005-03 01 ИзмЛист № докум.
72
Подп. Дата Копирова
Формат
ФП 02005-03 01
Приложение A (обязательное)
A PHYSICAL ANALOGY FOR MONADS Because monads are such abstract entities, it is sometimes useful to think about a physical system that is analogous to a monad instead of thinking about monads directly. In this way, we can use our physical intuition and experiences to gain insights that we can relate back to the abstract world of computational monads.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
The particular physical analogy developed here is that of a mechanized assembly line. It is not a perfect fit for monads — especially with some of the higher-order aspects of monadic computation — but I believe it could be helpful to gain the initial understanding of how a monad works. Begin by thinking about a Haskell program as a conveyor belt. Input goes on end of the conveyor belt and is carried to a succession of work areas. At each work area, some operation is performed on the item on the conveyor belt and the result is carried by the conveyor belt to the next work area. Finally, the conveyor belt carries the final product to the end of the assembly line to be output. In this assembly line model, our physical monad is a system of machines that controls how successive work areas on the assembly line combine their functionality to create the overall product. Our monad consists of three parts: 1) Trays that hold work products as they move along the conveyor belt. 2) Loader machines that can put any object into a tray. 3) Combiner machines that can take a tray with an object and produce a tray with a new object. These combiner machines are attached to worker machines that actualy produce the new objects. We use the monad by setting up our assembly line as a loader machine which puts materials into trays at the beginning of the assembly line. The conveyor belt then carries these trays to each work area, where a combiner machine takes the tray and may decide based on its contents whether to run them through a worker machine, as shown in Figure A-1.
Лист
ФП 02005-03 01 ИзмЛист № докум.
73
Подп. Дата Копирова
Формат
ФП 02005-03 01
Figure A-1. An assembly line using a monad architecture
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
The important thing to notice about the monadic assembly line is that it separates out the work of combining the output of the worker machines from the actual work done by the worker machines. Once they are separated, we can vary them independently. So the same combiner machines could be used on an assembly line to make airplanes and an assembly line to make chopsticks. Likewise, the same worker machines could be used with different combiners to alter the way the final product is produced. Lets take the example of an assembly line to make chopsticks, and see how it is handled in our physical analogy and how me might represent it as a program in Haskell. We will have three worker machines. The first takes small pieces of wood as input and outputs a tray containing a pair of roughly shaped chopsticks. The second takes a pair of roughly shaped chopsticks and outputs a tray containing a pair of smooth, polished chopsticks with the name of the restaurant printed on them. The third takes a pair of polished chopsticks and outputs a tray containing a finished pair of chopsticks in a printed paper wrapper. We could represent this in Haskell as: -- the basic types we are dealing with type Wood = ... type Chopsticks = ... data Wrapper x = Wrapper x -- NOTE: the Tray type comes from the Tray monad -- worker function 1: makes roughly shaped chopsticks makeChopsticks :: Wood -> Tray Chopsticks makeChopsticks w = ... -- worker function 2: polishes chopsticks polishChopsticks :: Chopsticks -> Tray Chopsticks polishChopsticks c = ... -- worker function 3: wraps chopsticks wrapChopsticks :: Chopsticks -> Tray Wrapper Chopsticks wrapChopsticks c = ...
Лист
ФП 02005-03 01 ИзмЛист № докум.
74
Подп. Дата Копирова
Формат
ФП 02005-03 01
It is clear that the worker machines contain all of the functionality needed to produce chopsticks. What is missing is the specification of the trays, loader, and combiner machines that collectively make up the Tray monad. Our trays should either be empty or contain a single item. Our loader machine would simply take an item and place it in a tray on the conveyor belt. The combiner machine would take each input tray and pass along empty trays while feeding the contents of non-empty trays to its worker machine. In Haskell, we would define the Tray monad as: -- trays are either empty or contain a single item data Tray x = Empty | Contains x -- Tray is a monad instance Monad Tray where Empty >>= _ (Contains x) >>= worker return fail _
= = = =
Empty worker x Contains Empty
You may recognize the Tray monad as a disguised version of the Maybe monad that is a standard part of Haskell 98 library.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
All that remains is to sequence the worker machines together using the loader and combiner machines to make a complete assembly line, as shown in Figure A-2.
Figure A-2. A complete assembly line for making chopsticks using a monadic approach.
In Haskell, the sequencing can be done using the standard monadic functions: assemblyLine :: Wood -> Tray Wrapped Chopsticks assemblyLine w = (return w) >>= makeChopsticks >>= polishChopsticks >>= wrapChopsticks
or using the built in Haskell "do" notation for monads: assemblyLine :: Wood -> assemblyLine w = do c c' c''
Tray Wrapped Chopsticks <- makeChopsticks w <- polishChopsticks c <- wrapChopsticks c'
Лист
ФП 02005-03 01 ИзмЛист № докум.
75
Подп. Дата Копирова
Формат
ФП 02005-03 01
return c''
So far, you have seen how monads are like a framework for building assembly lines, but you probably haven't been overawed by their utility. To see why we might want to build our assembly line using the monadic approach, consider what would happen if we wanted to change the manufacturing process. Right now, when a worker machine malfunctions, it uses the fail routine to produce an empty tray. The fail routine takes an argument describing the failure, but our Tray type ignores this and simply produces an empty tray. This empty tray travels down the assembly line and the combiner machines allow it to bypass the remaining worker machines. It eventually reaches the end of the assembly line, where it is brought to you, the quality control engineer. It is your job to figure out which machine failed, but all you have to go on is an empty tray.
To make the change, you simply create a new tray type that can never be empty. It will always either contain an item or it will contain a failure report describing the exact reason there is no item in the tray. -- tray2s either contain a single item or contain a failure report data Tray2 x = Contains x | Failed String -- Tray2 is a monad instance Monad Tray2 where (Failed reason) >>= _ (Contains x) >>= worker return fail reason
= = = =
Failed reason worker x Contains Failed reason
You may recognize the Tray2 monad as a disguised version of the Error monad that is a standard part of the Haskell 98 libraries. Replacing the Tray monad with the Tray2 monad instantly upgrades your assembly line. Now when a failure occurs, the tray that is brought to the quality control engineer contains a failure report detailing the exact cause of the failure!
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
You realize that your job would be much easier if you took advantage of the failure messages that are currently ignored by the fail routine in your Tray monad. Because your assembly line is organized around a monadic approach, it is easy for you to add this functionality to your assembly line without changing your worker machines.
Лист
ФП 02005-03 01 ИзмЛист № докум.
76
Подп. Дата Копирова
Формат
ФП 02005-03 01
Приложение B (обязательное)
HASKELL CODE EXAMPLES This appendix contains a list of all of the code examples supplied with the tutorial.
Example 1 This example is discussed in the section: Meet the monads. The example code introduces the monad concept without using Haskell typeclasses. It shows how a monadic combinator can be used to simplify the construction of computations from sequences of computations which may not return a result.
Example 2
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
This example is discussed in the section: Doing it with class. The example code builds on the first example, and shows how do-notation can be used with an instance of the Monad class (in this case, Maybe is the monad used).
Example 3 This example is discussed in the section: Monad support in Haskell. The example code builds on the first two examples, and shows a somewhat atypical — but very powerful — use of the foldM function outside of a do-block.
Example 4 This example is discussed in the section: Monad support in Haskell. The example code shows a more typical use of the foldM function within a do-block. It combines dictionary values read from different files into a single dictionary using the foldM function within the IO monad.
Example 5 This example is discussed in the section: Monad support in Haskell.
Лист
ФП 02005-03 01 ИзмЛист № докум.
77
Подп. Дата Копирова
Формат
ФП 02005-03 01
The example code shows the use of the filterM function within a do-block. It prints the subset of its arguments that specify directories and ignores non-directory arguments.
Example 6 This example is discussed in the section: Monad support in Haskell. The example code shows the use of the liftM function within a do-block. It looks up a name in a list and uses a lifted String manipulation function to modify it within the Maybe monad.
Example 7 This example is discussed in the section: Monad support in Haskell.
Взам. инв. № Инв. № дубл.
Подп. и дата
The example code shows a higher-order application of liftM2. It folds lifted operations within the List monad to produce lists of all combinations of elements combined with the lifted operator.
Example 8 This example is discussed in the section: Monad support in Haskell. The example code shows a higher-order application of ap. It folds ap through a list of Maybe (a->a) functions to process sequences of commands.
Example 9 This example is discussed in the section: Monad support in Haskell. The example code shows the use of msum in the Maybe monad to select the first variable match in a stack of binding environments.
Example 10
Инв. № подл.
Подп. и дата
This example is discussed in the section: Monad support in Haskell. The example code shows the use of guard in the Maybe monad to select only the records from a list that satisfy a predicate (equivalent to filter).
Лист
ФП 02005-03 01 ИзмЛист № докум.
78
Подп. Дата Копирова
Формат
ФП 02005-03 01
Example 11 This example is discussed in the section: The Maybe monad. The example code shows how to use the Maybe monad to build complex queries from simpler queries that may fail to return a result. The specific example used is looking up mail preferences for someone based on either their full name or a nickname.
Example 12 This example is discussed in the section: The Error monad. The example code demonstrates the use of the Either type constructor as an Error monad with a custom error type. The example parses hexadecimal digits and uses the exception handling mechanism of the Error monad to provide informative error messages in the event of a parse failure.
Example 13
Взам. инв. № Инв. № дубл.
Подп. и дата
This example is discussed in the section: The List monad. The example code uses the built-in list type constructor as a monad for non-deterministic computation. The example demonstrates parsing an ambiguous grammar consisting of integers, hex values, and words.
Example 14 This example is discussed in the section: The IO monad. The example code implements a simple version of the standard Unix command "tr". The example demonstrates use of the IO monad including implicit fail calls due to pattern matching failures and the use of catcherror.
Example 15
Инв. № подл.
Подп. и дата
This example is discussed in the section: The State monad. The example code shows how the State monad can be used instead of explicitly passing state. The example uses the State monad to manage the random number generator state while building a compound data value requiring multiple calls to the random number generator.
Лист
ФП 02005-03 01 ИзмЛист № докум.
79
Подп. Дата Копирова
Формат
ФП 02005-03 01
Example 16 This example is discussed in the section: The Reader monad. The example code shows how the Reader monad can be used to simplify computations involving a shared environment. The example uses the Reader monad to implement a simple template substitution system. The example code demonstrates the use of the Parsec monadic parser combinator library.
Example 17 This example is discussed in the section: The Writer monad. The example code shows how the Writer monad can be used to implement logging. The example implements a very simple firewall simulator and uses the Writer monad to log the firewall activity.
Example 18
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
This example is discussed in the section: The Continuation monad. The example code shows how the Continuation monad's escape continuations work. The example computes a complex transformation of a number.
Example 19 This example is discussed in the section: Combining monads the hard way. The example code shows how the Continuation monad can be nested within the IO monad given a suitable computational structure. The example is a slight modification of example 18.
Example 20 This example is discussed in the section: Combining monads the hard way. The example code shows how the Continuation monad and IO monad can be used simultaneously, but without using monad transformers. The example builds on examples 18 and 19.
Example 21
Инв. № подл.
This example is discussed in the section: Monad transformers.
Лист
ФП 02005-03 01 ИзмЛист № докум.
80
Подп. Дата Копирова
Формат
ФП 02005-03 01
The example code shows how the transformer version of the Continuation monad can be used to create a combined monad for using continuations and doing I/O. The example builds on examples 18, 19 and 20.
Example 22 This example is discussed in the section: Standard monad transformers. The example code shows how the transformer version of the Writer monad can be used to create a combined monad for logging and doing I/O. The example adds timestamps to the log entries of the firewall simulator from example 17.
Example 23 This example is discussed in the section: Standard monad transformers.
Инв. № подл.
Подп. и дата
Взам. инв. № Инв. № дубл.
Подп. и дата
The example code shows how the transformer version of the Reader monad can be used to create a monad that combines a shared environment with I/O. The example converts the template system of example 16 to use files as templates.
Example 24 This example is discussed in the section: Standard monad transformers. The example code uses the StateT transformer on the List monad to create a combined monad for doing non-deterministic stateful computations. The example uses the combined monad to solve a logic problem.
Example 25 This example is discussed in the section: An example with multiple monad transformers. The example code uses the StateT and WriterT transformers on the List monad to create a combined monad for doing non-deterministic stateful computations with logging. The example uses the combined monad to solve the N-queens problem.
Example 26 This example is discussed in the section: Heavy lifting. The example code demonstrates the use of the lift function and the necessity of managing its use in complex transformer stacks.
Лист
ФП 02005-03 01 ИзмЛист № докум.
81
Подп. Дата Копирова
Формат
Лист регистрации изменений Номера листов (страниц) Изм. изменённых
заменённых
новых
аннулированных
Всего листов (страниц) в докум.
№ документа
Входящий № сопроводительного докум. и дата
Подп.
Дата