ru haskell book

278
Учебник по Haskell Антон Холомьёв

Upload: aristarkh-anonimusov

Post on 20-Apr-2015

245 views

Category:

Documents


3 download

TRANSCRIPT

Page 1: Ru Haskell Book

Учебник по Haskell

Антон Холомьёв

Page 2: Ru Haskell Book

Книга зарегистрирована под лицензией Creative Commons Attribution-NonCommercial-NoDerivs3.0 Generic license (CC BY-NC-ND 3.0), 2012 год. Вы можете свободно распространять и копироватьэту книгу при условии указания автора. Вы не можете использовать эту книгу в коммерческихцелях, вы не можете изменять содержание книги при копировании или создавать производныеработы на основе содержания этой книги, конечно если это не программный код :) Любое изуказанных ограничений может быть смягчено по договорённости с правообладателем.

Обратная связь: [email protected]

Page 3: Ru Haskell Book

Оглавление

Предисловие 9Структура книги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9Основные понятия . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10Благодарности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

1 Основы 111.1 Общая картина . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

Типы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12Значения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14Классы типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16Контекст классов типов. Суперклассы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17Экземпляры классов типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

1.2 Ядро Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181.3 Двумерный синтаксис . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201.4 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

2 Первая программа 222.1 Интерпретатор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222.2 У-вей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222.3 Логические значения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242.4 Класс Show. Строки и символы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

Строки и символы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26Пример: Отображение дат и времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27

2.5 Автоматический вывод экземпляров классов типов . . . . . . . . . . . . . . . . . . . . . . . . 282.6 Арифметика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

Класс Eq. Сравнение на равенство . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29Класс Num. Сложение и умножение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30Стандартные числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

2.7 Документация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322.8 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322.9 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

3 Типы 343.1 Структура алгебраических типов данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 343.2 Структура констант . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

Несколько слов о теории графов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36Строчная запись деревьев . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38

3.3 Структура функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39Композиция и частичное применение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40Декомпозиция и сопоставление с образцом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

3.4 Проверка типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44Проверка типов с контекстом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

3.5 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463.6 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

3

Page 4: Ru Haskell Book

4 Декларативный и композиционный стиль 494.1 Локальные переменные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

where-выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49let-выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

4.2 Декомпозиция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51Сопоставление с образцом . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51case-выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

4.3 Условные выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52Охранные выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53if-выражения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54

4.4 Определение функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54Уравнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54Безымянные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

4.5 Какой стиль лучше? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 574.6 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 594.7 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

5 Функции высшего порядка 625.1 Приоритет инфиксных операций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 635.2 Обобщённые функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65

Функция тождества . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65Константная функция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65Функция композиции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66Аналогия с числами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67Функция применения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67Функция перестановки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68Функция on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68

5.3 Функциональный калькулятор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 695.4 Функции, возвращающие несколько значений . . . . . . . . . . . . . . . . . . . . . . . . . . . 70

Единичный тип . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70Пары . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71Композиция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

5.5 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 725.6 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

6 Специальные функции 746.1 Композиция функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

Класс Category . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75Специальные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75Взаимодействие с внешним миром . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75Три композиции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76Обобщённая формулировка категории Клейсли . . . . . . . . . . . . . . . . . . . . . . . . . . 76

6.2 Примеры специальных функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76Частично определённые функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76Многозначные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79

6.3 Применение функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81Применение функций многих переменных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82Несколько полезных функций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

6.4 Функторы и монады . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84Функторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84Аппликативные функторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85Монады . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85Свойства классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85Полное определение классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86Исторические замечания . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87

6.5 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 876.6 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

4

Page 5: Ru Haskell Book

7 Примеры из мира специальных функций 937.1 Случайные числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 957.2 Конечные автоматы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 967.3 Отложенное вычисление выражений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

Тип Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 997.4 Накопление результата . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101

Тип-обёртка newtype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101Записи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102Накопление чисел . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103Накопление логических значений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104Накопление списков . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

7.5 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1057.6 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

8 Ленивые вычисления 1088.1 Стратегии вычислений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

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

8.2 Реализация ленивых вычислений в ghc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114Форточка в мир вычислений по значению . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116Компиляция модулей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117Функции с хвостовой рекурсией . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117Тонкости применения seq . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118Взрывная декомпозиция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120Ленивее некуда . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

8.3 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1268.4 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

9 Ленивые чудеса 1289.1 Численные методы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

Дифференцирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128Интегрирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

9.2 Степенные ряды . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130Арифметика рядов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131Производная и интеграл . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132Элементарные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132

9.3 Водосборы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1339.4 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1359.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135

10 Структурная рекурсия 13610.1 Свёртка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

Логические значения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136Натуральные числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137Maybe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138Списки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138Деревья . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140

10.2 Развёртка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141Списки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141Потоки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142Натуральные числа . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142

10.3 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14310.4 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143

5

Page 6: Ru Haskell Book

11 IO 14511.1 Чистота и побочные эффекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14511.2 Монада IO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14611.3 Как пишутся программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14711.4 Типичные задачи IO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148

Вывод на экран . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148Ввод пользователя . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149Чтение и запись файлов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149Аргументы программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150Вызов других программ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151Случайные значения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151Исключения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154Потоки текстовых данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155

11.5 Форточка в мир побочных эффектов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156Отладка программ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

11.6 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15711.7 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157

12 Поиграем 15912.1 Стратегия написания программ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159

Описание задачи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159Набросок решения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159Каркас. Типы и классы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160Ленивое программирование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

12.2 Пятнашки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163Цикл игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163Приведём код в порядок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165Формат запросов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166Последние штрихи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168Правила игры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168

12.3 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172

13 Лямбда-исчисление 17313.1 Лямбда исчисление без типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173

Составление термов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173Абстракция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175Редукция. Вычисление термов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175Рекурсия. Комбинатор неподвижной точки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177Кодирование структур данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177Конструктивная математика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178Расширение лямбда исчисления . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179

13.2 Комбинаторная логика . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179Связь с лямбда-исчислением . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180Немного истории . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181

13.3 Лямбда-исчисление с типами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18113.4 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18213.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

14 Теория категорий 18314.1 Категория . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18314.2 Функтор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18514.3 Естественное преобразование . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18614.4 Монады . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188

Категория Клейсли . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18914.5 Дуальность . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18914.6 Начальный и конечный объекты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190

Начальный объект . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190Конечный объект . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191

14.7 Сумма и произведение . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19214.8 Экспонента . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19414.9 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19514.10Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195

6

Page 7: Ru Haskell Book

15 Категориальные типы 19715.1 Программирование в стиле оригами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19715.2 Индуктивные и коиндуктивные типы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201

Существование начальных и конечных объектов . . . . . . . . . . . . . . . . . . . . . . . . . . 20215.3 Хиломорфизм . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20415.4 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20715.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207

16 Дополнительные возможности 20816.1 Пуд сахара . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208

Сахар для списков . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208Сахар для монад, do-нотация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209

16.2 Расширения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211Обобщённые алгебраические типы данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212Семейства типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215Расширения для классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217Ограничение мономорфизма . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218

16.3 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21916.4 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219

17 Средства разработки 22017.1 Пакеты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220

Создание пакетов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220Создаём библиотеки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221Создаём исполняемые программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221Установка пакета . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222Удаление библиотеки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224Репозиторий пакетов Hackage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224Дополнительные атрибуты пакета . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225

17.2 Создание документации с помощью Haddock . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225Комментарии к определениям . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225Комментарии к модулю . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226Структура страницы документации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226Разметка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227

17.3 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22917.4 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229

18 Ориентируемся по карте 23018.1 Алгоритм эвристического поиска А* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231

Поиск маршрутов в метро . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23518.2 Тестирование с помощью QuickCheck . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235

Формирование тестовой выборки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237Классификация тестовых случаев . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238

18.3 Оценка шустродействия с помощью criterion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238Основные типы criterion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240

18.4 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24218.5 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242

19 Императивное программирование 24319.1 Основные библиотеки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243

Изменяемые значения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244OpenGL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245Chipmunk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250

19.2 Боремся с IO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25419.3 Определяемся с типами . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25619.4 Структура проекта . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25719.5 Детализируем функции обновления состояния игры . . . . . . . . . . . . . . . . . . . . . . . . 25819.6 Детализируем дальше . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25919.7 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25919.8 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259

7

Page 8: Ru Haskell Book

20 Музыкальный пример 26020.1 Музыкальная нотация . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260

Нотная запись в европейской традиции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260Протокол midi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 261

20.2 Музыкальная запись в виде событий . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262Преобразование событий во времени . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263Композиция треков . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263Экземпляры стандартных классов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264

20.3 Ноты в midi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264Синонимы для нот . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265

20.4 Перевод в midi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26620.5 Пример . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27020.6 Эффективное представление музыкальной нотации . . . . . . . . . . . . . . . . . . . . . . . . 27120.7 Краткое содержание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27120.8 Упражнения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272

Приложения 273Начало работы с Haskell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273Литература . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274

Книги . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274Тематический сборник . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274И все-все-все . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276

Обзор Hackage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277Стандартные библиотеки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277Эффективные типы данных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278И все-все-все . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278

8

Page 9: Ru Haskell Book

Предисловие

История языка Haskell начинается в 1987 году. В 1980е годы наблюдался всплеск интереса к ленивойстратегии вычислений. Один за другим появлялись новые функциональные языки программирования. Про-граммисты задумались и решили, объединив усилия, найти общий язык. Так появился Haskell. Он был названв честь одного из основателей комбинаторной логики Хаскеля Кэрри (Haskell Curry).Новый язык должен был стать свободным языком, пригодным для исследовательской деятельности и

решения практических задач. Свободные языки основаны на стандарте, который формулируется комите-том разработчиков. Дальше любой желающий может заняться реализацией стандарта, написать компиляторязыка. Первая версия стандарта была опубликована 1 апреля 1990 года. Haskell продолжает развиватьсяи сегодня, было зафиксировано два стандарта: 1998 и 2010 года. Это стабильные версии. Но кроме них вHaskell включается множество расширений, проходит обкат интересных идей. Сегодня Haskell переживаетбурный рост, к сожалению, эпицентры далеки от России, это Англия, Нидерланды, Америка и Австралия. Ин-терес к Haskell вызван популярностью многопроцессорных технологий. Модель вычислений Haskell хорошоподходит для распараллеливания. И сейчас проводятся исследования в этой области.Haskell очень компактный и красивый язык. Он придётся по душе математикам, программистам, склон-

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

Структура книгиHaskell славится высоким порогом вхождения. Он считается трудным языком для начинающих. Во многом

это связано с тем, что начинающие уже имеют приличный опыт программирования на императивных языках.И при первом знакомстве оказывается, что этот опыт ничем не может им помочь. Они не могут найти в Haskellаналогов привычных синтаксических конструкций и приёмов программирования. Haskell сильно отличаетсяот распространённых языков программирования. Но если вы совсем-совсем начинающий, скорее всего в этомплане вам будет гораздо проще. Если вы всё же не начинающий, попробуйте подойти к материалу этой книгис открытым сердцем. Не ищите в Haskell элементы вашего любимого языка и, возможно, таким языком станетHaskell.Ещё одна трудность связана с тем, что многие понятия тесно переплетены, Haskell не так просто разбить

на маленькие части и изучать их от простого к сложному, уже в самых простейших элементах кроются чертыновых и непривычных идей. Но, я надеюсь, что мы сможем преодолеть и этот барьер, мы не будем изучатьHaskell по кусочкам, а окунёмся в него с головой, уже в первой главе мы пробежимся по всему языку и далеебудем углубляться в отдельные моменты.В книге много примеров. Haskell оснащён не только компилятором, но и интерпретатором. Интерпрета-

тор это такая программа, которая позволяет писать программы в диалоговом режиме. Мы набираем выраже-ние языка и сразу видим ответ – вычисленное значение. Интерпретатор поможет нам разобраться во многихтонкостях языка. Мы будем обращаться к нему очень часто.Книгу можно разбить на несколько частей:• Основы языка (1-12). Из первых двенадцати глав вы узнаете, что такое Haskell и чем он хорош.• Теоретическая часть (13-15). Haskell питается соками математики, многие красивые научные идеи нетолько находят в нём воплощение, но и являются фундаментом языка. Из этих глав вы узнаете немноготеории, которая служила источником вдохновения разработчиков Haskell.• Разработка на Haskell (16-19). В этих главах мы познакомимся с расширениями языка (16), мы узнаемкак писать библиотеки и документацию (17), проводить тестирование и оценивать быстродействиепрограмм (18), также мы потренируемся в написании императивного кода на Haskell (19).• Примеры (12, 20). В этих главах мы посмотрим на несколько примеров применения Haskell. В глваве12 мы напишем программу для игры в пятнашки, а в главе 20 – midi-секвенсор и немного музыки.

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

Предисловие | 9

Page 10: Ru Haskell Book

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

слова функция, типы, значения, типы, функция, функция, типы – буквально постоянно. Перед тем как мыокунёмся с головой в программный код, я бы хотел словами пояснить, что всё это значит.Мы собираемся изучить новый язык, хоть и искусственный, но всё же язык. Языки служат описанию яв-

лений, словами мы можем зафиксировать мысли и чувства и передать их другому. Предложение языка опи-сывает что-то. У нас будут два разных вида описаний. Одни говорят о чём-то конкретном, их мы будемназывать значениями, а другие говорят о самих описаниях. Например это слова ”числа”, ”цвета” или ”люди”.Есть конкретное число: один два или три, а есть все числа. Такие описания мы будем называть типами. Типыописывают множество значений. Функции описывают одни значения через другие. Это такие шаблоны описа-ний. Типичный пример функции, это ”вычисление площади треугольника”. Функция как бы говорит: если тымне покажешь треугольник, то я тебе скажу его площадь (число). Функция ”вычисление площади треуголь-ника” связывает два типа между собой: тип всех треугольников и тип чисел (значение площади). Могут бытьи не математические функции. Например функция ”цвет глаз” говорит нам: если ты покажешь мне челове-ка, то я скажу какого цвета у него глаза. Эта функция связывает тип ”люди” и тип ”цвет”. При этом связьимеет направление. Функция сначала спрашивает у нас, чего ей не хватает, а потом говорит ответ. Ответназывают значением функции (или выходом функции), а то чего ей не хватает аргументами функции (иливходами). Математики говорят, что эта функция отображает значения типа ”люди” в значения типа ”цвет”.В Haskell функции тоже являются значениями. Функция может принимать в качестве аргумента функцию ивозвращать функцию.Функции бывают чистыми и с побочными эффектами. Чистые функции – это правдивые функции. Их основ-

ная особенность в том, что для одинаковых ответов на их вопросы, они скажут одинаковые ответы. Функциис побочными эффектами так не делают, например если мы спросим у такой функции какого цвета глаза уКоли? В один день она может сказать голубые, а в другой зелёные. В Haskell таким функциям не доверяют иогораживают их от чистых функций, но я увлёкся, обо всём об этом вы узнаете из этой книги.

БлагодарностиЯ бы хотел поблагодарить родителей за терпение и поддержку, сообщество Haskell, всех тех людей, у кото-

рых я мог свободно учится языку Haskell. Когда я только начинал мне очень помогли книга Мирана Липовача(Miran Lipovaca) Learn You A Haskell for a Great Good и книга Хал Дама (Hal Daume III) Yet another HaskellTutorial. Спасибо Дмитрию Астапову, Дугласу Мак Илрою (Douglas McIlroy) и Джону Хьюзу (John Huges) завеликодушное согласие на использование примеров из их статей. Большое спасибо Кате Столяровой за идеюнаписания книги. Спасибо Оксане Станевич за редактирование и правки первой главы.Технические благодарности: команде GHC, за компилятор Haskell, команде TexLive, авторам XeLatex,

авторам пакетов diagrams для LaTeX (Пол Тэйлор (Paul Taylor)) и diagrams для Haskell (Брент Йорги (BrentYorgey), Райан Йэйтс (Ryan Yates)), все картинки нарисованы в этих приложениях, автору пакета mintedКонараду Рудольфу (Konrad Rudolph) и авторам Pygments за подсветку синтаксиса в LaTeX, авторам пакетовc Hackage: QuickCheck (Коэн Клаессен (Koen Claessen), Бьорн Брингерт (Bjorn Bringert), Ник Смолбоун (NickSmallbone)), criterion (Брайан О’Салливан (Bryan O’Sullivan)), HCodecs (Джордж Гиоргадзе (George Giorgidze)),fingertree (Росс Патерсон (Ross Paterson), Ральф Хинце (Ralf Hinze)), Hipmunk (Фелипе Лесса (Felipe A. Lessa)),OpenGL (Джэйсон Даджит (Jason Dagit), Свен Пэнн (Sven Panne) и GLFW (Пол Лю (Paul H. Liu), Марк Санет(Marc Sunet)).

10 | Предисловие

Page 11: Ru Haskell Book

Глава 1

Основы

Есть мнение, что Haskell очень большой язык. Это и правда так. В Haskell много разных конструкций,синтаксического сахара, которые делают код более наглядным. Также в Haskell много библиотек на раз-ные случаи жизни. Однако, обману ли я ваши ожидания, сказав, что всё это имеет достаточно компактнуюоснову? Это и правда так, вам осталось лишь убедиться в наглядности и простоте Haskell. В этой главе мыпробежимся по нему, охватив одним взглядом целиком весь язык. Несколько наглядных конструкций, немно-го моих пояснений, и вы поймёте, что к чему. Если что-то сразу не станет ясно, или где-то я опущу какие-топояснения, будьте уверены – в следующих главах мы обязательно обратимся к этим моментам и обсудим ихподробнее.

1.1 Общая картинаПрограммы на Haskell бывают двух видов: это приложения (executable) и библиотеки (library). Приложе-

ния представляют собой исполняемые файлы, которые решают некоторую задачу, к примеру – это можетбыть компилятор языка, сортировщик данных в директориях, календарь, или цитатник на каждый день, лю-бая полезная утилита. Библиотеки тоже решают задачи, но решают их внутри самого языка. Они содержатотдельные значения, функции, которые можно подключать к другой программе Haskell, и которыми можнопользоваться.Программа состоит из модулей (module). И здесь работает правило: один модуль – один файл. Имя модуля

совпадает с именем файла. Имя модуля начинается с большой буквы, тогда как файлы имеют расширение.hs. Например FirstModule.hs. Посмотрим на типичный модуль в Haskell:---------------------------------------- шапка

module Имя(определение1, определение2,..., определениеN) where

import Модуль1(...)import Модуль2(...)...

----------------------------------------- определения

определение1определение2...

Каждый модуль содержит набор определений. Относительно модуля определения делятся на экспорти-руемые и внутренние. Экспортируемые определения могут быть использованы за пределами модуля, а внут-ренние – только внутри модуля, и обычно они служат для выражения экспортируемых определений.Модуль состоит из двух частей – шапки и определений.

ШапкаВ шапке после слова module объявляется имя модуля, за которым в скобках следует список экспорти-руемых определений; после скобок стоит слово where. Затем идут импортируемые модули. С помощьюимпорта модулей вы имеете возможность в данном модуле пользоваться определениями из другогомодуля.Как после имени модуля, так и в директиве import скобки с определениями можно не писать,так как вэтом случае считается, что экспортируются/импортируются все определения.

| 11

Page 12: Ru Haskell Book

ОпределенияЭта часть содержит все определения модуля, при этом порядок следования определений не имеет зна-чения. То есть, не обязательно пользоваться в данной функции лишь теми значениями, что были опре-делены выше.Модули взаимодействуют друг с другом с помощью экспортируемых определений. Один модуль может

сказать, что он хочет воспользоваться экспортируемыми определениями другого модуля, для этого он пишетimport Модуль(определения). Модуль – это айсберг, на вершине которого – те функции, ради которых онсоздавался (экспортируемые), а под водой – все служебные детали реализации (внутренние).Итак, программа состоит из модулей, модули состоят из определений. Но что такое определения?В Haskell определения могут описывать четыре вида сущностей:

• Типы.• Значения.• Классы типов.• Экземпляры классов типов.

Теперь давайте рассмотрим их подробнее.

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

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

not :: Bool -> Bool

Выражение v :: T означает, что значение v имеет тип T. Стрелка a -> b означает функцию, то есть из aмыможем получить b. Итак, перед нами функция из Bool в Bool, под названием not. Мы можем предположить,что это логическая операция ”не”. Или, перед нами такое определение типа:

reverse :: [a] -> [a]

Мы видим функцию с именем reverse, которая принимает список [a] и возвращает список [a], и мыможем догадаться, что эта функция переворачивает список, то есть мы получаем список, у которого элементыидут в обратном порядке. Маленькая буква a в [a] является параметром типа, на место параметра может бытьпоставлен любой тип. Она говорит о том, что список содержит элементы типа a. Например, такая функциясоглашается переворачивать только списки логических значений:

reverseBool :: [Bool] -> [Bool]

Программа представляет собой описание некоторого явления или процесса. Типы определяют основныеслова или термины и способы их комбинирования. А значения представляют собой комбинации базовыхслов. Но значения комбинируются не произвольным образом, а на основе определённых правил, которыезадаются типами.Например, такое выражение определяет тип, в котором два базовых термина True или False

data Bool = True | False

Слово data ключевое, с него начинается любое определение нового типа. Символ | означает или. Нашновый тип Bool является либо словом True, либо словом False. В этом типе есть только понятия, но нетспособов комбинирования, посмотрим на тип, в котором есть и то, и другое:

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

Это определение списка. Как мы уже поняли, a – это параметр. Список [a] может быть либо пустымсписком [], либо комбинацией a : [a]. В этой комбинации знак : объединяет элемент типа a и ещё одинсписок [a]. Это рекурсивное определение, они встречаются в Haskell очень часто. Если это пока кажетсянепонятным, не пугайтесь, в следующих главах будет представлено много примеров с пояснениями.Приведём ещё несколько примеров определений; ниже типы определяют базовые понятия для мира ка-

лендаря: то что стоит за -- является комментарием и игнорируется при выполнении программы:

12 | Глава 1: Основы

Page 13: Ru Haskell Book

-- Датаdata Date = Date Year Month Day

-- Годdata Year = Year Int -- Int это целые числа

-- Месяцdata Month = January | February | March | April

| May | June | July | August| September | October | November | December

data Day = Day Int

-- Неделяdata Week = Monday | Tuesday | Wednesday

| Thursday | Friday | Saturday| Sunday

-- Времяdata Time = Time Hour Minute Second

data Hour = Hour Int -- Часdata Minute = Minute Int -- Минутаdata Second = Second Int -- Секунда

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

Так, фраза

data Time = Time Hour Minute Second

означает, что тип Time – это значение с меткой Time, которое состоит из значений типов ”час”, ”время” и”секунда”, и больше ничего. Метку принято называть конструктором.Фраза

data Year = Year Int

означает, что тип Year – это значение с конструктором Year, которое состоит из одного значения типа Int.Конструктор обычно идёт первым, а за ним через пробел следуют другие типы. Конструктор может быть исамостоятельным значением, как в случае True или January.Типы делят выполнение программы на две стадии: компиляцию (compile-time) и вычисление (run-time). На

этапе компиляции происходит проверка типов. Программа, которая не прошла проверку типов, считаетсябессмысленной и не вычисляется. Приложение, которое выполняет компиляцию, называют компилятором(compiler), а то приложение, которое проводит вычисление, называют вычислителем (run-time system).Типами мы определяем основные понятия в том явлении, которое мы хотим описать, а также осмыслен-

ные способы их комбинирования. Мы говорим, как из простейших терминов получаются составные. Если мыпопытаемся построить бессмысленное предложение, компилятор языка автоматически найдёт такое предло-жение и сообщит нам об этом. Этот процесс заключается в проверке типов, к примеру если у нас есть функциясложения чисел, и мы попытаемся передать в неё строку или список, компилятор заметит это и скажет намоб этом перед тем как программа начнёт выполнятся. И важно то, что это произойдёт очень быстро. Если мыслучайно ошиблись в выражении, которое будет вычислено через час, нам не нужно ждать пока вычислительдойдёт до ошибки, мы узнаем об этом, не успев моргнуть, после запуска программы.Итак, если мы попробуем составить время из месяцев и логических значений:

Time January True 23

компилятор предупредит нас об ошибке. Наверное, вы думаете, что приведенный пример надуман, ведь комузахочется составлять время из логических значений? Но когда вы пишете программу, часто процесс работыскладывается так: вы думаете над одним, пишете другое, а также планируете вернуться к третьему. И знаниетого, что есть надежный компилятор, который не пропустит глупых ошибок, освобождает руки, вы можетене заботиться о таких пустяках, как правильное построение предложения.Отметим, что такой подход с разделением вычисления на две стадии и проверкой типов называется

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

Общая картина | 13

Page 14: Ru Haskell Book

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

собов их комбинирования. Не упускаем ли мы что-то важное из виду, или, может быть, типы имеют слишкомобщий характер и допускают ненужные нам предложения? Приходится задумываться. Но если типы подо-браны удачно, они сами начинают подсказывать, как строить программу.

ЗначенияИтак, мы определили типами базовые понятия и способы комбинирования. Обычно это небольшой набор

слов. Например в логических выражениях всего лишь два слова. Можем ли мы на что либо рассчитывать стаким словарным запасом? Оказывается, что да. Здесь на помощь приходят синонимы. Сейчас у нас в активелишь два слова:

data Bool = True | False

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

true :: Booltrue = True

false :: Boolfalse = False

В Haskell синонимы пишутся с маленькой буквы. Синоним определяется через знак =. Обратите вниманиена то, что это не процесс вычисления значения. Мы всего лишь объявляем новое имя для комбинации слов.Теперь мы имеем целых четыре слова! Тем не менее, ушли мы не далеко, и два новых слова, в сущности,

не делают язык выразительнее. Такие синонимы называют константами. Это значит, что одним словом мыбудем обозначать некоторую комбинацию других слов. В данном случае комбинации очень простые.Но наши синонимы могут определять одни слова через другие. Синонимы могут принимать параметры.

Параметры пишутся через пробел между новым именем и знаком равно:

not :: Bool -> Boolnot True = Falsenot False = True

Мы определили новое имя not с типом Bool -> Bool. Оно определяется двумя уравнениями (clause). Слеваот знака равно левая часть уравнения, а справа – правая. В первом уравнении мы говорим, что сочетание (notTrue) означает False, а сочетание (not False) означает True. Опять же, мы ничего не вычисляем, мы даёмновые имена нашим константам True и False. Только в этом случае имена составные.Если вычислителю нужно узнать, что кроется за составным именем not False он последовательно про-

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

not True == not False -- нет, пошли дальшеnot False == not False -- эврика, вернём правую часть=> True

Определим ещё два составных имени

and :: Bool -> Bool -> Booland False _ = Falseand True x = x

or :: Bool -> Bool -> Boolor True _ = Trueor False x = x

Эти синонимы определяют логические операции ”и” и ”или”. Здесь несколько новых конструкций, но выне пугайтесь, они не так трудны для понимания. Начнём с _:

and False _ = False

14 | Глава 1: Основы

Page 15: Ru Haskell Book

Здесь cимвол _ означает, что в этом уравнении, если первый параметр равен False, то второй нам уже неважен, мы знаем ответ. Так, если в логическом ”и” один из аргументов равен False, то всё выражение равноFalse. Так же и в случае с or.Теперь другая новая конструкция:

and True x = x

В этом случае параметр x служит для того, чтобы перетащить значение из аргумента в результат. Кон-кретное значение нам также не важно, но в этом случае мы полагаем, что слева и справа от =, x имеет однои то же значение.Итак у нас уже целых семь имён: True, False, true, false, not, and, or. Или не семь? На самом деле, их

уже бесконечное множество. Поскольку три из них составные, мы можем создавать самые разнообразныекомбинации:

not (and true False)or (and true true) (or False False)not (not true)not (or (or True True) (or False (not True)))...

Обратите внимание на использование скобок, они группируют значения. Так, если бы мы написали notnot true вместо not (not true), мы бы получили ошибку компиляции, потому что not ожидает один пара-метр, а в выражении not not true их два. Параметры дописываются к имени через пробел.Посмотрим, как происходят вычисления. В сущности, процесса вычислений нет, есть процесс замены

синонимов на основные понятия согласно уравнениям. Базовые понятия мы определили в типах. Так давайте”вычислим” выражение not (and true False):

-- выражение -- уравнение

not (and true False) -- true = Truenot (and True False) -- and True x = x => and True False = Falsenot False -- not False = TrueTrue

Слева в столбик написаны шаги ”вычисления”, а справа уравнения, по которым синонимы заменяютсяна комбинации слов. Процесс замены синонима (левой части уравнения) на комбинацию слов (правую частьуравнения) называется редукцией (reduction).Сначала мы заменили синоним true на правую часть его уравнения, т.е. на конструктор True. Затем мы

заменили выражение (and True False) на правую часть из уравнения для синонима and. Обратите вниманиена то, что переменная x была заменена на значение False. Последним шагом была замена синонима not. Вконце концов мы пришли к базовому понятию, а именно – к одному из двух конструкторов. В данном случаеTrue.Интересно, что новые синонимы могут быть использованы в правых частях уравнений. Так мы можем

определить операцию ”исключающее или”:

xor :: Bool -> Bool -> Boolxor a b = or (and (not a) b) (and a (not b))

Этим выражением мы говорим, что xor a b это или отрицание a и b, или a и отрицание b. Это и естьопределение ”исключающего или”.Может показаться, что с типом Bool мы зациклены на двух конструкторах, и единственное, что нам оста-

ётся – это давать всё новые и новые имена словам True и False. Но на самом деле это не так. С помощьютипов-параметров мы можем выйти за эти рамки. Определим функцию ветвления ifThenElse:

ifThenElse :: Bool -> a -> a -> aifThenElse True t _ = tifThenElse False _ e = e

Эта функция первым аргументом принимает значение типа Bool, а вторым и третьим – альтернативынекоторого типа a. Если первый аргумент – True, возвращается второй аргумент, а если – False, то третий.Интересно, что в Haskell ничего не происходит, мир Haskell-значений стоит на месте. Мы просто даём

имена разным комбинациям слов. Определяем новые термины. Потом на этих терминах определяем новыетермины, и так далее. Кажется, если ничего не меняется, то зачем язык? И что мы собираемся программиро-вать без вычислений?

Общая картина | 15

Page 16: Ru Haskell Book

Разгадка кроется в функциях not, and и or. До того как мы их определили, у нас было четыре имени, нопосле их определения имён стало бесконечное множество. Три синонима пополнили наш язык бесконечнымнабором комбинаций. В этом суть. Мы определяем базовые элементы и способы составления новых, потоммы просим ”вычислить” комбинацию из них. Мы не определяли явно, чему равна комбинация not (and trueFalse), это сделал за нас вычислитель Haskell 1.Вычислить стоит в кавычках, потому что на самом деле вычислений нет, есть замена синонимов на ком-

бинации простейших элементов.Ещё один пример, положим у нас есть тип:

data Status = Work | Rest

Он определяет, что делать в данный день: работать (Work) или отдыхать (Rest). У разных рабочих разныйграфик. Например, есть функции:

jonny :: Week -> Statusjonny x = ...

colin :: Week -> Statuscolin x = ...

Конкретное определение сейчас не важно, важно, что они определяют зависимость статуса (Status) отдня недели (Week) для работников Джонни (jonny) и Колина (colin).Также у нас есть полезная функция:

calendar :: Date -> Weekcalendar x = ...

Она определяет по дате день недели. И теперь, зная лишь эти функции, мы можем спросить у вычислителябудет ли у Джонни выходной 8 августа 3043 года:

jonny (calendar (Date (Year 3043) August (Day 8)))=> jonny Saturday=> Rest

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

Классы типовЕсли типы и значения – привычные понятия, которые можно найти в том или ином виде в любом языке

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

лены на аргументах разных типов; по сути, они описывают схожие понятия, но определены для значенийразных типов. Например, функция сравнения на равенство, говорящая о том, что два значения одного типаa равны, имеет тип a -> a -> Bool, или функция печати выражения имеет тип a -> String, но что такоеa в этих типах? Тип a является любым типом, для которого сравнение на равенство или печать (преобразо-вание в строку) имеют смысл. Это понятие как раз и кодируется в классах типов. Классы типов (type class)позволяют определять функции с одинаковым именем для разных типов.У классов типов есть имена. Также как и имена классов, они начинаются с большой буквы. Например,

класс сравнений на равенство называется Eq (от англ. equals – равняется), а класс печати выражений имеетимя Show (от англ. show – показывать). Посмотрим на их определения:Класс Eq:

class Eq a where(==) :: a -> a -> Bool(/=) :: a -> a -> Bool

Класс Show:

class Show a whereshow :: a -> String

1Было бы точнее называть вычислитель редуктором, поскольку мы проводим редукции, т.е. замену эквивалентных значений, нозакрепилось это название. К тому же, редуктор также обозначает прибор.

16 | Глава 1: Основы

Page 17: Ru Haskell Book

За ключевым словом class следует имя класса, тип-параметр и ещё одно ключевое слово where. Далее сотступами пишутся имена определённых в классе значений. Значения класса называются методами.Мы определяем лишь типы методов, конкретная реализация будет зависеть от типа a. Методы определя-

ются в экземплярах классов типов, мы скоро к ним перейдём.Программистская аналогия класса типов это интерфейс. В интерфейсе определён набор значений (как

констант, так и функций), которые могут быть применены ко всем типам, которые поддерживают данныйинтерфейс. К примеру, в интерфейсе ”сравнение на равенство” для некоторого типа a определены две функ-ции: равно (==) и не равно (/=) с одинаковым типом a -> a -> Bool, или в интерфейсе ”печати” для любоготипа a определена одна функция show типа a -> String.Математическая аналогия класса типов это алгебраическая система. Алгебра изучает свойства объекта в

терминах операций, определённых на нём, и взаимных ограничениях этих операций. Алгебраическая систе-ма представляет собой набор операций и свойств этих операций. Этот подход позволяет абстрагироватьсяот конкретного представления объектов. Например группа – это все объекты данного типа a, для которыхопределены значения: константа – единица типа a, бинарная операция типа a -> a -> a и операция взятияобратного элемента, типа a -> a. При этом на операции накладываются ограничения, называемые свойства-ми операций. Например, ассоциативность бинарной операции, или тот факт, что единица с любым другимэлементом, применённые к бинарной операции, дают на выходе исходный элемент.Давайте определим класс для группы:

class Group a wheree :: a(+) :: a -> a -> sinv :: a -> a

Класс с именем Group имеет для некоторого типа a три метода: константу e :: a, операцию (+) :: a ->a -> a и операцию взятия обратного элемента inv :: a -> a.Как и в алгебре, в Haskell классы типов позволяют описывать сущности в терминах, определённых на них

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

претироваться специфическим для типа образом:twice :: Group a => a -> atwice a = a + a

isE :: (Group a, Eq a) => a -> BoolisE x = (x == e)

Обратите внимание на запись Group a => и (Group a, Eq a) =>. Это называется контекстом функции. Вконтексте мы говорим, что данный тип должен быть из типа Group или из типов Group и Eq. Это значит, чтодля этого типа мы можем пользоваться методами из этих классов.В первой функции twice мы воспользовались методом (+) из класса Group, поэтому функция имеет кон-

текст Group a =>. А во второй функции isE мы воспользовались методом e из класса Group и методом (==)из класса Eq, поэтому функция имеет контекст (Group a, Eq a) =>.

Контекст классов типов. СуперклассыКласс типов также может содержать контекст. Он указывается между словом class и именем класса.

Напримерclass IsPerson a

class IsPerson a => HasName a wherename :: a -> String

Это определение говорит о том, что мы можем сделать экземпляр класса HasName только для тех типов,которые содержатся в IsPerson. Мы говорим, что класс HasName содержится в IsPerson. В этом случае классиз контекста IsPerson называют суперклассом для данного класса HasName.Это сказывается на контексте функции. Теперь, если мы пишем

fun :: HasName a => a -> a

Это означает, что мы можем пользоваться для значений типа a как методами из класса HasName, так иметодами из класса IsPerson. Поскольку если тип принадлежит классу HasName, то он также принадлежит иIsPerson.Запись (IsPerson a => HasName a) немного обманывает, было бы точнее писать IsPerson a <= HasName

a, если тип a в классе HasName, то он точно в классе IsPerson, но в Haskell закрепилась другая запись.

Общая картина | 17

Page 18: Ru Haskell Book

Экземпляры классов типовВ экземплярах (instance) классов типов мы даём конкретное наполнение для методов класса типов. Опре-

деление экземпляра пишется так же, как и определение класса типа, но вместо class мы пишем instance,вместо некоторого типа наш конкретный тип, а вместо типов методов – уравнения для них.Определим экземпляры для BoolКласс Eq:

instance Eq Bool where(==) True True = True(==) False False = True(==) _ _ = False

(/=) a b = not (a == b)

Класс Show:

instance Show Bool whereshow True = ”True”show False = ”False”

Класс Group:

instance Group Bool wheree = True(+) a b = and a binv a = not a

Отметим важность наличия свойств (ограничений) у значений, определённых в классе типов. Так, на-пример, в классе типов ”сравнение на равенство” для любых двух значений данного типа одна из операцийдолжна вернуть ”истину”, а другая ”ложь”, т.е. два элемента данного типа либо равны, либо не равны. Недо-статочно определить равенство для конкретного типа, необходимо убедиться в том, что для всех элементовданного типа свойства понятия равенства не нарушаются.На самом деле приведённое выше определение экземпляра для Group не верно, хотя по типам оно под-

ходит. Оно не верно как раз из-за нарушения свойств. Для группы необходимо, чтобы для любого a выпол-нялось:

inv a + a == e

У нас лишь два значения, и это свойство не выполняется ни для одного из них. Проверим:

inv True + True=> (not True) + True=> False + True=> and False True=> False

inv False + False=> (not False) + False=> True + False=> and True False=> False

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

1.2 Ядро HaskellФуууухх. Мы закончили наш пробег. Теперь можно остановиться, отдышаться и подвести итоги. Давайте

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

18 | Глава 1: Основы

Page 19: Ru Haskell Book

Модулиmodule New(edef1, edef2, ..., edefN) where

import Old1(idef11, idef12, ..., idef1N)import Old2(idef21, idef22, ..., idef2M)...import OldK(idefK1, idefK2, ..., idefKP)

-- определения :...

Ключевые слова: module, where, import. Мы определили модуль с именем New, который экспортируетопределения edef1, edef2, …, edefN. И импортирует определения из модулей Old1, Old2, и т.д., определениянаписаны в скобках за ключевыми словами import и именами модулей.

Типы

Тип определяется с помощью:

• Перечисления альтернатив через |

data Type = Alt1 | Alt2 | ... | AltN

Эту операцию называют суммой типов.

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

data Type = Name Sub1 Sub2 ... SubN

Эту операцию называют произведением типов.Есть одно исключение: если тип состоит из двух подтипов, мы можем дать конструктору символьное(а не буквенное) имя, но оно должно начинаться с двоеточия :, как в случае списка, например, можноделать такие определения типов:

data Type = Sub1 :+ Sub2data Type = Sub1 :| Sub2

• Комбинации суммы и произведения типов:

data Type = Name1 Sub11 Sub12 ... Sub1N| Name2 Sub21 Sub22 ... Sub2M...| NameK SubK1 SubK2 ... SubKP

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

Значения

Как это ни странно, нам встретилась лишь одна операция создания значений: определение синонима. Онапишется так

name x1 x2 ... xN = Expr1name x1 x2 ... xN = Expr2name x1 x2 ... xN = Expr3

Слева от знака равно стоит составное имя, а справа от знака равно некоторое выражение, построенноесогласно типам. Разные комбинации имени name с параметрами определяют разные уравнения для синонимаname.Также мы видели символ _, который означает ”всё, что угодно” на месте аргумента. А также мы увидели,

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

Ядро Haskell | 19

Page 20: Ru Haskell Book

Классы типовНам встретилась одна конструкция определения классов типов:

class Name a wheremethod1 :: a -> ...method2 :: a -> ......methodN :: a -> ...

Экземпляры классов типовНам встретилась одна конструкция определения экземпляров классов типов:

instance Name Type wheremethod1 x1 ... xN = ...method2 x1 ... xM = ......methodN x1 ... xP = ...

Типы, значения и классы типовКаждое значение имеет тип. Значение v имеет тип T на Haskell:

v :: T

Функциональный тип обозначается стрелкой: a -> b

fun :: a -> b

Тип значения может иметь контекст, он говорит о том, что параметр должен принадлежать классу типов:fun1 :: С a => a -> afun2 :: (C1 a, C2, ..., CN) => a -> a

СуперклассыТакже контекст может быть и у классов, запись

class A a => B a where...

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

1.3 Двумерный синтаксисНаверное вы обратили внимание на то, что в Haskell нет разделителей строк и дополнительных скобок,

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

class Eq a where(==) :: a -> a -> a(/=) :: a -> a -> a

По отступам за первой строкой определения компилятор понимает, что класс содержит два метода. Еслибы мы написали:class Eq a where

(==) :: a -> a -> a(/=) :: a -> a -> a

То смысл был бы совсем другим. Теперь мы определяем класс Eq с одним методом == и указываем типнекоторого значения (/=). Основное правило такое: конструкции, расположенные на одном уровне, вырав-ниваются с помощью табуляции. Чем правее находится определение, тем глубже оно вложено в какую-нибудьспециальную конструкцию. Пока нам встретилось лишь несколько специальных конструкций, но дальше по-явятся и другие.

20 | Глава 1: Основы

Page 21: Ru Haskell Book

1.4 Краткое содержаниеИтак подведём итоги: у нас есть две операции для определения типов (сумма и произведение) и по одной

для значений (синонимы), классов типов и экземпляров. А также бесконечное множество их комбинаций, изкоторых и состоит увлекательный мир Haskell. Конечно не только из них, есть нюансы, синтаксический сахар,расширения языка. Об этом и многом другом мы узнаем из этой книги.Интересно, что в Haskell, несмотря на обилие конструкций и библиотек, ты чувствуешь, что за ними стоит

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

1.5 УпражненияПотренируйтесь в описаниях в рамках системы типов. Вы определяете базовые понятия и способы их

комбинирования. У вас есть три операции:

• Сумма типов data T = A1 | A2. Перечисление альтернатив• Произведение типов data T = S S1 S2. Этим мы говорим, что понятие состоит из нескольких.• Взятие в список [T]. Обозначает множественное число, элементов типа T их может быть несколько.

Опишите что-либо: комнату, дорогу, город, человека, главу из книги, математическую теорию, всё чтоугодно.Ниже приведён пример для понятий из этой главы:

data Program = Programm ProgramType [Module]data ProgramType = Executable | Library

data Module = Module [Definition]

data Definition = Definition DefinitionType Elementdata DefinitionType = Export | Inner

data Element = ET Type | EV Value | EC Class | EI Instance

data Type = Type Stringdata Value = Value Stringdata Class = Class Stringdata Instance = Instance String

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

-- Все оьъявления типов в модулеgetTypes :: Module -> [Type]

-- Провести редукцию значения:reduce :: Value -> Program -> Value

-- Проверить типы:checkTypes :: Program -> Bool

-- Заменить все определения в модуле на новыеsetDefinitions :: Module -> [Definition] -> Module

-- Упорядочить определения по какому-лбо принципуorderDefinitions :: [Definition] -> [Definition]

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

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

Page 22: Ru Haskell Book

Глава 2

Первая программа

Я вот говорю-говорю, а вдруг я вас обманываю, и ничего этого нет. В этой главе мы перейдём к програм-мированию и запустим нашу первую программу в Haskell. Будет много примеров, на которых мы закрепимнаши знания.

2.1 ИнтерпретаторДля запуска кода мы будем пользоваться приложением GHC (Glorious Glasgow Haskell Compiler) наиболее

развитой системой интерпретации Haskell программ. В GHC есть компилятор ghc и интерпретатор ghci. Покамы будем пользоваться лишь интерпретатором. Если вы не знаете как установить ghc загляните в приложе-ние. Также нам понадобится текстовый редактор с подсветкой синтаксиса. Подсветка синтаксиса для Haskellпо умолчанию есть в редакторах Vim, Emacs, gedit, yi. Есть IDE для Haskell Leksah. Мы будем писать модулив файлах и загружать их в интерпретатор. Если вы не знаете продвинутых текстовых редакторов вроде Vimили Emacs, лучше всего будет начать с gedit.Интерпретатор позволяет загружать модуль с определениями и набирать значения в командной строке.

Мы набираем значение, а интерпретатор редуцирует его и показывает нам ответ. Интерпретатор запускаетсякомандой ghci в терминале. Определения из модуля могут быть загружены в интерпретатор двумя способа-ми, либо при запуске интерпретатора командой ghci ИмяМодуля.hs либо в самом интерпретаторе командой:l ИмяМодуля.hs.Рассмотрим некоторые полезные команды интерпретатора:

:?Выводит на экран список доступных команд

:t ExpressionВозвращает тип выражения.

:set +tПосле выполнения команды интерпретатор будет выводить на экран не только результат вычислениявыражения, но и его тип.

:set +sПосле выполнения команды интерпретатор будет выводить на экран не только результат вычислениявыражения, но и статистику вычислений.

:l ИмяМодуляЗагружает модуль в интерпретатор.

:cd ДиректорияПерейти в данную директорию.

:rПерезагружает, последний загруженный модуль. Этой командой можно пользоваться после внесения вмодуль изменений.

:qВыход из интерпретатора.

2.2 У-вейСогласно даосам основной принцип жизни заключается в недеянии (у-вей). Всё происходит естественно и

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

22 | Глава 2: Первая программа

Page 23: Ru Haskell Book

module Empty where

import Prelude()

Зачем мы написали import Prelude()? Этой фразой мы говорим, что не хотим ничего импортироватьиз модуля Prelude. По умолчанию в любой модуль загружается модуль Prelude, который содержит многополезных определений. К примеру там определяется тип Bool, списки и функции для них, символы, классытипов для сравнения на равенство и печати значений и много, много других определений. В первых главахя хочу сделать акцент на самом языке Haskell, а не на производных выражениях, поэтому пока мы будем вявном виде загружать из модуля Prelude лишь самые необходимые определения.Сохраним модуль в файле Empty.hs, сделаем директорию модуля текущей и запустим интерпретатор

командой ghci Empty (имя расширения можно не писать). Также можно просто запустить интерпретаторкомандой ghci, переключиться на директорию командой :cd и загрузить модуль командой :l Empty.

$ ghciGHCi, version 7.4.1: http://www.haskell.org/ghc/ :? for helpLoading package ghc-prim ... linking ... done.Loading package integer-gmp ... linking ... done.Loading package base ... linking ... done.Prelude> :cd ~/haskell-notes/code/ch-2/Prelude> :l Empty.hs[1 of 1] Compiling Empty ( Empty.hs, interpreted )Ok, modules loaded: Empty.*Empty>

Слева от знака приглашения к вводу > отображаются загруженные в интерпретатор модули. По умол-чанию загружается модуль Prelude. После выполнения команды :l мы видим, что Prelude сменилось наEmpty.Теперь давайте потренируемся перезагружать модули. Давайте изменим наш модуль, сделаем его не та-

ким пустым, убрав последние две скобки от модуля Prelude в директиве import. Теперь сохраним измененияи выполним команду :r.

*Empty> :r[1 of 1] Compiling Empty ( Empty.hs, interpreted )Ok, modules loaded: Empty.*Empty>

Завершим сессию интерпретатора командой :q.

*Empty> :qLeaving GHCi.

Внешние модули должны находится в текущей директории. Давайте потренируемся с подключениемопределений из внешних модулей. Создадим модуль близнец модуля Empty.hs:

module EmptyEmpty where

import Prelude()

И сохраним его в той же директории, что и модуль Empty, теперь мы можем включить все определенияиз модуля EmptyEmpty:

module Empty where

import EmptyEmpty

Когда у нас будет много модулей мы можем разместить их по директориям. Создадим в одной дирек-тории с модулем Empty директорию Sub, а в неё поместим копию модуля Empty. Существует одна тонкость:поскольку модуль находится в поддиректории, для того чтобы он стал виден из текущей директории, необ-ходимо дописать через точку имя директории в которой он находится:

module Sub.Empty where

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

У-вей | 23

Page 24: Ru Haskell Book

module Empty where

import EmptyEmptyimport Sub.Empty

Обратите внимание на то, что мы приписываем к модулю в поддиректории Sub имя поддиректории. Еслибы он был заложен в ещё одной директории, то мы написали бы через точку имя и этой поддиректории:

module Empty where

import Sub1.Sub2.Sub3.Sub4.Empty

2.3 Логические значенияПустой модуль это хорошо, но слишком скучно. Давайте перепишем объявленные в этой главе опреде-

ления в модуль, загрузим его в интерпретатор и понабираем значения.Начнём с логических операций. Давайте не будем переопределять Bool, Show и Eq, а просто возьмём их

из Prelude:

module Logic where

import Prelude(Bool(..), Show(..), Eq(..))

Две точки в скобках означают ”все конструкторы” (в случае типа) и ”все методы” (в случае класса типа).Строчку

import Prelude(Bool(..), Show(..), Eq(..))

Следует читать так: Импортируй из модуля Prelude тип Bool и все его конструкторы и классы Show иEq со всеми их методами. Если бы мы захотели импортировать только конструктор True, мы бы написалиBool(True), а если бы мы захотели импортировать лишь имя типа, мы бы написали просто Bool без скобок.Сначала выпишем в модуль наши синонимы:

module Logic where

import Prelude(Bool(..), Show(..), Eq(..))

true :: Booltrue = True

false :: Boolfalse = False

not :: Bool -> Boolnot True = Falsenot False = True

and :: Bool -> Bool -> Booland False _ = Falseand True x = x

or :: Bool -> Bool -> Boolor True _ = Trueor False x = x

xor :: Bool -> Bool -> Boolxor a b = or (and (not a) b) (and a (not b))

ifThenElse :: Bool -> a -> a -> aifThenElse True t _ = tifThenElse False _ e = e

Теперь сохраним модуль и загрузим его в интерпретатор. Для наглядности мы установим флаг +t, приэтом будет возвращено не только значение, но и его тип. Понабираем разные комбинации значений:

24 | Глава 2: Первая программа

Page 25: Ru Haskell Book

*Logic> :l Logic[1 of 1] Compiling Logic ( Logic.hs, interpreted )Ok, modules loaded: Logic.*Logic> :set +t*Logic> not (and true False)Trueit :: Bool*Logic> or (and true true) (or False False)Trueit :: Bool*Logic> xor (not True) (False)Falseit :: Bool*Logic> ifThenElse (or true false) True FalseTrueit :: Bool

Разумеется в Haskell уже определены логические операции, здесь мы просто тренировались. Они называ-ются not, (&&), ||. Операция xor это то же самое, что и (/=). Для Bool определён экземпляр класса Eq. Такжев Haskell есть конструкция ветвления она пишется так:x = if cond then t else e

Слова if, then и else – ключевые. cond имеет тип Bool, а t и e одинаковый тип.В коде программы обычно пишут так:

x = if a > 3then ”Hello”else (if a < 0

then ”Hello”else ”Bye”)

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

ми:*Logic> :m PreludePrelude> not (True && False)Trueit :: BoolPrelude> (True && True) || (False || False)Trueit :: BoolPrelude> not True /= FalseFalseit :: BoolPrelude> if (True || False) then True else FalseTrueit :: Bool

Бинарные операции с символьными именами пишутся в инфиксной форме, т.е. между аргументами какв a && b или a + b. Значение с буквенным именем также можно писать в инфиксной форме, для этого онозаключается в апострофы, например a ‘and‘ b или a ‘plus‘ b. Апострофы обычно находятся на однойкнопке с буквой ”ё”. Также символьные функции можно применять в префиксной форме, заключив их вскобки, например (&&) a b и (+) a b. Попробуем в интерпретаторе:Prelude> True && FalseFalseit :: IntegerPrelude> (&&) True FalseFalseit :: BoolPrelude> let and a b = a && band :: Bool -> Bool -> BoolPrelude> and True FalseFalseit :: BoolPrelude> True ‘and‘ FalseFalseit :: Bool

Логические значения | 25

Page 26: Ru Haskell Book

Обратите внимание на строчку let and a b = a && b. В ней мы определили синоним в интерпретаторе.Сначала мы пишем ключевое слово let затем обычное определение синонима, как в программе. Но не совсемобычное синоним должен быть однострочным. У нас не получится например определить такой синоним какnot. Потому что в нём два уравнения.

2.4 Класс Show. Строки и символыМы набираем в интерпретаторе какое-нибудь сложное выражение, или составной синоним, интерпрета-

тор проводит редукцию и выводит ответ на экран. Откуда интерпретатор знает как отображать значения типаBool? Внутри интерпретатора вызывается метод класса Show, который переводит значение в строку. И затеммы видим на экране ответ. Для типа Bool экземпляр класса Show уже определён, поэтому интерпретаторзнает как его отображать.Обратите внимание на эту особенность языка, вид значения определяется пользователем, в экземпляре

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

ках и символах в языке Haskell.

Строки и символыПосмотрим в интерпретаторе что из себя представляют строки (тип String), для этого мы воспользуемся

командой :i (сокращение от :info):

Prelude> :i Stringtype String = [Char] -- Defined in ‘GHC.Base’

Интерпретатор показал определение типа и в комментариях указал в каком модуле тип определён. Вэтом определении мы видим новое ключевое слово type. До этого для определения типов нам встречалосьлишь слово data. Ключевое слово type определяет синоним типа. При этом мы не вводим новый тип, мылишь определяем для него псевдоним. String является синонимом для списка значений типа Char. ТипChar представляет символы. Итак строка – это список символов. В Haskell символы пишутся в ординарныхкавычках, а строки в двойных:

Prelude> [’H’,’e’,’l’,’l’,’o’]”Hello”it :: [Char]Prelude> ”Hello””Hello”it :: [Char]Prelude> ’+’’+’it :: Char

Для обозначения переноса используется специальный символ ’\n’. Если строка слишком длинная и непомещается на одной строке, то её можно перенести так:

str = ”My long long long long \\long long string”

Перенос осуществляется с помощью комбинации следующих друг за другом обратных слэшей.Нам понадобится функция конкатенации списков (++), она определена в Prelude, с её помощью мы будем

объединять строки:

Prelude> :t (++)(++) :: [a] -> [a] -> [a]Prelude> ”Hello” ++ [’ ’] ++ ”World””Hello World”it :: [Char]

26 | Глава 2: Первая программа

Page 27: Ru Haskell Book

Пример: Отображение дат и времениПриведём, пример в котором отображаемое значение не совпадает с видом значения в коде. Мы отобра-

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

module Calendar where

import Prelude (Int, Char, String, Show(..), (++))

-- Датаdata Date = Date Year Month Day

-- Годdata Year = Year Int -- Int это целые числа

-- Месяцdata Month = January | February | March | April

| May | June | July | August| September | October | November | December

data Day = Day Int

-- Неделяdata Week = Monday | Tuesday | Wednesday

| Thursday | Friday | Saturday| Sunday

-- Времяdata Time = Time Hour Minute Second

data Hour = Hour Int -- Часdata Minute = Minute Int -- Минутаdata Second = Second Int -- Секунда

Теперь сохраним наш модуль под именем Calendar.hs и загрузим в интерпретатор:

Prelude> :l Calendar[1 of 1] Compiling Calendar ( Calendar.hs, interpreted )Ok, modules loaded: Calendar.*Calendar> Monday

<interactive>:3:1:No instance for (Show Week)

arising from a use of ‘System.IO.print’Possible fix: add an instance declaration for (Show Week)In a stmt of an interactive GHCi command: System.IO.print it

Смотрите мы попытались распечатать значение Monday, но в ответ получили ошибку. В ней интерпре-татор сообщает нам о том, что для типа Week не определён экземпляр класса Show и он не знает как егораспечатывать. Давайте подскажем ему. Обычно дни недели в календарях печатают не полностью, в имяпопадают лишь три первых буквы:

instance Show Week whereshow Monday = ”Mon”show Tuesday = ”Tue”show Wednesday = ”Wed”show Thursday = ”Thu”show Friday = ”Fri”show Saturday = ”Sat”show Sunday = ”Sun”

Отступы перед show обязательны, но выравнивание по знаку равно не обязательно, мне просто нравитсятак писать. По отступам компилятор понимает, что все определения относятся к определению instance.Теперь запишем экземпляр в модуль, сохраним, и перезагрузим в интерпретатор:

*Calendar> :r[1 of 1] Compiling Calendar ( Calendar.hs, interpreted )

Класс Show. Строки и символы | 27

Page 28: Ru Haskell Book

Ok, modules loaded: Calendar.*Calendar> MondayMonit :: Week*Calendar> SundaySunit :: Week

Теперь наши дни отображаются. Я выпишу ещё один пример экземпляра для Time, а остальные достанутсявам в качестве упражнения.

instance Show Time whereshow (Time h m s) = show h ++ ”:” ++ show m ++ ”:” ++ show s

instance Show Hour whereshow (Hour h) = addZero (show h)

instance Show Minute whereshow (Minute m) = addZero (show m)

instance Show Second whereshow (Second s) = addZero (show s)

addZero :: String -> StringaddZero (a:[]) = ’0’ : a : []addZero as = as

Функцией addZero мы добавляем ноль в начало строки, в том случае если число однозначное, также вэтом определении мы воспользовались тем, что для типа целых чисел Int экземпляр Show уже определён.Проверим в интерпретаторе:

*Calendar> Time (Hour 13) (Minute 25) (Second 2)13:25:02it :: Time

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

Это делается с помощью директивы deriving. Она пишется сразу после объявления типа. Например так мыможем определить тип и экземпляры для классов Show и Eq:

data T = A | B | Cderiving (Show, Eq)

Отступ за deriving обязателен, после ключевого слова в скобках указываются классы, которые мы хотимвывести.

2.6 АрифметикаВ этом разделе мы обсудим основные арифметические операции. В Haskell много стандартных классов,

которые группируют различные типы операций, есть класс для сравнения на равенство, отдельный класс длясравнения на больше/меньше, класс для умножения, класс для деления, класс для упорядоченных чисел, имного других. Зачем такое изобилие классов?Каждый из классов отвечает независимой группе операций. Есть много объектов, которые можно только

складывать, но нельзя умножать или делить. Есть объекты, для которых сравнение на равенство имеет смысл,а сравнение на больше/меньше – нет.Для иллюстрации мы воспользуемся числами Пеано, у них компактное определение, всего два конструк-

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

module Nat where

data Nat = Zero | Succ Natderiving (Show, Eq, Ord)

28 | Глава 2: Первая программа

Page 29: Ru Haskell Book

Конструктор Zero указывает на число ноль, а (Succ n) на число следующее за данным числом n. Впоследней строчке мы видим новый класс Ord, этот класс содержит операции сравнения на больше/меньше:

Prelude> :i Ordclass (Eq a) => Ord a wherecompare :: a -> a -> Ordering(<) :: a -> a -> Bool(>=) :: a -> a -> Bool(>) :: a -> a -> Bool(<=) :: a -> a -> Boolmax :: a -> a -> amin :: a -> a -> a

Тип Ordering кодирует результаты сравнения:

Prelude> :i Orderingdata Ordering = LT | EQ | GT -- Defined in GHC.Ordering

Он содержит конструкторы, соответствующие таким понятиям как меньше, равно и больше.

Класс Eq. Сравнение на равенствоВспомним определение класса Eq:

class Eq a where(==) :: a -> a -> Bool(/=) :: a -> a -> Bool

a == b = not (a /= b)a /= b = not (a == b)

Появились две детали, о которых я умолчал в предыдущей главе. Это две последние строчки. В нихмы видим определение == через /= и наоборот. Это определения методов по умолчанию. Такие определениядают нам возможность определять не все методы класса, а лишь часть основных, а все остальные мы получимавтоматически из определений по умолчанию.Казалось бы почему не оставить в классе Eq один метод а другой метод определить в виде отдельной

функции:

class Eq a where(==) :: a -> a -> Bool

(/=) :: Eq a => a -> a -> Boola /= b = not (a == b)

Так не делают по соображениям эффективности. Есть типы для которых проще вычислить /= чем ==.Тогда мы определим тот метод, который нам проще вычислять и второй получим автоматически.Набор основных методов, через которые определены все остальные называют минимальным полным опре-

делением (minimal complete definition) класса. В случае класса Eq это метод == или метод /=.Мы уже вывели экземпляр для Eq, поэтому мы можем пользоваться методами == и /= для значений типа

Num:

*Calendar> :l Nat[1 of 1] Compiling Nat ( Nat.hs, interpreted )Ok, modules loaded: Nat.*Nat> Zero == Succ (Succ Zero)Falseit :: Bool*Nat> Zero /= Succ (Succ Zero)Trueit :: Bool

Арифметика | 29

Page 30: Ru Haskell Book

Класс Num. Сложение и умножениеСложение и умножение определены в классе Num. Посмотрим на его определение:

*Nat> :i Numclass (Eq a, Show a) => Num a where(+) :: a -> a -> a(*) :: a -> a -> a(-) :: a -> a -> anegate :: a -> aabs :: a -> asignum :: a -> afromInteger :: Integer -> a

-- Defined in GHC.Num

Методы (+), (*), (-) в представлении не нуждаются, метод negate является унарным минусом, его можноопределить через (-) так:negate x = 0 - x

Метод abs является модулем числа, а метод signum возвращает знак числа, метод fromInteger позволяетсоздавать значения данного типа из стандартных целых чисел Integer.Этот класс устарел, было бы лучше сделать отельный класс для сложения и вычитания и отдельный

класс для умножения. Также контекст класса, часто становится помехой. Есть объекты, которые нет смыслапечатать но, есть смысл определить на них сложение и умножение. Но пока в целях совместимости с уженаписанным кодом, класс Num остаётся прежним.Определим экземпляр для чисел Пеано, но давайте сначала разберём функции по частям.

СложениеНачнём со сложения:

instance Num Nat where(+) a Zero = a(+) a (Succ b) = Succ (a + b)

Первое уравнение говорит о том, что если второй аргумент равен нулю, то мы вернём первый аргументв качестве результата. Во втором уравнении мы ”перекидываем” конструктор Succ из второго аргумента запределы суммы. Схематически вычисление суммы можно представить так:

3 + 2 → 1 + (3 + 1) → 1 + (1 + (3 + 0)) →1 + (1 + 3) → 1 + (1 + (1 + (1 + (1 + 0)))) → 5

Все наши числа имеют вид 0 или 1+n, мы принимаем на вход два числа в таком виде и хотим в результатесоставить число в этом же виде, для этого мы последовательно перекидываем (1+) в начало выражения извторого аргумента.

ВычитаниеОперация отрицания не имеет смысла, поэтому мы воспользуемся специальной функцией error ::

String -> a, она принимает строку с сообщением об ошибке, при её вычислении программа остановит-ся с ошибкой и сообщение будет выведено на экран.

negate _ = error ”negate is undefined for Nat”

УмножениеТеперь посмотрим на умножение:(*) a Zero = Zero(*) a (Succ b) = a + (a * b)

В первом уравнении мы вернём ноль, если второй аргумент окажется нулём, а во втором мы за каждыйконструктор Succ во втором аргументе прибавляем к результату первый аргумент. В итоге, после вычисле-ния a * b мы получим аргумент a сложенный b раз. Это и есть умножение. При этом мы воспользовалисьоперацией сложения, которую только что определили. Посмотрим на схему вычисления:

3 ∗ 2 → 3+(3 ∗ 1) → 3 + (3+(3 ∗ 0)) →3 + (3 + 0) → 3 + 3 →1 + (3 + 2) → 1 + (1 + (3 + 1)) → 1 + (1 + (1 + (3 + 0))) →1 + (1 + 1 + 3) → 1 + (1 + (1 + (1 + (1 + (1 + 0))))) → 6

30 | Глава 2: Первая программа

Page 31: Ru Haskell Book

Операции abs и signumПоскольку числа у нас положительные, то методы abs и signum почти ничего не делают:abs x = xsignum Zero = Zerosignum _ = Succ Zero

Перегрузка чиселОстался последний метод fromInteger. Он конструирует значение нашего типа из стандартного:fromInteger 0 = ZerofromInteger n = Succ (fromInteger (n-1))

Зачем он нужен? Попробуйте узнать тип числа 1 в интерпретаторе:*Nat> :t 11 :: (Num t) => t

Интерпретатор говорит о том, тип значения 1 является некоторым типом из класса Num. В Haskell обозна-чения для чисел перегружены. Когда мы пишем 1 на самом деле мы пишем (fromInteger (1::Integer)).Поэтому теперь мы можем не писать цепочку Succ-ов, а воспользоваться методом fromInteger, для этогосохраним определение экземпляра для Num и загрузим обновлённый модуль в интерпретатор:[1 of 1] Compiling Nat ( Nat.hs, interpreted )Ok, modules loaded: Nat.*Nat> 7 :: NatSucc (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))*Nat> (2 + 2) :: NatSucc (Succ (Succ (Succ Zero)))*Nat> 2 * 3 :: NatSucc (Succ (Succ (Succ (Succ (Succ Zero)))))

Вы можете убедиться насколько гибкими являются числа в Haskell:*Nat> (1 + 1) :: NatSucc (Succ Zero)*Nat> (1 + 1) :: Double2.0*Nat> 1 + 12

Мы выписали три одинаковых выражения и получили три разных результата, меняя объявление типов. Впоследнем выражении тип был приведён к Integer. Это поведение интерпретатора по умолчанию. Если мынапишем:*Nat> let q = 1 + 1*Nat> :t qq :: Integer

Мы видим, что значение q было переведено в Integer, это происходит лишь в интерпретаторе, если такаяпеременная встретится в программе и компилятор не сможет определить её тип из контекста, произойдётошибка проверки типов, компилятор скажет, что он не смог определить тип. Помочь компилятору можно,добавив объявление типа с помощью конструкции (v :: T).Посмотрим ещё раз на определение экземпляра Num для Nat целиком:

instance Num Nat where(+) a Zero = a(+) a (Succ b) = Succ (a + b)

(*) a Zero = Zero(*) a (Succ b) = a + (a * b)

fromInteger 0 = ZerofromInteger n = Succ (fromInteger (n-1))

abs x = xsignum Zero = Zerosignum _ = Succ Zero

negate _ = error ”negate is undefined for Nat”

Арифметика | 31

Page 32: Ru Haskell Book

Стандартные числаВ этом подразделе мы рассмотрим несколько стандартных типов для чисел в Haskell. Все эти числа яв-

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

Целые числа

В Haskell предусмотрено два типа для целых чисел. Это Integer и Int. Чем они отличаются? Значениятипа Integer не ограничены, мы можем проводить вычисления с очень-очень-очень большими числами, еслипамяти на нашем компьютере хватит. Числа из типа Int ограничены. Каждое число занимает определённыйразмер в памяти компьютера. Диапазон значений для Int составляет от −229 до 229 − 1. Вычисления с Intболее эффективны.

Действительные числа

Действительные числа бывают дробными (тип Rational), с ординарной точностью Float и с двойнойточностью Double. Числа из типа Float занимают меньше места и операции с ними проходят быстрее, ноони не такие точные как Double.

2.7 ДокументацияК этой главе мы уже рассмотрели основные конструкции языка и базовые типы. Если у вас есть какая-то

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

нибудь уже написал. Самые полезные функции и классы определены в модуле Prelude и основных стан-дартных библиотечных модулях. Было бы излишним описывать каждую функцию, книга превратилась быв справочник. Вместо этого давайте научимся искать функции в документации. Нам понадобится умениесоставлять типы функций и небольшое знание английского языка.Для начала о том, где находится документация к стандартным модулям. Если вы установили ghc вме-

сте с Haskell Platform под Windows скорее всего во вкладке Пуск, там где иконка ghc там же находитсяи документация. В Linux необходимо найти директорию с документацией, скорее всего она в директории/usr/local/share/doc/ghc/libraries. Также документацию можно найти в интернете, наберите в поиско-вике Haskell Hierarchical Libraries. На главной странице документации вы найдёте огромное количество мо-дулей. Нас пока интересуют разделы Data и Prelude. Разделы расположены по алфавиту. То что вы видитеэто стандартный вид документации в Haskell. Документация делается с помощью специального приложе-ния Haddock, мы тоже научимся такие делать, но позже, пока мы попробуем разобраться с тем как искать вдокументации функции.Предположим нам нужно вычислить длину списка. Нам нужна функция, которая принимает список и

возвращает целое число, скорее всего её тип [a] -> Int, обычно во всех библиотечных функциях для це-лых чисел используется тип Int, также на месте параметра используются буквы a, b, c. Мы можем открытьдокументацию к Prelude набрать в строке поиска тип [a] -> Int. Или поискать такую функцию в разде-ле функций для списков List Operations. Тогда мы увидим единственную функцию с таким типом, подговорящим именем length. Так мы нашли то, что искали.Или мы ищем функцию, которая переворачивает список, нам нужна функция с типом [a] -> [a]. Таких

функций в Prelude несколько, но имя reverse одной из них может намекнуть на её смысл.Но одной Prelude мир стандартных функций Haskell не ограничивается, если вы не нашли необходимую

вам функцию в Prelude её стоит поискать в других библиотечных модулях. Обычно функции разделяютсяпо тому на каких типах они определены. Так например функция sort :: Ord a => [a] -> [a] определенане в Prelude, а в отдельном библиотечном модуле для списков он называется Data.List. Так же есть многодругих модулей для разных типов, таких как Data.Bool, Data.Char, Data.Function, Data.Maybe и многиедругие. Не пугайтесь изобилия модулей постепенно они станут вашей опорой.

2.8 Краткое содержаниеВ этой главе мы познакомились с интерпретатором ghci и основными типами. Рассмотрели много при-

меров.

32 | Глава 2: Первая программа

Page 33: Ru Haskell Book

ТипыBool – Основные операции: &&, ||, not, if c then t else eChar – Значения пишутся в ординарных кавычках, как в ’H’, ’+’String – Значения пишутся в двойных кавычках, как в ”Hello World”Int – Эффективные целые числа, но ограниченныеInteger – Не ограниченные целые числа, но не эффективныеDouble – Числа с двойной точностьюFloat – Числа с ординарной точностьюRational – Дробные числа

КлассыShow – ПечатьEq – Сравнение на равенствоNum – Сложение и умножение

Особенности синтаксиса

Префиксная и инфиксная запись применения функции

• add a b (+) a b

• a ‘add‘ b a + b

2.9 Упражнения• Определите экземпляры класса Show для всех типов из модуля Calendar.• Напишите функцию beside :: Nat -> Nat -> Bool, которая будет возвращать True только в томслучае, если два аргумента находятся рядом, т.е. один из них можно получить через другой операциейSucc.• Напишите функцию beside2 :: Nat -> Nat -> Bool, которая будет возвращать True только еслиаргументы являются соседями через некоторое другое число.• Напишите функцию возведения в степень pow :: Nat -> Nat -> Nat.• Напишите тип, описывающий бинарные деревья BinTree a. Бинарное дерево может быть либо листомсо значением типа a, либо хранить два поддерева.• Напишите функцию reverse :: BinTree a -> BinTree a, которая переворачивает дерево. Она меняетместами два элемента в узле дерева.• Напишите функцию depth :: BinTree a -> Nat, которая вычисляет глубину дерева, т.е. самый длинныйпуть от корня дерева к листу.• Напишите функцию leaves :: BinTree a -> [a], которая переводит бинарное дерево в список, воз-вращая все элементы в листьях дерева.• Обратите внимание на раздел List Operations в Prelude. Посмотрите на функции и их типы. Попро-буйте догадаться по типу функции и названию что она делает.• Попробуйте разобраться по документации с классами Ord (сравнение на больше/меньше) и Fractional(деление). Если у вас не получится, не беда, эти и многие другие стандартные классы мы рассмотримв отдельной главе.

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

Page 34: Ru Haskell Book

Глава 3

Типы

С помощью типов мы определяем все возможные значения в нашей программе. Мы определяем основныепримитивы и способы их комбинирования. Например в типе Nat:

data Nat = Zero | Succ Nat

Один конструктор-примитив Zero, и один конструктор Succ, с помощью которого мы можем делать со-ставные значения. Определив тип Nat таким образом, мы говорим, что значения типа Nat могут быть толькотакими:

Zero, Succ Zero, Succ (Succ Zero), Succ (Succ (Succ Zero)), ...

Все значения являются цепочками Succ с Zero на конце. Если где-нибудь мы попытаемся построить зна-чение, которое не соответствует нашему типу, мы получим ошибку компиляции, т.е. программа не пройдётпроверку типов. Так типы описывают множество допустимых значений.Значения, которые проходят проверку типов мы будем называть допустимыми, а те, которые не проходят

соответственно недопустимыми. Так например следующие значения недопустимы для Nat

Succ Zero Zero, Succ Succ, True, Zero (Zero Succ), ...

Недопустимых значений конечно гораздо больше. Такое проявляется и в естественном языке, бессмыс-ленных комбинаций слов гораздо больше, чем осмысленных предложений. Обратите внимание на то, что мыговорим о значениях (не)допустимых для некоторого типа, например значение True допустимо для Bool, нонедопустимо для Nat.Сами типы строятся не произвольным образом. Мы узнали, что при их построении используются две ос-

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

3.1 Структура алгебраических типов данныхИтак у нас лишь две операции: сумма и произведение. Давайте для начала рассмотрим два крайних

случая.

• Только произведение типов

data T = Name T1 T2 ... TN

Мы говорим, что значение нашего нового типа T состоит из значений типов T1, T2, …, TN и у нас естьлишь один способ составить значение этого типа, единственное, что мы можем сделать это применитьк значениям типов Ti конструктор Name.Пример:

data Time = Time Hour Second Minute

• Только сумма типов

data T = Name1 | Name2 | ... | NameN

34 | Глава 3: Типы

Page 35: Ru Haskell Book

Мы говорим, что у нашего нового типа T может быть лишь несколько значений, и перечисляем их вальтернативах через знак |.Пример:data Bool = True | False

Сделаем первое наблюдение: каждое произведение типов определяет новый конструктор. Число кон-структоров в типе равно числу альтернатив. Так в первом случае у нас была одна альтернатива и следова-тельно у нас был лишь один конструктор Name.Имена конструкторов должны быть уникальными во всей программе. У нас нет таких двух типов, у ко-

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

не случайна, она копирует структуру функции. В качестве имени функции выступает конструктор, а в ка-честве аргументов – значения заданных в произведении подтипов. Функция-конструктор после применения”оборачивает” значения аргументов и создаёт новое значение. За счёт этого мы могли бы определить типыпо-другому. Мы могли бы определить их в стиле классов типов:data Bool where

True :: BoolFalse :: Bool

Мы видим ”класс” Bool, у которого два метода. Или определим в таком стиле Nat:data Nat where

Zero :: NatSucc :: Nat -> Nat

Мы переписываем подтипы по порядку в аргументы метода. Или определим в таком стиле списки:data [a] where

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

Конструктор пустого списка [] является константой, а конструктор объединения элемента со списком(:), является функцией. Когда я говорил, что типы определяют примитивы и методы составления из прими-тивов, я имел ввиду, что некоторые конструкторы по сути являются константами, а другие функциями.Эти ”методы” определяют базовые значения типа, все другие значения будут комбинациями базовых.

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

3.2 Структура константМы уже знаем, что значения могут быть функциями и константами. Объявляя константу, мы даём имя-

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

Succ (Succ Zero)Neg (Add One (Mul Six Ten))Not (Follows A (And A B))Cons 1 (Cons 2 (Cons 3 (Cons 4 Nil)))

Заменим все функциональные конструкторы на букву f (от слова function), а все примитивные конструк-торы на букву c (от слова constant).f (f c)f (f c (f c c))f (f c (f c c))f c (f c (f c (f c c)))

Те кто знаком с теорией графов, возможно уже узнали в этой записи строчную запись дерева. Все зна-чения в Haskell являются деревьями. Узел дерева содержит составной конструктор, а лист дерева содержитпримитивный конструктор. Далее будет небольшой подраздел посвящённый терминологии теории графов,которая нам понадобится, будет много картинок, если вам это известно, то вы можете спокойно его пропу-стить.

Структура констант | 35

Page 36: Ru Haskell Book

Несколько слов о теории графовЕсли вы не знакомы с теорией графов, то сейчас как раз самое время с ней познакомится, хотя бы на

уровне основных терминов. Теория графов изучает дискретные объекты в терминах зависимостей междуобъектами или связей. При этом объекты и связи изображаются графически.Граф состоит из узлов и рёбер, которые соединяют узлы. Приведём пример графа:

h

g

f

ed

c

ba

43

21

6

78

5

Рис. 3.1: Граф

В этом графе восемь узлов, они пронумерованы, и восемь рёбер, они обозначены буквами. Теорию графовпридумал Леонард Эйлер, когда решал задачу о кёнингсбергских мостах. Он решал задачу о том, можно лиобойти все семь кёнингсбергских мостов, так чтобы пройти по каждому лишь один раз. Эйлер представилмосты в виде рёбер а участки суши в виде узлов графа и показал, что это сделать нельзя. Но мы отвлеклись.А что такое дерево? Дерево это такой граф, у которого нет циклов. Циклы – это замкнутые последова-

тельности рёбер. Например граф на рисунке выше не является деревом, но если мы сотрём ребро e, то у насполучится дерево.Ориентированный граф – это такой граф, у которого все рёбра являются стрелками, они ориентирова-

ны, отсюда и название. При этом теперь каждое ребро не просто связывает узлы, но имеет начало и конец. Вориентированных деревьях обычно выделяют один узел, который называют корнем. Его особенность заключа-ется в том, что все стрелки в ориентированном дереве как бы ”разбегаются” от корня. Корень определяет всестрелки в дереве. Проиллюстрируем на картинке, давайте сотрём ребро e и назначим первый узел корнем.Все наши стрелки будут идти от корня. Сначала мы проведём стрелки к узлам связанным с корнем:Затем представим, что каждый из этих узлов сам является корнем в своём дереве и повторим эту процеду-

ру. На этом шаге мы дорисовываем стрелки в поддеревьях, которые находятся в узлах 3 и 6. Узел 5 являетсявырожденным деревом, в нём всего лишь одна вершина. Мы будем называть такие поддеревья листьями.А невырожденные поддеревья мы будем называть узлами. Корневой узел в данном поддереве называют ро-дительским. А его соседние узлы, в которые направлены исходящие из него стрелки называют дочернимиузлами. На предыдущем шаге у нас появился один родительский узел 1, у которого три дочерних узла: 3, 6,и 5. А на этом шаге у нас появились ещё два родительских узла 3 и 6. У узла 3 один дочерний узел (4), а уузла 6 – три дочерних узла (2, 8, 7).Отметим, что положение узлов и рёбер на картинке не важно, главное это то, какие рёбра какие узлы

соединяют. Мы можем перерисовать это дерево в более привычном виде (рис. 3.4).Теперь если вы посмотрите на константы в Haskell вы заметите, что очень похожи на деревья. Листья со-

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

В теории графов порядок поддеревьев не важен, на рис. 3.4 мы могли бы нарисовать поддеревья в любомпорядке, главное сохранить связи. А в Haskell порядок следования аргументов в конструкторе важен. Нарис. 3.5 в виде деревьев изображены две константы.

36 | Глава 3: Типы

Page 37: Ru Haskell Book

h

g

f

d

c

ba

43

21

6

78

5

Рис. 3.2: Превращаем в дерево …

h

g

f

d

c

ba

43

21

6

78

5

Рис. 3.3: Превращаем в дерево …На рис. 3.5 изображены две константы, это Succ (Succ Zero) :: Nat и Neg (Add One (Mul Six Ten)) ::

Expr. Но они изображены немного по-другому. Я перевернул стрелки и добавил корнем ещё один узел, этотип константы.Стрелки перевёрнуты так, чтобы стрелки на картинке соответствовали стрелкам в типе конструктора.

Например по виду узла Succ :: Nat -> Nat, можно понять, что это функция от одного аргумента, в неёвпадает одна стрелка-аргумент и вытекает одна стрелка-значение. В конструктор Mul впадает две стрелки,значит это конструктор-функция от двух аргументов.Константы похожи на деревья за счёт структуры операции произведения типов. В произведении типов

мы пишем:

data Tnew = Name T1 T2 ... Tn

Так и получается, что у нашего узла New одна вытекающая стрелка, которая символизирует значение типаTnew и несколько впадающих стрелок T1, T2, …, Tn, они символизируют аргументы конструктора.

Структура констант | 37

Page 38: Ru Haskell Book

cfbh

adg

4 872

6653

1

Рис. 3.4: Ориентированное дерево

One

Add

Neg

Expr

TenSix

Mul

Zero

Succ

Succ

Nat

Рис. 3.5: КонстантыПотренируйтесь изображать константы в виде деревьев, вспомните константы из предыдущей главы, или

придумайте какие-нибудь новые.

Строчная запись деревьевИтак все константы в Haskell за счёт особой структуры построения типов являются деревьями, но мы

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

узлы и листья. Сначала мы пишем имя в текущем узле, затем через пробел имена в дочерних узлах, если намвстречается невырожденный узел мы заключаем его в скобки. Давайте последовательно запишем в строчнойзаписи дерево из первого примера:Начнём с корня и будем последовательно дописывать поддеревья, точками обозначаются дочерние узлы,

которые нам ещё предстоит дописать:

(1 . . . )(1 (3 .) 5 (6 . . .))(1 (3 4) 5 (6 2 7 8))

Мы можем ставить любое число пробелов между дочерними узлами, здесь для наглядности точки выров-нены. Так мы можем закодировать исходное дерево строкой. Часто самые внешние кавычки опускаются. Витоге получилась такая запись:

38 | Глава 3: Типы

Page 39: Ru Haskell Book

4 872

6653

1

Рис. 3.6: Ориентированное деревоtree = 1 (3 4) 5 (6 2 7 8)

По этой записи мыможем понять, что у нас есть два конструктора трёх аргументов 1 и 6, один конструктородного аргумента 3 и пять примитивных конструкторов. Точно так же мы строим и все другие константы вHaskell:

Succ (Succ (Succ Zero))Time (Hour 13) (Minute 10) (Second 0)Mul (Add One Ten) (Neg (Mul Six Zero))

За одним исключением, если конструктор бинарный, символьный (начинается с двоеточия), мы помеща-ем его между аргументов:

(One :+ Ten) :* (Neg (Six :* Zero))

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

новое имя, пусть и составное. Мы можем написать 5, или 2+3, это лишь два разных имени для одной кон-станты. Теперь мы разобрались с тем, что константы это деревья. Значит функции строят одни деревья издругих. Как они это делают? Для этого этого в Haskell есть две операции: это композиция и декомпозиция де-ревьев. С помощью композиции мы строим из простых деревьев сложные, а с помощью декомпозиции разбиваемсоставные деревья на простейшие.Композиция и декомпозиция объединены в одной операции, с которой мы уже встречались, это операция

определения синонима. Давайте вспомним какое-нибудь объявление функции:

(+) a Zero = a(+) a (Succ b) = Succ (a + b)

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

show (Time h m s) = show h ++ ”:” ++ show m ++ ”:” ++ show s

Слева от знака равно мы также выделили из составного дерева (Time h m s) три его дочерних для корняузла и связали их с переменными h, m и s. А справа от знака равно мы составили из этих переменных новоевыражение.Итак операцию объявления синонима можно представить в таком виде:

name декомпозиция = композиция

В каждом уравнении у нас три части: новое имя, декомпозиция, поступающих на вход аргументов, икомпозиция нового значения. Теперь давайте остановимся поподробнее на каждой из этих операций.

Структура функций | 39

Page 40: Ru Haskell Book

Композиция и частичное применениеКомпозиция строится по очень простому правилу, если у нас есть значение f типа a -> b и значение x

типа a, мы можем получить новое значение (f x) типа b. Это основное правило построения новых значений,поэтому давайте запишем его отдельно:

f :: a -> b, x :: a--------------------------

(f x) :: b

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

торую мы обошли стороной. В случае деревьев мы строили только константы, и конструктор получал столькоаргументов, сколько у него было дочерних узлов (или подтипов). Так мы строили константы. Но в Haskell мыможем с помощью применения строить функции на лету, передавая меньшее число аргументов, этот процессназывается частичным применением или каррированием (currying). Поясним на примере, предположим у насесть функция двух аргументов:

add :: Nat -> Nat -> Natadd a b = ...

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

add :: Nat -> (Nat -> Nat)add a b = ...

Функция add является функцией одного аргумента, которая в свою очередь возвращает функцию одногоаргумента (Nat -> Nat). Когда мы пишем в где-нибудь в правой части функции:

... = ... (add Zero (Succ Zero)) ...

Компилятор воспринимает эту запись так:

... = ... ((add Zero) (Succ Zero)) ...

Присмотримся к этому выражению, что изменилось? У нас появились новые скобки, вокруг выражения(add Zero). Давайте посмотрим как происходит применение:

add :: Nat -> (Nat -> Nat), Zero :: Nat----------------------------------------------

(add Zero) :: Nat -> Nat

Итак применение функции add к Zero возвращает новую функцию (add Zero), которая зависит от одногоаргумента. Теперь применим к этой функции второе значение:

(add Zero) :: Nat -> Nat, (Succ Zero) :: Nat----------------------------------------------

((add Zero) (Succ Zero)) :: Nat

И только теперь мы получили константу. Обратите внимание на то, что получившаяся константа не можетпринять ещё один аргумент. Поскольку в правиле для применения функция f должна содержать стрелку, ау нас есть лишь Nat, это значение может участвовать в других выражениях лишь на месте аргумента.Тоже самое работает и для функций от большего числа аргументов, если мы пишем

fun :: a1 -> a2 -> a3 -> a4 -> res

... = fun a b c d

На самом деле мы пишем

fun :: a1 -> (a2 -> (a3 -> (a4 -> res)))

... = (((fun a) b) c) d

40 | Глава 3: Типы

Page 41: Ru Haskell Book

Это очень удобно. Так, определив лишь одну функцию fun, мы получили в подарок ещё три функции(fun a), (fun a b) и (fun a b c). С ростом числа аргументов растёт и число подарков. Если смотреть нафункцию fun, как на функцию одного аргумента, то она представляется таким генератором функций типаa2 -> a3 -> a4 -> res, который зависит от параметра. Применение функций через пробел значительноупрощает процесс комбинирования функций.Поэтому в Haskell аргументы функций, которые играют роль параметров или специфических флагов,

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

process :: Param1 -> Param2 -> Arg1 -> Arg2 -> Result

Два первых аргумента функции process выступают в роли параметров для генерации функций с типомArg1 -> Arg2 -> Result.Давайте потренируемся с частичным применением в интерпретаторе. Для этого загрузим модуль Nat из

предыдущей главы:

Prelude> :l Nat[1 of 1] Compiling Nat ( Nat.hs, interpreted )Ok, modules loaded: Nat.*Nat> let add = (+) :: Nat -> Nat -> Nat*Nat> let addTwo = add (Succ (Succ Zero))*Nat> :t addTwoaddTwo :: Nat -> Nat*Nat> addTwo (Succ Zero)Succ (Succ (Succ Zero))*Nat> addTwo (addTwo Zero)Succ (Succ (Succ (Succ Zero)))

Сначала мы ввели локальную переменную add, и присвоили ей метод (+) из класса Num для Nat. Нампришлось выписать тип функции, поскольку ghci не знает для какого экземпляра мы хотим определить этотсиноним. В данном случае мы подсказали ему, что это Nat. Затем с помощью частичного применения мыобъявили новый синоним addTwo, как мы видим из следующей строки это функция оного аргумента. Онапринимает любое значение типа Nat и прибавляет к нему двойку. Мы видим, что этой функцией можнопользоваться также как и обычной функцией.Попробуем выполнить тоже самое для функции с символьной записью имени:

*Nat> let add2 = (+) (Succ (Succ Zero))*Nat> add2 ZeroSucc (Succ Zero)

Мы рассмотрели частичное применение для функций в префиксной форме записи. В префиксной фор-ме записи функция пишется первой, затем следуют аргументы. Для функций в инфиксной форме записисуществует два правила применения.Это применение слева:

(*) :: a -> (b -> c), x :: a-----------------------------

(x *) :: b -> c

И применение справа:

(*) :: a -> (b -> c), x :: b-----------------------------

(* x) :: a -> c

Обратите внимание на типы аргумента и возвращаемого значения. Скобки в выражениях (x*) и (*x)обязательны. Применением слева мы фиксируем в бинарной операции первый аргумент, а применениемсправа – второй.Поясним на примере, для этого давайте возьмём функцию минус (-). Если мы напишем (2-) 1 то мы

получим 1, а если мы напишем (-2) 1, то мы получим -1. Проверим в интерпретаторе:

*Nat> (2-) 11*Nat> (-2) 1

<interactive>:4:2:

Структура функций | 41

Page 42: Ru Haskell Book

No instance for (Num (a0 -> t0))arising from a use of syntactic negation

Possible fix: add an instance declaration for (Num (a0 -> t0))In the expression: - 2In the expression: (- 2) 1In an equation for ‘it’: it = (- 2) 1

Ох уж этот минус. Незадача. Ошибка произошла из-за того, что минус является хамелеоном. Если мыпишем -2, компилятор воспринимает минус как унарную операцию, и думает, что мы написали константуминус два. Это сделано для удобства, но иногда это мешает. Это единственное такое исключение в Haskell.Давайте введём новый синоним для операции минус:*Nat> let (#) = (-)*Nat> (2#) 11*Nat> (#2) 1-1

Эти правила левого и правого применения работают и для буквенных имён в инфиксной форме записи:*Nat> let minus = (-)*Nat> (2 ‘minus‘ ) 11*Nat> ( ‘minus‘ 2) 1-1

Так если мы хотим на лету получить новую функцию, связав в функции второй аргумент мы можемнаписать:... = ... ( ‘fun‘ x) ...

Частичное применение для функций в инфиксной форме записи называют сечением (section), они бываютсоответственно левыми и правыми.

Связь с логикойОтметим связь основного правила применения с Modus Ponens, известным правилом вывода в логике:

a -> b, a-------------

b

Оно говорит о том, что если у нас есть выражение из a следует b и мы знаем, что a истинно, мы смеломожем утверждать, что b тоже истинно. Если перевести это правило на Haskell, то мы получим: Если у насопределена функция типа a -> b и у нас есть значение типа a, то мы можем получить значение типа b.

Декомпозиция и сопоставление с образцомДекомпозиция применяется слева от знака равно, при этом наша задача состоит в том, чтобы опознать

дерево определённого вида и выделить из него некоторые поддеревья. Мы уже пользовались декомпозициеймного раз в предыдущих главах, давайте выпишем примеры декомпозиции:not :: Bool -> Boolnot True = ...not False = ...

xor :: Bool -> Bool -> Boolxor a b = ...

show :: Show a => a -> String

show (Time h m s) = ...

addZero :: String -> StringaddZero (a:[]) = ...addZero as = ...

(*) a Zero = ...(*) a (Succ b) = ...

42 | Глава 3: Типы

Page 43: Ru Haskell Book

Декомпозицию можно проводить в аргументах функции. Там мы видим строчную запись дерева, в узлахстоят конструкторы (начинаются с большой буквы), переменные (с маленькой буквы) или символ безразлич-ной переменой (подчёркивание).С помощью конструкторов, мы указываем те части, которые обязательно должны быть в дереве для дан-

ного уравнения. Так уравнениеnot True = ...

сработает, только если на вход функции поступит значение True. Мы можем углубляться в дерево значениянастолько насколько нам позволят типы, так мы можем определить функцию:is7 :: Nat -> Boolis7 (Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))) = Trueis7 _ = False

С помощью переменных мы даём синонимы поддеревьям. Этими синонимами мы можем пользоваться вправой части функции. Так в уравненииaddZero (a:[])

мы извлекаем первый элемент из списка, и одновременно говорим о том, что список может содержать толькоодин элемент. Отметим, что если мы хотим дать синоним всему дереву а не какой-то части, мы просто пишемна месте аргумента переменную, как в случае функции xor:xor a b = ...

С помощью безразличной переменной говорим, что нам не важно, что находится у дерева в этом узле.Уравнения в определении синонима обходятся сверху вниз, поэтому часто безразличной переменной поль-зуются в смысле ”а во всех остальных случаях”, как в:instance Eq Nat where

(==) Zero Zero = True(==) (Succ a) (Succ b) = a == b(==) _ _ = False

Переменные и безразличные переменные также могут уходить вглубь дерева сколь угодно далеко (иливвысь дерева, поскольку первый уровень в строчной записи это корень):lessThan7 :: Nat -> BoollessThan7 (Succ (Succ (Succ (Succ (Succ (Succ (Succ _))))))) = FalselessThan7 _ = True

Декомпозицию можно применять только к значениям константам. Проявляется интересная закономер-ность, если для композиции необходимым элементом было значение со стрелочным типом (функция), тов случае декомпозиции нам нужно значение с типом без стрелок (константа). Это говорит о том, что всефункции будут полностью применены, т.е. константы будут записаны в виде строчной записи дерева. Еслимы ожидаем на входе функцию, то мы можем только дать ей синоним с помощью с помощью переменнойили проигнорировать её безразличной переменной.Как в

name (Succ (Succ Zero)) = ...name (Zero : Succ Zero : []) = ...

Но неname Succ = ...name (Zero :) = ...

Отметим, что для композиции это допустимые значения, в первом случае это функция Nat -> Nat, а вовтором это функция типа [Nat] -> [Nat].Ещё одна особенность декомпозиции заключается в том, что при декомпозиции мы можем пользоваться

только ”настоящими” значениями, т.е. конструкторами, объявленными в типах. В случае композиции мымогли пользоваться как конструкторами, так и синонимами.Например мы не можем написать в декомпозиции:

name (add Zero Zero) = ...name (or (xor a b) True) = ...

В Haskell декомпозицию принято называть сопоставлением с образцом (pattern matching). Термин намекаетна то, что в аргументе мы выписываемшаблон (или заготовку) для целого набора значений. Наборы значениймогут получиться, если мы пользуемся переменными. Конструкторы дают нам возможность зафиксироватьвид ожидаемого на вход дерева.

Структура функций | 43

Page 44: Ru Haskell Book

3.4 Проверка типовВ этом разделе мы поговорим об ошибках проверки типов. Почти все ошибки, которые происходят в

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

f :: a -> b, x :: a--------------------------

(f x) :: b

Что может привести к ошибке? В этом правиле есть два источника ошибки.

• Тип f не содержит стрелок, или f не является функцией.• Типы x и аргумента для f не совпадают.

Вот и все ошибки. Универсальное представление всех функций в виде функций одного аргумента, значи-тельно сокращает число различных видов ошибок. Итак мыможем ошибиться применяя значение к константеи передав в функцию не то, что она ожидает.Потренируемся в интерпретаторе, сначала попытаемся создать ошибку первого типа:

*Nat> Zero Zero

<interactive>:1:1:The function ‘Zero’ is applied to one argument,but its type ‘Nat’ has noneIn the expression: Zero ZeroIn an equation for ‘it’: it = Zero Zero

Если перевести на русский интерпретатор говорит:

*Nat> Zero Zero

<interactive>:1:1:Функция ’Zero’ применяется к одному аргументу,но её тип ’Nat’ не имеет аргументов

В выражении: Zero ZeroВ уравнении для ‘it’: it = Zero Zero

Компилятор увидел применение функции f x, далее он посмотрел, что x = Zero, из этого на основеправила применения он сделал вывод о том, что f имеет тип Nat -> t, тогда он заглянул в f и нашёл тамZero :: Nat, что и привело к несовпадению типов.Составим ещё одно выражение с такой же ошибкой:

*Nat> True Succ

<interactive>:6:1:The function ‘True’ is applied to one argument,but its type ‘Bool’ has noneIn the expression: True SuccIn an equation for ‘it’: it = True Succ

В этом выражении аргумент Succ имеет тип Nat -> Nat, значит по правилу вывода тип True равен (Nat-> Nat) -> t, где t некоторый произвольный тип, но мы знаем, что True имеет тип Bool.Теперь перейдём к ошибкам второго типа. Попробуем вызывать функции с неправильными аргументами:

*Nat> :m +Prelude*Nat Prelude> not (Succ Zero)

<interactive>:9:6:Couldn’t match expected type ‘Bool’ with actual type ‘Nat’In the return type of a call of ‘Succ’In the first argument of ‘not’, namely ‘(Succ Zero)’In the expression: not (Succ Zero)

44 | Глава 3: Типы

Page 45: Ru Haskell Book

Опишем действия компилятора в терминах правила применения. В этом выражении у нас есть три зна-чения not, Succ и Zero. Нам нужно узнать тип выражения и проверить правильно ли оно построено.

not (Succ Zero) - ?

not :: Bool -> Bool, Succ :: Nat -> Nat, Zero :: Nat----------------------------------------------------------

f x, f = not и x = (Succ Zero)------------------------------------------------------------

f :: Bool -> Bool следовательно x :: Bool-------------------------------------------------------------

(Succ Zero) :: Bool

Воспользовавшись правилом применения мы узнали, что тип выражения Succ Zero должен быть равенBool, проверим так ли это?

(Succ Zero) - ?Succ :: Nat -> Nat, Zero :: Nat

----------------------------------------------------------f x, f = Succ, x = Zero следовательно (f x) :: Nat

----------------------------------------------------------(Succ Zero) :: Nat

Из этой цепочки следует, что (Succ Zero) имеет тип Nat, мы пришли к противоречию и сообщаем обэтом пользователю.

<interactive>:1:5:Не могу сопоставить ожидаемый тип ’Bool’ с выведенным ’Nat’В типе результата вызова ‘Succ’

В первом аргументе ‘not’, а именно ‘(Succ Zero)’В выражении: not (Succ Zero)

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

Специализация типов при подстановке

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

*Nat> Succ Zero + ZeroSucc (Succ Zero)

Происходит специализация общей функции (+) :: Num a => a -> a -> a до функции (+) :: Nat ->Nat -> Nat, которая определена в экземпляре Num для Nat.

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

некоторому классу f :: C a => a -> b, тогда значение, которое мы подставляем в функцию должно бытьэкземпляром класса C.Для иллюстрации давайте попробуем сложить логические значения:

*Nat Prelude> True + False

<interactive>:11:6:No instance for (Num Bool)

arising from a use of ‘+’Possible fix: add an instance declaration for (Num Bool)In the expression: True + FalseIn an equation for ‘it’: it = True + False

Компилятор говорит о том, что для типа Bool не определён экземпляр для класса Num

Проверка типов | 45

Page 46: Ru Haskell Book

No instance for (Num Bool)

Запишем это в виде правила:

f :: C a => a -> b, x :: T, instance C T-----------------------------------------

(f x) :: b

Важно отметить, что x имеет конкретный тип T, если x – значение, у которого тип с параметром, компи-лятор не сможет определить для какого типа конкретно мы хотим выполнить применение.У этого поведения есть исключение, по умолчанию числа приводятся к Integer, если они не содержат

знаков после точки, и к Double если содержат.

*Nat Prelude> let f = (1.5 + )*Nat Prelude> :t ff :: Double -> Double*Nat Prelude> let x = 5 + 0*Nat Prelude> :t xx :: Integer*Nat Prelude> let x = 5 + Zero*Nat Prelude> :t xx :: Nat

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

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

name декомпозиция = композиция

Существует несколько правил для построения композиций:

• Одно для функций в префиксной форме записи:

f :: a -> b, x :: a-------------------------------

(f x) :: b

• И два для функций в инфиксной форме записи:Это левое сечение:

(*) :: a -> (b -> c), x :: a---------------------------------

(x *) :: b -> c

И правое сечение:

(*) :: a -> (b -> c), x :: b---------------------------------

(* x) :: a -> c

Декомпозиция происходит в аргументах функции. С её помощью мы можем извлечь из составнойконстанты-дерева какую-нибудь часть. Или указать на какие константы мы реагируем в данном уравнении.Ещё мы узнали о частичном применении. О том, что все функции в Haskell являются функциями одного

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

правил применения узнаёт, что они неправильные.

46 | Глава 3: Типы

Page 47: Ru Haskell Book

notSucc

Рис. 3.7: Конструкторы и синонимы

3.6 Упражнения• В этой главе было много картинок и графических аналогий, попробуйте попрограммировать в картин-ках. Нарисуйте определённые нами функции или какие-нибудь новые в виде деревьев. Например этоможно сделать так. Мы будем отличать конструкторы от синонимов. Конструкторы будем рисовать вординарном кружке, а синонимы в двойном:Мы будем все функции писать также как и прежде, но вместо аргументов слева от знака равно и выра-жений справа от знака равно будем рисовать деревья.Например объявим простой синоним-константу (рис. 3.8). Мы будем дорисовывать сверху типы значе-ний, вместо объявления типа функции.

Zero

Succ

Nat=one

Рис. 3.8: Синоним-константа

Несколько функций для списков. Извлечение первого элемента (рис. 3.9) и функция преобразованиявсех элементов списка (рис. 3.10).

x

a=

x

:

[a]head

Рис. 3.9: Функция извлечения первого элемента списка

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

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

Page 48: Ru Haskell Book

mapf

:

[b]

xsfx

=

xsx

:

[a]

f

a->bmap

[]

[b]=

[]

[a]

f

a->bmap

Рис. 3.10: Функция преобразования элементов списка• Составьте в интерпретаторе как можно больше неправильных выражений и посмотрите, на сообще-ния об ошибках. Разберитесь почему выражение оказалось неправильным. Для этого проверьте типы спомощью правил применения.

48 | Глава 3: Типы

Page 49: Ru Haskell Book

Глава 4

Декларативный и композиционныйстиль

В Haskell существует несколько встроенных выражений, которые облегчают построение функций и дела-ют код более наглядным. Их можно разделить на два вида: выражения, которые поддерживают декларативныйстиль (declarative style) определения функций, и выражения которые поддерживают композиционный стиль(expression style).Что это за стили? В декларативном стиле определения функций больше похожи на математическую но-

тацию, словно это предложения языка. В композиционном стиле мы строим из маленьких выражений болеесложные, применяем к этим выражениям другие выражения и строим ещё большие.В Haskell есть полноценная поддержка и того и другого стиля, поэтому конструкции которые мы рас-

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

4.1 Локальные переменныеВспомним определение площади треугольника по трём сторонам:

S =√p · (p− a) · (p− b) · (p− c)

Где a, b и c – длины сторон треугольника, а p это полупериметр.Как бы мы определили эту функцию теми средствами, что у нас есть? Наверное мы бы написали так:

square a b c = sqrt (p a b c * (p a b c - a) * (p a b c - b) * (p a b c - c))

p a b c = (a + b + c) / 2

Согласитесь это не многим лучше чем решение в лоб:

square a b c = sqrt ((a+b+c)/2 * ((a+b+c)/2 - a) * ((a+b+c)/2 - b) * ((a+b+c)/2 - c))

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

square a b c = sqrt (p * (p - a) * (p - b) * (p - c))

p = (a + b + c) / 2

Нам нужно, чтобы p знало, что a, b и c берутся из аргументов функции square. В этом нам помогутлокальные переменные.

where-выраженияВ декларативном стиле для этого предусмотрены where-выражения. Они пишутся так:

square a b c = sqrt (p * (p - a) * (p - b) * (p - c))where p = (a + b + c) / 2

| 49

Page 50: Ru Haskell Book

Или так:square a b c = sqrt (p * (p - a) * (p - b) * (p - c)) where

p = (a + b + c) / 2

За определением функции следует специальное слово where, которое вводит локальные имена-синонимы. При этом аргументы функции включены в область видимости имён. Синонимов может бытьнесколько:square a b c = sqrt (p * pa * pb * pc)

where p = (a + b + c) / 2pa = p - apb = p - bpc = p - c

Отметим, что отступы обязательны. Haskell по отступам понимает, что эти выражения относятся к where.Как и в случае объявления функций порядок следования локальных переменных в where-выражении не

важен. Главное чтобы в выражениях справа от знака равно мы пользовались именами из списка аргументовисходной функции или другими определёнными именами. Локальные переменные видны только в пределахтой функции, в которой они вводятся.Что интересно, слева от знака равно в where-выражениях можно проводить декомпозицию значений, так-

же как и в аргументах функции:pred :: Nat -> Natpred x = y

where (Succ y) = x

Эта функция делает тоже самое что и функцияpred :: Nat -> Natpred (Succ y) = y

В where-выражениях можно определять новые функции а также выписывать их типы:add2 x = succ (succ x)

where succ :: Int -> Intsucc x = x + 1

А можно и не выписывать, компилятор догадается:add2 x = succ (succ x)

where succ x = x + 1

Но иногда это бывает полезно, при использовании классов типов, для избежания неопределённости при-менения.Приведём ещё один пример. Посмотрим на функцию фильтрации списков, она определена в Prelude:

filter :: (a -> Bool) -> [a] -> [a]filter p [] = []filter p (x:xs) = if p x then x : rest else rest

where rest = filter p xs

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

where-выражения определяются для каждого уравнения в определении функции:even :: Nat -> Booleven Zero = res

where res = Trueeven (Succ Zero) = res

where res = Falseeven x = even res

where (Succ (Succ res)) = x

Конечно в этом примере where не нужны, но здесь они приведены для иллюстрации привязки where-выражения к данному уравнению. Мы определили три локальных переменных с одним и тем же именем.

where-выражения могут быть и у значений, которые определяются внутри where-выражений. Но лучшеизбегать сильно вложенных выражений.

50 | Глава 4: Декларативный и композиционный стиль

Page 51: Ru Haskell Book

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

square a b c = let p = (a + b + c) / 2in sqrt (p * (p - a) * (p - b) * (p - c))

Слова let и in – ключевые. Выгодным отличием let-выражений является то, что они являются обычнымивыражениями и не привязаны к определённому месту как where-выражения. Они могут участвовать в любойчасти обычного выражения:

square a b c = let p = (a + b + c) / 2in sqrt ((let pa = p - a in p * pa) *

(let pb = p - bpc = p - c

in pb * pc))

В этом проявляется их принадлежность композиционному стилю. let-выражения могут участвовать влюбом подвыражении, они также группируются скобками. А where-выражения привязаны к уравнениям вопределении функции.Также как и в where-выражениях, в let-выражениях слева от знака равно можно проводить декомпозицию

значений.

pred :: Nat -> Natpred x = let (Succ y) = x

in y

Определим функцию фильтрации списков через let:

filter :: (a -> Bool) -> [a] -> [a]filter p [] = []filter p (x:xs) =

let rest = filter p xsin if p x then x : rest else rest

4.2 ДекомпозицияДекомпозиция или сопоставление с образцом позволяет выделять из составных значений, простейшие

значения с помощью которых они были построены

pred (Succ x) = x

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

not True = Falsenot False = True

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

ментах функции. Рассмотрим одну полезную возможность при декомпозиции. Иногда нам хочется провестидекомпозицию и дать псевдоним всему значению. Это можно сделать с помощью специального символа @.Например определим функцию, которая возвращает соседние числа для данного числа Пеано:

beside :: Nat -> (Nat, Nat)beside Zero = error ”undefined”beside x@(Succ y) = (y, Succ x)

В выражении x@(Succ y) мы одновременно проводим разбор и даём имя всему значению. В этом опре-делении нам встретился новый тип – кортеж. В Haskell кортежи пишутся в скобках, значения разделяютсязапятыми. В отличие от списка кортеж может содержать значения разных типов, но размер его фиксирован.Мы пользуемся кортежами, когда хотим вернуть из функции несколько значений.

Декомпозиция | 51

Page 52: Ru Haskell Book

case-выраженияОказывается декомпозицию можно проводить в любом выражении, для этого существуют case-

выражения:data AnotherNat = None | One | Two | Many

deriving (Show, Eq)

toAnother :: Nat -> AnotherNattoAnother x =

case x ofZero -> NoneSucc Zero -> OneSucc (Succ Zero) -> Two_ -> Many

fromAnother :: AnotherNat -> NatfromAnother None = ZerofromAnother One = Succ ZerofromAnother Two = Succ (Succ Zero)fromAnother Many = error ”undefined”

Слова case и of – ключевые. Выгодным отличием case-выражений является то, что нам не приходит-ся каждый раз выписывать имя функции. Обратите внимание на то, что в case-выражениях также можнопользоваться обычными переменными и безымянными переменными.Для проведения декомпозиции по нескольким переменнымможно воспользоваться кортежами. Например

определим знакомую функцию равенства для Nat:instance Eq Nat where

(==) a b =case (a, b) of

(Zero, Zero) -> True(Succ a’, Succ b’) -> a’ == b’_ -> False

Мы проводим сопоставление с образцом по кортежу (a, b), соответственно слева от знака -> мы прове-ряем значения в кортежах, для этого мы также заключаем значения в скобки и пишем их через запятую.Давайте определим функцию filter в ещё более композиционном стиле. Для этого мы заменим в исход-

ном определении where на let и декомпозицию в аргументах на case-выражение:filter :: (a -> Bool) -> [a] -> [a]filter p a =

case a of[] -> []x:xs -> let rest = filter p xs

in if (p x)then (x:rest)else rest

4.3 Условные выраженияС условными выражениями мы уже сталкивались в сопоставлении с образцом. Например в определении

функции not:not True = Falsenot False = True

В зависимости от поступающего значения мы выбираем одну из двух альтернатив. Условные выражениив сопоставлении с образцом позволяют реагировать лишь на частичное (с учётом переменных) совпадениедерева значения в аргументах функции.Часто нам хочется определить более сложные условия для альтернатив. Например, если значение на

входе функции больше 2, но меньше 10, верни A, а если больше 10, верни B, а во всех остальных случаяхверни C. Или если на вход поступила строка состоящая только из букв латинского алфавита, верни A, ав противном случае верни B. Нам бы хотелось реагировать лишь в том случае, если значение некотороготипа a удовлетворяет некоторому предикату. Предикатами обычно называют функции типа a -> Bool. Мыговорим, что значение удовлетворяет предикату, если предикат для этого значения возвращает True.

52 | Глава 4: Декларативный и композиционный стиль

Page 53: Ru Haskell Book

Охранные выраженияВ декларативном стиле условные выражения представлены охранными выражениями (guards). Предполо-

жим у нас есть тип:

data HowMany = Little | Enough | Many

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

hallCapacity :: Int -> HowManyhallCapacity n

| n < 10 = Little| n < 30 = Enough| True = Many

Специальный символ | уже встречался нам в определении типов. Там он играл роль разделителя аль-тернатив в сумме типов. Здесь же он разделяет альтернативы в условных выражениях. Сначала мы пишем| затем выражение-предикат, которое возвращает значение типа Bool, затем равно и после равно – возвра-щаемое значение. Альтернативы так же как и в случае декомпозиции аргументов функции обходятся сверхувниз, до тех пор пока в одной из альтернатив предикат не вернёт значение True. Обратите внимание на то,что нам не нужно писать во второй альтернативе:

| 10 <= n && n < 30 = Enough

Если вычислитель дошёл до этой альтернативы, значит значение точно больше либо равно 10. Посколькув предыдущей альтернативе предикат вернул False.Предикат в последней альтернативе является константой True, он пройдёт сопоставление с любым зна-

чением n. В данном случае, если учесть предыдущие альтернативы мы знаем, что если вычислитель дошёлдо последней альтернативы , значение n больше либо равно 30. Для повышения наглядности кода в Preludeопределена специальная константа-синоним значению True под именем otherwise.Определим функцию filter для списков в более декларативном стиле, для этого заменим if-выражение

в исходной версии на охранные выражения:

filter :: (a -> Bool) -> [a] -> [a]filter p [] = []filter p (x:xs)

| p x = x : rest| otherwise = restwhere rest = filter p xs

Или мы можем разместить охранные выражения по-другому:

filter :: (a -> Bool) -> [a] -> [a]filter p [] = []filter p (x:xs) | p x = x : rest

| otherwise = restwhere rest = filter p xs

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

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

all :: (a -> Bool) -> [a] -> Boolall p [] = Trueall p (x:xs)

| p x = all p xs| otherwise = False

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

Условные выражения | 53

Page 54: Ru Haskell Book

all :: (a -> Bool) -> [a] -> Boolall p [] = Trueall p (x:xs) = p x && all p xs

Или так:

all :: (a -> Bool) -> [a] -> Boolall p xs = null (filter notP xs)

where notP x = not (p x)

Или даже так)

import Prelude(all)

Функция null определена в Prelude она возвращает True только если список пуст.

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

Вспомним как они выглядят:

a = if boolthen x1else x2

Слова if, then и else – ключевые. Тип a, x1 и x2 совпадают.Любое охранное выражение, в котором больше одной альтернативы, можно представить в виде if-

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

hallCapacity :: Int -> HowManyhallCapacity n =

if (n < 10)then Littleelse (if n < 30

then Enoughelse Many)

all :: (a -> Bool) -> [a] -> Boolall p [] = Trueall p (x:xs) = if (p x) then all p xs else False

4.4 Определение функцийПод функцией мы понимаем составной синоним, который принимает аргументы, возможно разбирает их

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

УравненияВ декларативном стиле функции определяются с помощью уравнений. Пока мы видели лишь этот способ

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

name декомпозиция1 = композиция1name декомпозиция2 = композиция2...name декомпозицияN = композицияN

Где name – имя функции. В декомпозиции происходит разбор поступающих на вход значений, а в компо-зиции происходит составление значения результата. Уравнения обходятся вычислителем сверху вниз до техпор пока он не найдёт такое уравнение, для которого переданные в функции значения не подойдут в указан-ный в декомпозиции шаблон значений (если сопоставление с образцом аргументов пройдёт успешно). Кактолько такое уравнение найдено, составляется выражение справа от знака равно (композиция). Это значениебудет результатом функции. Если такое уравнение не будет найдено программа остановится с ошибкой.К примеру попробуйте вычислить в интерпретаторе выражение notT False, для такой функции:

54 | Глава 4: Декларативный и композиционный стиль

Page 55: Ru Haskell Book

notT :: Bool -> BoolnotT True = False

Что мы увидим?

Prelude> notT False*** Exception: <interactive>:1:4-20: Non-exhaustive patterns in function notT

Интерпретатор сообщил нам о том, что он не нашёл уравнения для переданного в функцию значения.

Безымянные функцииВ композиционном стиле функции определяются по-другому. Это необычный метод, он пришёл в

Haskell из лямбда-исчисления. Функции строятся с помощью специальных конструкций, которые называ-ются лямбда-функциями. По сути лямбда-функции являются безымянными функциями. Давайте посмотримна лямбда функцию, которая прибавляет к аргументу единицу:

\x -> x + 1

Для того, чтобы превратить лямбда-функцию в обычную функцию мысленно замените знак \ на имяnoName, а стрелку на знак равно:

noName x = x + 1

Мы получили обычную функцию Haskell, с такими мы уже много раз встречались. Зачем специальныйсинтаксис для определения безымянных функций? Ведь можно определить её в виде уравнений. К тому жекому могут понадобиться безымянные функции? Ведь смысл функции в том, чтобы выделить определённыйшаблон поведения и затем ссылаться на него по имени функции.Смысл безымянной функции в том, что ею, также как и любым другим элементом композиционного

стиля, можно пользоваться в любой части обычных выражений. С её помощью мы можем создавать функции”на лету”. Предположим, что мы хотим профильтровать список чисел, мы хотим выбрать из них лишь те, чтоменьше 10, но больше 2, и к тому же они должны быть чётными. Мы можем написать:

f :: [Int] -> [Int]f = filter p

where p x = x > 2 && x < 10 && even x

При этом нам приходится давать какое-нибудь имя предикату, например p. С помощью безымянной функ-ции мы могли бы написать так:

f :: [Int] -> [Int]f = filter (\x -> x > 2 && x < 10 && even x)

Смотрите мы составили предикат сразу в аргументе функции filter. Выражение (\x -> x > 2 && x <10 && even x) является обычным значением.Возможно у вас появился вопрос, где аргумент функции? Где тот список по которому мы проводим филь-

трацию. Ответ на этот вопрос кроется в частичном применении. Давайте вычислим по правилу применениятип функции filter:

f :: (a -> Bool) -> [a] -> [a], x :: (Int -> Bool)------------------------------------------------------

(f x) :: [Int] -> [Int]

После применения параметр a связывается с типом Int, поскольку при применении происходит сопостав-ление более общего предиката a -> Bool из функции filter с тем, который мы передали первым аргументомInt -> Bool. После этого мы получаем тип (f x) :: [Int] -> [Int] это как раз тип функции, которая прини-мает список целых чисел и возвращает список целых чисел. Частичное применение позволяет нам не писатьв таких выражениях:

f xs = filter p xswhere p x = ...

последний аргумент xs.К примеру вместо

Определение функций | 55

Page 56: Ru Haskell Book

add a b = (+) a b

мы можем просто написать:

add = (+)

Такой стиль определения функций называют бесточечным (point-free).Давайте выразим функцию filter с помощью лямбда-функций:

filter :: (a -> Bool) -> ([a] -> [a])filter = \p -> \xs -> case xs of

[] -> [](x:xs) -> let rest = filter p xs

in if p xthen x : restelse rest

Мы определили функцию filter пользуясь только элементами композиционного стиля. Обратите внима-ние на скобки в объявлении типа функции. Я хотел напомнить вам о том, что все функции в Haskell являютсяфункциями одного аргумента. Это определение функции filter как нельзя лучше подчёркивает этот факт.Мы говорим, что функция filter является функцией одного аргумента p в выражении \p ->, которая возвра-щает также функцию одного аргумента. Мы выписываем это в явном виде в выражении \xs ->. Далее идётвыражение, которое содержит определение функции.Отметим, что лямбда функции могут принимать несколько аргументов, в предыдущем определении мы

могли бы написать:

filter :: (a -> Bool) -> ([a] -> [a])filter = \p xs -> case xs of

...

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

функций (все они определены в Prelude):

fst :: (a, b) -> afst = \(a, _) -> a

snd :: (a, b) -> bsnd = \(_, b) -> b

swap :: (a, b) -> (b, a)swap = \(a, b) -> (b, a)

Обратите внимание на то, что все функции словно являются константами. Они не содержат аргументов.Аргументы мы ”пристраиваем” с помощью безымянных функций.Определим функции применения функции к первому и второму элементам списков (эти функции опре-

делены в модуле Control.Arrow)

first :: (a -> a’) -> (a, b) -> (a’, b)first = \f (a, b) -> (f a, b)

second :: (b -> b’) -> (a, b) -> (a, b’)second = \f (a, b) -> (a, f b)

Также в Prelude есть полезные функции, которые превращают функции с частичным применением вобычны функции и наоборот:

curry :: ((a, b) -> c) -> a -> b -> ccurry = \f -> \a -> \b -> f (a, b)

uncurry :: (a -> b -> c) -> ((a, b) -> c)uncurry = \f -> \(a, b) -> f a b

56 | Глава 4: Декларативный и композиционный стиль

Page 57: Ru Haskell Book

Функция curry принимает функцию двух аргументов для которой частичное применение невозможно.Это имитируется с помощью кортежей. Функция принимает кортеж из двух элементов. Функция curry (отслова каррирование, частичное применение) превращает такую функцию в обычную функцию Haskell. Афункция uncurry выполняет обратное преобразование.С помощью лямбда-функций можно имитировать локальные переменные. Так например можно перепи-

сать формулу для вычисления площади треугольника:

square a b c =(\p -> sqrt (p * (p - a) * (p - b) * (p - c)))((a + b + c) / 2)

Смотрите мы определили функцию, которая принимает параметром полупериметр p и передали в неёзначение ((a + b + c) / 2). Если в нашей функции несколько локальных переменных, то мы можемсоставить лямбда-функцию от нескольких переменных и подставить в неё нужные значения.

4.5 Какой стиль лучше?Основной критерий выбора заключается в том, сделает ли этот элемент код более ясным. Наглядность

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

или иной стиль. Начнём с класса Ord и посмотрим на определения по умолчанию:

-- Тип упорядочивания

data Ordering = LT | EQ | GTderiving (Eq, Ord, Enum, Read, Show, Bounded)

class (Eq a) => Ord a wherecompare :: a -> a -> Ordering(<), (<=), (>=), (>) :: a -> a -> Boolmax, min :: a -> a -> a

-- Минимальное полное определение:-- (<=) или compare-- Использование compare может оказаться более-- эффективным для сложных типов.

compare x y| x == y = EQ| x <= y = LT| otherwise = GT

x <= y = compare x y /= GTx < y = compare x y == LTx >= y = compare x y /= LTx > y = compare x y == GT

max x y| x <= y = y| otherwise = x

min x y| x <= y = x| otherwise = y

Все функции определены в декларативном стиле. Тип Ordering кодирует результат операции сравнения.Два числа могут быть либо равны (значение EQ), либо первое меньше второго (значение LT), либо первоебольше второго (значение GT).Обратите внимание на функцию compare. Мы не пишем дословное определение значений типа Ordering:

compare x y| x == y = EQ| x < y = LT| x > y = GT

Какой стиль лучше? | 57

Page 58: Ru Haskell Book

В этом случае функция compare была бы определена через две других функции класса Ord, а именнобольше > и меньше <. Мы же хотим минимизировать число функций в этом определении. Поэтому вместоэтого определения мы полагаемся на очерёдность обхода альтернатив в охранном выражении.Если первый случай не прошёл, то во втором случае нет разницы между функциями < и <=. А если не

прошёл и этот случай, то остаётся только вернуть значение GT. Так мы определили функцию compare черезодну функцию класса Ord.Теперь посмотрим на несколько полезных функций для списков. Посмотрим на три основные функции

для списков, одна из них возможно вам уже порядком поднадоела:-- Преобразование спискаmap :: (a -> b) -> [a] -> [b]map f [] = []map f (x:xs) = f x : map f xs

-- Фильтрация спискаfilter :: (a -> Bool) -> [a] -> [a]filter p [] = []filter p (x:xs) | p x = x : filter p xs

| otherwise = filter p xs

-- Свёртка спискаfoldr :: (a -> b -> b) -> b -> [a] -> bfoldr f z [] = zfoldr f z (x:xs) = f x (foldr f z xs)

Все эти функции определены в декларативном стиле. С преобразованием списка и фильтрацией мы ужесталкивались. Функция свёртки списка заменяет в узлах дерева списка все конструкторы [] на значение zи все конструкторы (:) на функцию двух аргументов f :: (a -> b -> b). Это очень полезная функция спомощью неё можно определить много других функций на списках. Приведём несколько примеров:and, or :: [Bool] -> Booland = foldr (&&) Trueor = foldr (||) False

(++) :: [a] -> [a] -> [a][] ++ ys = ys(x:xs) ++ ys = x : (xs ++ ys)

concat :: [[a]] -> [a]concat = foldr (++) []

Функции and и or выполняют логические операции на списках. Так каждый конструктор (:) заменяетсяна соответствующую логическую операцию, а пустой список заменяется на значение, которое не влияет нарезультат выполнения данной логической операции. Имеется ввиду, что функции (&& True) и (|| False)дают тот же результат, что и функция id x = x.Функция (++) объединяет два списка, а функция concat выполняет ту же операцию, но на списке списков.Функция zip принимает два списка и смешивает их в список пар. Как только один из списков оборвётся

оборвётся и список-результат. Эта функция является частным случаем более общей функции zipWith, кото-рая принимает функцию двух аргументов и два списка и составляет новый список попарных применений.-- zip-ыzip :: [a] -> [b] -> [(a, b)]zip = zipWith (,)

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]zipWith z (a:as) (b:bs) = z a b : zipWith z as bszipWith _ _ _ = []

Посмотрим как работают эти функции в интерпретаторе:Prelude> zip [1,2,3] ”hello”[(1,’h’),(2,’e’),(3,’l’)]Prelude> zipWith (+) [1,2,3] [3,2,1][4,4,4]Prelude> zipWith (*) [1,2,3] [5,4,3,2,1][5,8,9]

58 | Глава 4: Декларативный и композиционный стиль

Page 59: Ru Haskell Book

Отметим, что в Prelude также определена обратная функция unzip:unzip :: [(a,b)] -> ([a], [b])

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

тился нам лишь в функции break. Но давайте посмотрим и на функции с композиционным стилем:lines :: String -> [String]lines ”” = []lines s = let (l, s’) = break (== ’\n’) s

in l : case s’ of[] -> [](_:s’’) -> lines s’’

Функция line разбивает строку на список строк. Эти строки были разделены в исходной строке символомпереноса ’\textbackslash n’.Функция break принимает предикат и список и возвращает два списка. В первом все элементы от начала

списка, которые не удовлетворяют предикату, а во втором все остальные. Наш предикат (== ’\n’) выделяетвсе символы кроме переноса каретки. В строкеlet (l, s’) = break (== ’\n’) s

Мы сохраняем все символы до ’\textbackslash n’ от начала строки в переменной l. Затем мы рекурсивновызываем функцию lines на оставшейся части списка:

in l : case s’ of[] -> [](_:s’’) -> lines s’’

При этом мы пропускаем в s’ первый элемент, поскольку он содержит символ переноса каретки.Посмотрим на ещё одну функцию для работы со строками.

words :: String -> [String]words s = case dropWhile Char.isSpace s of

”” -> []s’ -> w : words s’’

where (w, s’’) = break Char.isSpace s’

Функция words делает тоже самое, что и lines, только теперь в качестве разделителя выступает пробел.Функция dropWhile отбрасывает от начала списка все элементы, которые удовлетворяют предикату. В строкеcase dropWhile Char.isSpace s of

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

”” -> []s’ -> w : words s’’

where (w, s’’) = break Char.isSpace s’

Если строка пуста, то делать больше нечего. Если – нет, мы также как и в предыдущей функции приме-няем функцию break для того, чтобы выделить все элементы кроме пробела, а затем рекурсивно вызываемфункцию words на оставшейся части списка.

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

появлялись парами. Сведём их в таблицу:Элемент Декларативный стиль Композиционный стиль

Локальные переменные where-выражения let-выраженияДекомпозиция Сопоставление с образцом в уравнениях case-выраженияУсловные выражения Охранные выражения if-выраженияОпределение функций Уравнения лямбда-функции

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

Page 60: Ru Haskell Book

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

(a, b)(a, b, c)(a, b, c, d)...

Особенности синтаксисаНам встретилась новая конструкция в сопоставлении с образцом:

beside :: Nat -> (Nat, Nat)beside Zero = error ”undefined”beside x@(Succ y) = (y, Succ x)

Она позволяет проводить декомпозицию и давать имя всему значению одновременно. Такие выраженияx(…)@ в англоязычной литературе принято называть as-patterns.

4.7 Упражнения• В этой главе нам встретилось много полезных стандартных функций, потренируйтесь с ними в интер-претаторе. Вызывайте их с различными значениями, экспериментируйте.• Попробуйте определить функции из предыдущих глав в чисто композиционном стиле.• Посмотрите на те функции, которые мы прошли и попробуйте переписать их определения шиворотна выворот. Если вы видите, что элемент написан композиционном стиле перепишите его в деклара-тивном и наоборот. Получившиеся функции могут показаться монстрами, но это упражнение можетпомочь вам в закреплении новых конструкций и почувствовать сильные и слабые стороны того илииного стиля.• Определите модуль, который будет вычислять площади простых фигур, треугольника, окружности,прямоугольника, трапеции. Помните, что фигуры могут задаваться различными способами.• Поток это бесконечный список, т.е. список, у которого нет конструктора пустого списка:data Stream a = a :& Stream a

Так например мы можем составить поток из всех чисел Пеано:nats :: Nat -> Stream Natnats a = a :& nats (Succ a)

Или поток, который содержит один и тот же элемент:constStream :: a -> Stream aconstStream a = a :& constStream a

Напишите модуль для потоков. В первую очередь нам понадобятся функции выделения частей потока,поскольку мы не сможем распечатать поток целиком (ведь он бесконечный):-- Первый элемент потокаhead :: Stream a -> a

-- Хвост потока, всё кроме первого элементаtail :: Stream a -> Stream a

-- n-тый элемент потока(!!) :: Stream a -> Int -> a

-- Берёт из потока несколько первых элементов:take :: Int -> Stream a -> [a]

Имена этих функций будут совпадать с именами функций для списков чтобы избежать коллизий имёнмы воспользуемся квалифицированным импортом функций. Делается это так:

60 | Глава 4: Декларативный и композиционный стиль

Page 61: Ru Haskell Book

import qualified Prelude as P( определения )

Слова qualified и as – ключевые. Теперь для использования функций из модуля Prelude мы будемписать P.имяФункции. Такие имена называются квалифицированными. Для того чтобы пользоватьсяквалифицированными именами только для тех функций, для которых возможна коллизия имён можнопоступить так:

import qualified Prelude as Pimport Prelude

Компилятор разберётся, какую функцию мы имеем в виду.Для удобства тестирования можно определить такую функцию печати потоков:

instance Show a => Show (Stream a) whereshow xs = showInfinity (show (take 5 xs))

where showInfinity x = P.init x P.++ ”...”

Функция P.init выделяет все элементы списка кроме последнего. В данном случае она откусит отстроки закрывающуюся скобку. После этого мы добавляем троеточие, как символ бесконечности спис-ка.Функции преобразования потоков:

-- Преобразование потокаmap :: (a -> b) -> Stream a -> Stream b

-- Фильтрация потокаfilter :: (a -> Bool) -> Stream a -> Stream a

-- zip-ы для потоков:zip :: Stream a -> Stream b -> Stream (a, b)

zipWith :: (a -> b -> c) -> Stream a -> Stream b -> Stream c

Функция генерации потока:

iterate :: (a -> a) -> a -> Stream a

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

iterate f a = a :& f a :& f (f a) :& f (f (f a)) :& ...

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

nats = iterate Succ ZeroconstStream a = iterate (\x -> x) a

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

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

Page 62: Ru Haskell Book

Глава 5

Функции высшего порядка

Функцией высшего порядка называют функцию, которая может принимать на вход функции или возвращатьфункции в качестве результата. За счёт частичного применения в Haskell все функции, которые принимаютболее одного аргумента, являются функциями высшего порядка.В этой главе мы подробно обсудим способы составления функций, недаром Haskell – функциональный

язык. В Haskell функции являются очень гибким объектом, они позволяют выделять сложные способы ком-бинирования значений. Часто за счёт развитых средств составления новых функций в Haskell пользовательопределяет лишь базовые функции, получая остальные ”на лету” применением двух-трёх операций, это вы-глядит примерно как (2+3)*5, где вместо чисел стоят базовые функции, а операции + и * составляют новыефункции из простейших.К примеру мы можем определить экземпляр Num для функций, которые возвращают числа. Смысл этих

операций заключается в том, что теперь мы применяем обычные операции сложения умножения к функциям,аргумент которых совпадает по типу. Например для того чтобы умножить функции \t -> t+2 и \t -> t+3мы составляем новую функцию \t -> (t+2) * (t+3), которая получает на вход значение t применяет его ккаждой из функций и затем умножает результаты:

module FunNat where

import Prelude(Show(..), Eq(..), Num(..), error)

instance Show (t -> a) whereshow _ = error ”Sorry, no show. It’s just for Num”

instance Eq (t -> a) where(==) _ _ = error ”Sorry, no Eq. It’s just for Num”

instance Num a => Num (t -> a) where(+) = fun2 (+)(*) = fun2 (*)(-) = fun2 (-)

abs = fun1 abssignum = fun1 signum

fromInteger n = \t -> fromInteger n

fun1 :: (a -> b) -> ((t -> a) -> (t -> b))fun1 op a = \t -> op (a t)

fun2 :: (a -> b -> c) -> ((t -> a) -> (t -> b) -> (t -> c))fun2 op a b = \t -> a t ‘op‘ b t

Функции fun1 и fun2 превращают функции, которые принимают значения, в функции, которые прини-мают другие функции.Из-за контекста класса Num нам пришлось объявить два фиктивных экземпляра для классов Show и Eq.

Загрузим модуль FunNat в интерпретатор и посмотрим что же у нас получилось:

Prelude> :l FunNat.hs[1 of 1] Compiling FunNat ( FunNat.hs, interpreted )Ok, modules loaded: FunNat.*FunNat> 2 2

62 | Глава 5: Функции высшего порядка

Page 63: Ru Haskell Book

2*FunNat> 2 52*FunNat> (2 + (+1)) 03*FunNat> ((+2) * (+3)) 112

На первый взгляд кажется что выражение 2 2 не должно пройти проверку типов, ведь мы применяемзначение к константе. Но на самом деле 2 это не константа, а значение 2 :: Num a => a. Поскольку в нашеммодуле мы определили экземпляр Num для функций, второе число 2 было конкретизировано по умолчаниюдо Integer, а первое число 2 было конкретизировано до Integer -> Integer. Компилятор вывел из контек-ста, что под 2 мы понимаем функцию. Функция была создана с помощью метода fromInteger. Эта функцияпринимает любое значение и возвращает двойку.Далее мы складываем и перемножаем функции словно это обычные значения. Что интересно мы можем

составлять и такие выражения:

*FunNat> let f = ((+) - (*))*FunNat> f 1 21

Как была вычислена эта функция? Мы определили экземпляр функций для значений типа Num a => t-> a. Если мы вспомним, что функция двух аргументов на самом деле является функцией одного аргумента:Num a => t1 -> (t2 -> a), мы заметим, что тип Num a => (t2 -> a) принадлежит Num, теперь если мыобозначим его за a’, то мы получим тип Num a’ => t1 -> a’, это совпадает с нашим исходным экземпляром.Получается, что за счёт механизма частичного применения мы одним махом определили экземпляры Num

для функций любого числа аргументов, которые возвращают значение типа Num.Итак функция f имеет вид:

\t1 t2 -> (t1 + t2) - (t1 * t2)

Подставим значения:

(\t1 t2 -> (t1 + t2) - (t1 * t2)) 1 2(\t2 -> (1 + t2) - (1 * t2) 2(1 + 2) - (1 * 2)3 - 21

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

часто используются бинарные операции для составления функций ”на лету”. В этом помогает и частичноеприменение, мы можем в одном выражении применить к функции часть аргументов, построить из неё новуюфункцию с помощью какой-нибудь такой бинарной операции и всё это передать в другую функцию!Для сокращения числа скобок нам понадобится разобраться в понятии приоритета операции. Так напри-

мер в выражении

> 2 + 3 * 1032

Мы полагаем, что умножение имеет больший приоритет чем сложение и со скобками это выражениебудет выглядеть так:

> 2 + (3 * 10)32

Фраза ”больший приоритет” означает: сначала умножение потом сложение. Мы всегда можем изменитьповедение по умолчанию с помощью скобок:

> (2 + 3) * 1050

Приоритет инфиксных операций | 63

Page 64: Ru Haskell Book

В Haskell приоритет функций складывается из двух понятий: старшинство и ассоциативность. Старшин-ство определяется числами, они могут быть от 0 до 9. Чем больше это число, тем выше приоритет функций.Старшинство используется вычислителем для группировки разных операций, например (+) имеет стар-

шинство 6, а (*) имеет старшинство 7. Поэтому интерпретатор сначала ставит скобки вокруг выражения с(*), а затем вокруг (+). Считается, что обычное префиксное применение имеет высший приоритет 10. Нельзязадать приоритет выше применения, это значит, что операция ”пробел” будет всегда выполняться первой.Ассоциативность используется для группировки одинаковых операций, например мы видим:

1+2+3+4

Как нам быть? Мы можем группировать скобки слева направо:

((1+2)+3)+4

Или справа налево:

1+(2+(3+4))

Ответ на этот вопрос даёт ассоциативность, она бывает левая и правая. Например операции (+) (-) и (*)являются лево-ассоциативными, а операция возведения в степень (^) является право-ассоциативной.

1 + 2 + 3 == (1 + 2) + 31 ^ 2 ^ 3 == 1 ^ (2 ^ 3)

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

*FunNat> :m PreludePrelude> :i (+)class (Eq a, Show a) => Num a where(+) :: a -> a -> a...

-- Defined in GHC.Numinfixl 6 +Prelude> :i (*)class (Eq a, Show a) => Num a where...(*) :: a -> a -> a...

-- Defined in GHC.Numinfixl 7 *Prelude> :i (^)(^) :: (Num a, Integral b) => a -> b -> a -- Defined in GHC.Realinfixr 8 ^

Приоритет указывается в строчках infixl 6 + и infixl 7 *. Цифра указывает на старшинство операции,а суффикс l (от англ. left – левая) или r (от англ. right – правая) на ассоциативность.Если мы создали свою функцию, мы можем определить для неё ассоциативность. Для этого мы пишем в

коде:

module Fixity where

import Prelude(Num(..))

infixl 4 ***infixl 5 +++infixr 5 ‘neg‘

(***) = (*)(+++) = (+)neg = (-)

Мы ввели новые операции и поменяли старшинство операций сложения и умножения местами и изме-нили ассоциативность у вычитания. Проверим в интерпретаторе:

64 | Глава 5: Функции высшего порядка

Page 65: Ru Haskell Book

Prelude> :l Fixity[1 of 1] Compiling Fixity ( Fixity.hs, interpreted )Ok, modules loaded: Fixity.*Fixity> 1 + 2 * 37*Fixity> 1 +++ 2 *** 39*Fixity> 1 - 2 - 3-4*Fixity> 1 ‘neg‘ 2 ‘neg‘ 32

Посмотрим как это вычислялось:1 + 2 * 3 == 1 + (2 * 3)1 +++ 2 *** 3 == (1 +++ 2) *** 3

1 - 2 - 3 == (1 - 2) - 31 ‘neg‘ 2 ‘neg 3‘ == 1 ‘neg‘ (2 ‘neg‘ 3)

Также в Haskell есть директива infix это тоже самое, что и infixl.

5.2 Обобщённые функцииВ этом разделе мы познакомимся с несколькими функциями, которые принимают одни функции и состав-

ляют по ним другие. Эти функции используются в Haskell очень часто. Все они живут в модуле Data.Function.Модуль Prelude экспортирует их из этого модуля.

Функция тождестваНачнём с самой простой функции. Это функция id. Она ничего не делает с аргументом, просто возвращает

его:id :: a -> aid x = x

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

Константная функцияСледующая функция const принимает значение и возвращает постоянную функцию. Эта функция будет

возвращать константу для любого переданного в неё значения:const :: a -> b -> aconst a _ = a

Вспомним определение из модуля FunNat для метода fromInteger:fromInteger n = \t -> fromInteger n

С помощью функции const мы могли бы написать его в так:fromInteger n = const (fromInteger n)

Функция const является конструктором постоянных функций. С её помощью мы можем легко построитьи постоянную функцию двух аргументов:const2 a = const (const a)

Вспомним определение для &&:(&&) :: Bool -> Bool -> Bool(&&) True x = x(&&) False _ = False

С помощью функций id и const мы можем сократить число аргументов и уравнений:(&&) :: Bool -> Bool -> Bool(&&) a = if a then id else (const False)

Также мы можем определить и логическое ”или”:(||) :: Bool -> Bool -> Bool(||) a = if a then (const True) else id

Обобщённые функции | 65

Page 66: Ru Haskell Book

Функция композицииФункция композиции принимает две функции и составляет из них последовательное применение функ-

ций:(.) :: (b -> c) -> (a -> b) -> a -> c(.) f g = \x -> f (g x)

Это очень полезная функция. Она позволяет нанизывать функции друг на друга. Мы перехватываем выходвторой функции, сразу подставляем его в первую и возвращаем её выход в качестве результата.Вспомним определение вспомогательной функции fun1 из модуля FunNat:

fun1 :: (a -> b) -> ((t -> a) -> (t -> b))fun1 op a = \t -> op (a t)

С помощью функции композиции мы могли бы определить её так:fun1 :: (a -> b) -> ((t -> a) -> (t -> b))fun1 = (.)

Это и есть функция композиции. Приведём пример немного посложнее:add :: Nat -> Nat -> Natadd a Zero = aadd a (Succ b) = Succ (add a b)

Если мы определим функцию свёртки для Nat, которая будет заменять в значении типа Nat конструкторына соответствующие по типу функции:foldNat :: a -> (a -> a) -> Nat -> afoldNat zero succ Zero = zerofoldNat zero succ (Succ b) = succ (foldNat zero succ b)

То мы можем переписать с помощью функции композиции эту функцию так:add :: Nat -> Nat -> Natadd = foldNat id (Succ . )

Куда делись аргументы? Они выражаются через функции id и (.). Поведение этой функции лучше про-иллюстрировать на примере. Пусть у нас есть два числа типа Nat:two = Succ (Succ Zero)three = Succ (Succ (Succ Zero))

Вычислимadd two three

Вспомним о частичном применении:add two three

=> (add two) three=> (foldNat id (Succ . ) (Succ (Succ Zero))) three

Теперь функция свёртки заменит все конструкторы Succ на (Succ . ), а конструкторы Zero на id:=> ((Succ . ) ((Succ . ) id)) three

Что это за монстр?((Succ . ) ((Succ . ) id))

Функция (Succ . ) это левое сечение операции (.). Эта функция, которая принимает функции и возвра-щает функции. Она принимает функцию и навешивает на её выход конструктор Succ. Давайте упростим этобольшое выражение с помощью определений функций (.) и id:

((Succ . ) ((Succ . ) id))=> (Succ . ) (\x -> Succ (id x))=> (Succ . ) (\x -> Succ x)=> \x -> Succ (Succ x)

Теперь нам осталось применить к этой функции наше второе значение:(\x -> Succ (Succ x)) three

=> Succ (Succ three)=> Succ (Succ (Succ (Succ (Succ x))))

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

66 | Глава 5: Функции высшего порядка

Page 67: Ru Haskell Book

Приоритет функции композицииПосмотрим на приоритет функции композиции:

Prelude> :i (.)(.) :: (b -> c) -> (a -> b) -> a -> c -- Defined in GHC.Baseinfixr 9 .

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

fun a b = fun1 a . fun2 (x1 + x2) . fun3 . (+x1)

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

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

Prelude> let f = foldr (.) id [sin, cos, sin, cos, exp, (+1), tan]Prelude> f 20.6330525927559899Prelude> f 150.7978497904127007

Функция foldr заменит в списке каждый конструктор (:) на функцию композиции, а пустой список нафункцию id. В результате получается композиция из всех функций в списке.Это очень похоже на сложение или умножение чисел в списке. При этом в качестве нуля (для сложения)

или единицы (для умножения) мы используем функцию id. Мы пользуемся тем, что по определению длялюбой функции f выполнены тождества:

f . id == fid . f == f

Поэтому мы можем использовать id в качестве накопителя результата композиции, как в случае:

Prelude> foldr (*) 1 [1,2,3,4]24

Если сравнить (.) с умножением, то id похоже на единицу, а (const a) на ноль. В самом деле для любойфункции f и любого значения a выполнено тождество:

const a . f == const a

Мы словно умножаем функцию на ноль, делая её вычисление бессмысленным.

Функция примененияЕщё одной очень полезной функцией является функция применения ($). Посмотрим на её определение:

infixr 0 $

($) :: (a -> b) -> a -> bf $ x = f x

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

foldNat zero succ (Succ b) = succ (foldNat zero succ b)

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

foldNat zero succ (Succ b) = succ $ foldNat zero succ b

Если бы мы написали без скобок:

Обобщённые функции | 67

Page 68: Ru Haskell Book

... = succ foldNat zero succ b

То выражение было бы сгруппировано так:

... = (((succ foldNat) zero) succ) b

Но поскольку мы поставили барьер в виде операции ($) с низким приоритетом, группировка скобокпроизойдёт так:

... = (succ $ ((foldNat zero) succ) b)

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

xs :: [Int]xs = reverse $ map ((+1) . (*10)) $ filter even $ ns 40

ns :: Int -> [Int]ns 0 = []ns n = n : ns (n - 1)

В списке xs мы сначала создаём в функции ns убывающий список чисел, затем оставляем лишь чётные,потом применяем два арифметических действия ко всем элементам списка, затем переворачиваем список.Проверим работает ли это в интерпретаторе, заодно поупражняемся в композиционном стиле:

Prelude> let ns n = if (n == 0) then [] else n : ns (n - 1)Prelude> let even x = 0 == mod x 2Prelude> let xs = reverse $ map ((+1) . (*10)) $ filter even $ ns 20Prelude> xs[21,41,61,81,101,121,141,161,181,201]

Если бы не функция применения нам пришлось бы написать это выражение так:

xs = reverse (map ((+1) . (*10)) (filter even (ns 40)))

Функция перестановкиФункция перестановки flip принимает функцию двух аргументов и меняет аргументы местами:

flip :: (a -> b -> c) -> b -> a -> cflip f x y = f y x

К примеру:

Prelude> foldr (-) 0 [1,2,3,4]-2Prelude> foldr (flip (-)) 0 [1,2,3,4]-10

Иногда это бывает полезно.

Функция onФункция on (от англ. на) перед применением бинарной функции пропускает аргументы через унарную

функцию:

on :: (b -> b -> c) -> (a -> b) -> a -> a -> c(.*.) ‘on‘ f = \x y -> f x .*. f y

Она часто используется в сочетании с функцией sortBy из модуля Data.List. Эта функция имеет тип:

sortBy :: (a -> a -> Ordering) -> [a] -> [a]

Она сортирует элементы списка согласно некоторой функции упорядочивания f :: (a -> a -> Ordering).С помощью функции on мы можем легко составить такую функцию на лету:

68 | Глава 5: Функции высшего порядка

Page 69: Ru Haskell Book

let xs = [(3, ”John”), (2, ”Jack”), (34, ”Jim”), (100, ”Jenny”), (-3, ”Josh”)]Prelude> :m +Data.List Data.FunctionPrelude Data.List Data.Function>Prelude Data.List Data.Function> sortBy (compare ‘on‘ fst) xs[(-3,”Josh”),(2,”Jack”),(3,”John”),(34,”Jim”),(100,”Jenny”)]Prelude Data.List Data.Function> map fst $ sortBy (compare ‘on‘ fst) xs[-3,2,3,34,100]Prelude Data.List Data.Function> map snd $ sortBy (compare ‘on‘ fst) xs[”Josh”,”Jack”,”John”,”Jim”,”Jenny”]

Мы импортировали в интерпретатор модуль Data.List для функции sortBy а также модульData.Function для функции on. Они не импортируются модулем Prelude.Выражением (compare ‘on‘ fst) мы составили функцию

\a b -> compare (fst a) (fst b)

fst = \(a, b) -> a

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

5.3 Функциональный калькуляторВспомним самый первый пример из этой главы. Там мы сделали экземпляром класса Num функции из

произвольного типа в числа. Теперь давайте составим несколько выражений с теми функциями, о кото-рых мы только что узнали. Для этого добавим в модуль FunNat директиву импорта функций из модуляData.Function. Также добавим несколько основных функций для списков и класс Ord:

module FunNat where

import Prelude(Show(..), Eq(..), Ord(..), Num(..), error)

import Data.Function(id, const, (.), ($), flip, on)import Prelude(map, foldr, filter, zip, zipWith)

...

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

Prelude> :load FunNat[1 of 1] Compiling FunNat ( FunNat.hs, interpreted )Ok, modules loaded: FunNat.

Составим функцию, которая принимает один аргумент, умножает его на два, вычитает 10 и берёт модульчисла.

*FunNat> let f = abs $ id * 2 - 10*FunNat> f 26*FunNat> f 1010

Давайте посмотрим как была составлена эта функция:

abs $ id * 2 - 10

=> abs $ (id * 2) - 10 -- приоритет умножения=> abs $ (\x -> x * \x -> 2) - 10 -- развернём id и 2=> abs $ (\x -> x * 2) - 10 -- по определению (*) для функций=> abs $ (\x -> x * 2) - \x -> 10 -- развернём 10=> abs $ \x -> (x * 2) - 10 -- по определению (-) для функций=> \x -> abs x . \x -> (x * 2) - 10 -- по определению abs для функций=> \x -> abs ((x * 2) - 10) -- по определению (.)

=> \x -> abs ((x * 2) - 10)

Функциональный калькулятор | 69

Page 70: Ru Haskell Book

Функция возведения в квадрат:

*FunNat> let f = id * id*FunNat> map f [1,2,3,4,5][1,4,9,16,25]*FunNat> map (id * id - 1) [1,2,3,4,5][0,3,8,15,24]

Обратите внимание на краткость записи. В этом выражении (id * id - 1) проявляется основное пре-имущество бесточечного стиля, избавившись от аргументов, мы можем пользоваться функциями так, словноэто простые значения. Этот приём используется в Haskell очень активно. Пока нам встретились лишь двеинфиксных операции для функций (это композиция и применение с низким приоритетом), но в будущем выстолкнётесь с целым морем подобных операций. Все они служат одной цели, они прячут аргументы функции,позволяя быстро составлять функции на лету из примитивов. Чтобы не захлебнуться в этом море помните,что скорее всего новый символ означает либо композицию либо применение для функций специальноговида.Возведём в четвёртую степень:

*FunNat> map (f . f) [1,2,3,4,5][1,16,81,256,625]

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

*FunNat> let x = const id*FunNat> let y = flip $ const id*FunNat> let d = x * x + y * y*FunNat> d 1 25*FunNat> d 3 213

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

*FunNat> filter ((<10) . d 1) [1,2,3,4,5][1,2]*FunNat> zipWith d [1,2,3] [3,2,1][10,8,10]*FunNat> foldr (x*x - y*y) 0 [1,2,3,4]3721610024*FunNat> zipWith ((-) * (-) + const id) [1,2,3] [3,2,1][7,2,5]

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

5.4 Функции, возвращающие несколько значенийФункции, которые возвращают несколько значений, реализованы в Haskell с помощью кортежей. Напри-

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

decons :: Stream a -> (a, Stream a)decons (a :& as) = (a, as)

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

Единичный типЕдиничный тип (unit type) состоит из одного элемента, он пишется как пустой кортеж (). Зачем нам может

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

70 | Глава 5: Функции высшего порядка

Page 71: Ru Haskell Book

ПарыПары это кортежи, состоящие из двух элементов. Пары стоят особняком, возможно потому, что с их по-

мощью можно имитировать любые другие кортежи большего размера. Например так:(a, b)(a, (b, c))(a, (b, (c, d)))...

Отметим несколько функций для работы с парами из модуля Prelude. Извлечение элементов:fst :: (a, b) -> asnd :: (a, b) -> b

И несколько функций из модуля Control.Arrow, которые выполняют преобразование элементов:first :: Arrow fun => fun a a’ -> fun (a, b) (a’, b)second :: Arrow fun => fun b b’ -> fun (a, b) (a, b’)

Пока не обращайте внимание на класс Arrow, это обобщение обычного функционального типа. Для функ-ционального типа определён экземпляр, так что этими функциями можно пользоваться как функциями стипами:first :: (a -> a’) -> (a, b) -> (a’, b)second :: (b -> b’) -> (a, b) -> (a, b’)

Приведём пример в интерпретаторе:Prelude> :m + Control.ArrowPrelude Control.Arrow> first (+10) (1, ”Hello”)(11,”Hello”)Prelude Control.Arrow> second reverse (1, ”Hello”)(1,”olleH”)Prelude Control.Arrow>

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

мые значения с помощью сопоставления с образцом и затем использовать эти значения в других функциях.Посудите сами если у нас есть функции:f :: a -> (b1, b2)g :: b1 -> (c1, c2)h :: b2 -> (c3, c4)

Мы уже не сможем комбинировать их так просто как если бы это были обычные функции без кортежей.q x = (\(a, b) -> (g a, h b)) (f x)

В случае пар нам могут прийти на помощь функции first и second:q = first g . second h . f

Если мы захотим составить какую-нибудь другую функцию из q, то ситуация заметно усложнится. Функ-ции, возвращающие кортежи, сложнее комбинировать в бесточечном стиле. Здесь стоит вспомнить правилоUnix.

Пишите функции, которые делают одну вещь, но делают её хорошо.

Функция, которая возвращает кортеж пытается сделать сразу несколько дел. И теряет в гибкости, ейтрудно взаимодействовать с другими функциями. Старайтесь чтобы таких функций было как можно меньше.Если функция возвращает несколько значений, попытайтесь разбить её на несколько, которые возвраща-

ют лишь одно значение. Часто бывает так, что эти значения тесно связаны между собой и такую функциюне удаётся разбить на несколько составляющих. Если у вас появляется много таких функций, то это поводзадуматься о создании нового типа данных.Например в качестве точки на плоскости можно использовать пару (Float, Float). В этом случае, если

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

Функции, возвращающие несколько значений | 71

Page 72: Ru Haskell Book

rotate :: Float -> (Float, Float) -> (Float, Float)norm :: (Float, Float) -> (Float, Float)translate :: (Float, Float) -> (Float, Float) -> (Float, Float)...

Все они стараются делать несколько дел одновременно, возвращая кортежи. Но мы можем изменитьситуацию определением новых типов:

data Point = Point Float Floatdata Vector = Vector Float Floatdata Angle = Angle Float

Объявления функций станут более краткими и наглядными.

rotate :: Angle -> Point -> Pointnorm :: Point -> Pointtranslate :: Vector -> Point -> Point...

5.5 Краткое содержаниеОсновные функции высшего порядкаМы познакомились с функциями из модуля Data.Function. Их можно разбить на несколько типов:

• Примитивные функции (генераторы функций).

id = \x -> xconst a = \_ -> a

• Функции, которые комбинируют функции или функции и значения:

f . g = \x -> f (g x)f $ x = f x

(.*.) ‘on‘ f = \x y -> f x .*. f y

• Преобразователи функций, принимают функцию и возвращают функцию:

flip f = \x y -> f y x

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

infixl 3 #infixr 6 ‘op‘

Приоритет складывается из двух частей: старшинства (от 1 до 9) и ассоциативности (бывает левая иправая). Старшинство определяет распределение скобок между разными функциями:

infixl 6 +infixl 7 *

1 + 2 * 3 == 1 + (2 * 3)

А ассоциативность – между одинаковыми:

infixl 6 +infixr 8 ^

1 + 2 + 3 == (1 + 2) + 31 ^ 2 ^ 3 == 1 ^ (2 ^ 3)

72 | Глава 5: Функции высшего порядка

Page 73: Ru Haskell Book

5.6 Упражнения• Просмотрите написанные вами функции, или функции из примеров. Можно ли их переписать с по-мощью основных функций высшего порядка? Если да, то перепишите. Попробуйте определить их вбесточечном стиле.• В прошлой главе у нас было упражнение о потоках. Сделайте поток экземпляром класса Num. Для этогопоток должен содержать значения из класса Num. Методы из класса Num применяются поэлементно. Таксложение двух потоков будет выглядеть так:

(a1 :& a2 :& a3 :& ...) + (b1 :& b2 :& b3) ==== (a1 + b1 :& a2 + b2 :& a3 + b3 :& ...)

• Определите приоритет инфиксной операции (:&) так чтобы вам было удобно использовать её в соче-тании с арифметическими операциями.• Дополните модуль потоков новым типом. Этот тип будет содержать функции преобразования потоков.

data F a b = F (Stream a -> Stream b)

Определите для этого типа основные функции высшего порядка. Чтобы не возникало конфликта имёнс модулем Data.Function мы не будем его импортировать. Вместо него мы импортируем модульControl.Category. Он содержит класс:

class Category cat whereid :: cat a a(.) :: cat b c -> cat a b -> cat a c

Если присмотреться к типам функций, можно понять, что тип-экземпляр cat принимает два параметра.Совсем как тип функции (a -> b). Формально его можно записать в префиксной форме так (->) a b.Получается, что тип cat это что-то вроде функции. Это некоторые сущности, у которых есть понятиятождества и композиции.Для обычных функций экземпляр класса Category уже определён. Но в этом модуле у нас есть ещё инеобычные функции, функции на потоках. Функции id и (.) мы определим, сделав наш тип F экзем-пляром класса Category.Для остальных функций мы переопределим исходные функции:

const :: a -> F b a -- константаap :: F a b -> Stream a -> Stream b -- применениеflip :: F a (F b c) -> F b (F a c) -- перестановка

curry :: F (a,b) c -> F a (F b c) -- частичное применение

Потренируйтесь с тем, что у вас получилось в интерпретаторе на манер нашего функционального каль-кулятора. Попробуйте составлять сочетания выражений из этого модуля с функциями из упражненияпредыдущей главы.• Сделайте тип (Num b => F a b) экземпляром класса Num.

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

Page 74: Ru Haskell Book

Глава 6

Специальные функции

Мы научились комбинировать функции наиболее общего типа a -> b. В этой главе мы посмотрим на спе-циальные функции и способы их комбинирования. Под специальными функциями понимаются такие функ-ции, результат которых имеет некоторую структуру. Среди них функции, которые могут вычислить значениеили упасть, или функции, которые возвращают сразу несколько вариантов значений. Для составления такихфункций из простейших в Haskell предусмотрено несколько классов типов. Это функторы и монады. Их мыи рассмотрим в этой главе.

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

общего типа:

(.) :: (b -> c) -> (a -> b) -> (a -> c)f . g = \x -> f (g x)

Композиция двух функций f и g это такая функция, в которой мы сначала применяем g, а затем f. Для тогочтобы тип функции стал более наглядным, мы определим эту функцию немного по-другому. Мы поменяемаргументы местами.

(>>) :: (a -> b) -> (b -> c) -> (a -> c)f >> g = \x -> g (f x)

На рис. 6.1 эта функция изображена графически. Мы будем изображать функции кружками, а значения– стрелками. Значения словно текут от узла к узлу по стрелкам. Поскольку тип стрелки выходящей из fсовпадает с типом стрелки входящей в g мы можем соединить их и получить составную функцию (f>>g).

cf>>ga

bcgfa

cgbbfa

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

74 | Глава 6: Специальные функции

Page 75: Ru Haskell Book

Класс CategoryС помощью операции композиции можно обобщить понятие функции. Для этого существует класс

Category:class Category cat where

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

Функция cat это тип с двумя параметрами, в котором выделено специальное значение id, которое остав-ляет аргумент без изменений. Также мы можем составлять из простых функций сложные с помощью компо-зиции, если функции совпадают по типу. Здесь мы для наглядности также заменили метод (.) на (>>), носуть остаётся прежней. Для любого экземпляра класса должны выполняться свойства:f >> id == fid >> f == f

f >> (g >> h) == (f >> g) >> h

Первые два свойства говорят о том, что id является нейтральным элементом для (>>) слева и справа.Третье свойство говорит о том, что нам не важно в каком порядке проводить композицию. Можно проверить,что эти правила выполнены для функций.

Специальные функцииВсе специальные функции, которые мы рассмотрим в этой главе будут иметь один и тот же тип:

a -> m b

Смотрите вместо произвольного типа b функция возвращает m b. Единственное, что будет меняться отраздела к разделу это тип m. Добавив этот тип к результату, мы сузили область значений функции. Простымпримером таких функций могут быть функции, которые возвращают списки:a -> [b]

Если раньше наши функции могли возвращать произвольное значение b, то теперь мы знаем, что всерезультирующие значения таких функций будут списками.При этом для каждого такого m мы попытаемся построить свой замкнутый мир специальных функций a

-> m b. Он будет жить внутри вселенной всех произвольных функций типа a -> b. В этом нам поможетспециальный класс типов, который называется категорией Клейсли (эта конструкция носит имя математикаХенрика Клейсли).class Kleisli m where

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

Этот класс является классом Category в мире наших специальных функций. Если мы сотрём все буквы m,то мы получим обычные типы для тождества и композиции. В этом мире должны выполняться те же правила:f *> idK == fidK *> f == f

f *> (g *> h) == (f *> g) *> h

Взаимодействие с внешним миромС помощью класса Kleisli мы можем составлять из одних специальных функций другие. Но как мы

сможем комбинировать специальные функции с обычными?Поскольку слева у нашей специальной функции обычный общий тип, то с этой стороны мы можем вос-

пользоваться обычной функцией композиции >>. Но как быть при композиции справа? Нам нужна функциятипа:(a -> m b) -> (b -> c) -> (a -> m c)

Оказывается мы можем составить её из методов класса Kleisli. Мы назовём эту функцию композиции(+>).(+>) :: Kleisli m => (a -> m b) -> (b -> c) -> (a -> m c)f +> g = f *> (g >> idK)

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

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

Page 76: Ru Haskell Book

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

аргументы | результат

обычная >> обычная == обычнаяспециальная +> обычная == специальнаяспециальная *> специальная == специальная

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

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

Category:

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

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

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

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

module Kleisli where

import Prelude hiding (id, (>>))

class Category cat whereid :: cat a a(>>) :: cat a b -> cat b c -> cat a c

class Kleisli m whereidK :: a -> m a(*>) :: (a -> m b) -> (b -> m c) -> (a -> m c)

(+>) :: Kleisli m => (a -> m b) -> (b -> c) -> (a -> m c)f +> g = f *> (g >> idK)

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

instance Category (->) whereid = \x -> xf >> g = \x -> g (f x)

Мы не будем импортировать функцию id, а определим её в классе Category. Также в Prelude уже опре-делена функция (>>) мы спрячем её с помощью специальной директивы hiding для того, чтобы она нам немешалась. Далее мы будем дополнять этот модуль экземплярами класса Kleisli и примерами.

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

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

data Maybe a = Nothing | Just aderiving (Show, Eq, Ord)

76 | Глава 6: Специальные функции

Page 77: Ru Haskell Book

Nothing

bfa

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

она вернёт (Just a), а в случае ошибки будет возвращено значение Nothing. На рис. 6.2 изображена схемачастично определённой функции. Теперь мы можем определить нашу функцию так:pred :: Nat -> Maybe Natpred Zero = Nothingpred (Succ a) = Just a

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

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

разворачивать его каждый раз. Как будет выглядеть функция извлечения дважды предыдущего натуральногочисла:pred2 :: Nat -> Maybe Natpred2 x =

case pred x ofJust (Succ a) -> Just a_ -> Nothing

Если мы захотим определить pred3, мы заменим pred в case-выражении на pred2. Вроде не такое уж идлинное решение. Но всё же мы теряем все преимущества гибких функций, все преимущества бесточечногостиля. Нам бы хотелось написать так:pred2 :: Nat -> Maybe Natpred2 = pred >> pred

pred3 :: Nat -> Maybe Natpred3 = pred >> pred >> pred

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

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

ние графически (рис. 6.3). Сверху изображены две частично определённых функции. Если функция f вернулазначение, то оно подставляется в следующую частично определённую функцию. Если же первая функция несмогла вычислить результат и вернула Nothing, то считается что вся функция (f*>g) вернула Nothing.Теперь давайте закодируем это определение в Haskell. При этом мы воспользуемся нашим классом

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

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

Nothing -> NothingJust b -> g b

Смотрите, в case-выражении мы возвращаем Nothing, если функция f вернула Nothing, а если ей удалосьвычислить значение и она вернула (Just b) мы передаём это значение в следующую функцию, т.е. состав-ляем выражение (g b).Сохраним это определение в модуле Kleisli, а также определение для функции pred и загрузим модуль

в интерпретатор. Перед этим нам придётся добавить в список функций, которые мы не хотим импортироватьиз Prelude функцию pred, она также уже определена в Prelude. Для определения нашей функции нам по-требуется модуль Nat, который мы уже определили. Скопируем файл Nat.hs в ту же директорию, в которойсодержится файл Kleisli.hs и подключим этот модуль. Шапка модуля примет вид:

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

Page 78: Ru Haskell Book

Nothing

cf*>ga

b

Nothing

cgfa

Nothing

cgb

Nothing

bfa

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

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

Добавим определение экземпляра Kleisli для Maybe в модуль Kleisli а также определение функцииpred. Сохраним обновлённый модуль и загрузим в интерпретатор.

*Kleisli> :load Kleisli[1 of 2] Compiling Nat ( Nat.hs, interpreted )[2 of 2] Compiling Kleisli ( Kleisli.hs, interpreted )Ok, modules loaded: Kleisli, Nat.*Kleisli> let pred2 = pred *> pred*Kleisli> let pred3 = pred *> pred *> pred*Kleisli> let two = Succ (Succ Zero)*Kleisli>*Kleisli> pred twoJust (Succ Zero)*Kleisli> pred3 twoNothing

Обратите внимание на то, как легко определяются производные функции. Желаемое поведение для ча-стично определённых функций закодировано в функции (*>) теперь нам не нужно заворачивать значения иразворачивать их из типа Maybe.Приведём пример функции, которая составлена из частично определённой функции и обычной. Опреде-

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

*Kleisli> let beside = pred +> \a -> (a, a + 2)*Kleisli> beside ZeroNothing*Kleisli> beside twoJust (Succ Zero,Succ (Succ (Succ Zero)))*Kleisli> (pred *> beside) twoJust (Zero,Succ (Succ Zero))

В выражении

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

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

78 | Глава 6: Специальные функции

Page 79: Ru Haskell Book

Итак с помощью функций из класса Kleisli мы можем составлять частично определённые функции вбесточечном стиле. Обратите внимание на то, что все функции кроме pred были составлены в интерпрета-торе.Отметим, что в Prelude определена специальная функция maybe, которая похожа на функцию foldr для

списков, она заменяет в значении типа Maybe конструкторы на функции. Посмотрим на её определение:maybe :: b -> (a -> b) -> Maybe a -> bmaybe n f Nothing = nmaybe n f (Just x) = f x

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

idM = Justf *> g = f >> maybe Nothing g

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

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

bfa

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

Приведём пример. Системы Линденмайера (или L-системы) моделируют развитие живого организма.Считается, что организм состоит из последовательности букв (или клеток). В каждый момент времени однабуква заменяется на новую последовательность букв, согласно определённым правилам. Так организм живёти развивается. Приведём пример:

a→ abb→ a

aababaabaababaababa

У нас есть два правила размножения клеток-букв в организме. На каждом этапе мы во всём слове заме-няем букву a на слово ab и букву b на a. Начав с одной буквы a, мы за несколько шагов пришли к болеесложному слову.Опишем этот процесс в Haskell. Для этого определим правила развития организма в виде многозначной

функции:next :: Char -> Stringnext ’a’ = ”ab”next ’b’ = ”a”

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

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

многозначных функций. После применения первой функции f мы применяем функцию к каждому элементусписка, который был получен из f. Так у нас получится список списков. Но нам нужен список, для этогомы после применения g объединяем все значения в один большой список. Отметим, что функции f и g взависимости от значений могут возвращать разное число значений, поэтому на выходе у функций g разноечисло стрелок.Закодируем эту схему в Haskell:

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

Page 80: Ru Haskell Book

cf*>ga

b

b

b

cg

cg

cgfa

cgbbfa

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

idK = \a -> [a]f *> g = f >> map g >> concat

Функция тождества принимает одно значение и погружает его в список. В композиции мы сначала при-меняем f затем к каждому элементу списка результата применяем g, так у нас получается список списков.После чего мы сворачиваем его в один список с помощью функции concat.Вспомним тип функций map и concat:

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

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

generate :: Int -> (a -> [a]) -> (a -> [a])generate 0 f = fgenerate n f = f *> generate (n - 1) f

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

generate :: Int -> (a -> [a]) -> (a -> [a])generate n f = iterate (*> f) f !! n

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

iterate :: (a -> a) -> a -> [a]iterate f a = [a, f a, f (f a), f (f (f a)), ...]

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

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

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

*Kleisli> :reload[2 of 2] Compiling Kleisli ( Kleisli.hs, interpreted )Ok, modules loaded: Kleisli, Nat.*Kleisli> let gen n = generate n next ’a’*Kleisli> gen 0”ab”

80 | Глава 6: Специальные функции

Page 81: Ru Haskell Book

*Kleisli> gen 1”aba”*Kleisli> gen 2”abaab”*Kleisli> gen 3”abaababa”*Kleisli> gen 4”abaababaabaab”

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

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

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

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

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

($) :: (a -> b) -> a -> bf $ a = (const a >> f) ()

Зачем такое запутанное определение, вместо привычного (f a)? Оказывается точно таким же способоммы можем определить применение в нашем мире специальных функций a -> m b.Применение в этом мире происходит особенным образом. Необходимо помнить о том, что второй аргу-

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

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

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

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

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

infixr 0 +$, *$

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

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

*Kleisli> let three = Succ (Succ (Succ Zero))*Kleisli> pred *$ pred *$ idK threeJust (Succ Zero)*Kleisli> pred *$ pred *$ idK ZeroNothing

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

Page 82: Ru Haskell Book

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

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

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

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

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

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

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

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

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

Оказывается с помощью методов класса Kleisli мы можем определить такую функцию для любой обыч-ной функции, а не только для функции двух аргументов. Мы будем называть такие функции словом liftN,где N – число, указывающее на арность функции. Функция (liftN f) ”поднимает” (от англ. lift) обычнуюфункцию f в мир специальных функций.Функция lift1 у нас уже есть, это просто функция +$. Теперь давайте определим функцию lift2:

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

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

lift1 :: (a’ -> b’) -> m’ a’ -> m’ b’f :: (a -> b -> c)a :: m a

lift1 f a :: m (b -> c) -- m’ == m, a’ == a, b’ == b -> c

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

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

where g = lift1 f a

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

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

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

($$) :: Kleisli m => m (a -> b) -> m a -> m bmf $$ ma = ( +$ ma) *$ mf

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

82 | Глава 6: Специальные функции

Page 83: Ru Haskell Book

*Kleisli> :reload KleisliOk, modules loaded: Kleisli, Nat.*Kleisli> Just (+2) $$ Just 2Just 4*Kleisli> Nothing $$ Just 2Nothing*Kleisli> [(+1), (+2), (+3)] $$ [10,20,30][11,21,31,12,22,32,13,23,33]*Kleisli> [(+1), (+2), (+3)] $$ [][]

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

lift2 :: Kleisli m => (a -> b -> c) -> m a -> m b -> m clift2 f a b = f’ $$ b

where f’ = lift1 f a

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

lift2 :: Kleisli m => (a -> b -> c) -> m a -> m b -> m clift2 f a b = lift1 f a $$ b

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

*Kleisli> :reload[2 of 2] Compiling Kleisli ( Kleisli.hs, interpreted )Ok, modules loaded: Kleisli, Nat.*Kleisli> lift2 (+) (Just 2) (Just 2)Just 4*Kleisli> lift2 (+) (Just 2) NothingNothing

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

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

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

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

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

У нас опять появился тип m (c -> d) и к нему нам нужно применить значение m c, чтобы получить m d.Этим как раз и занимается функция $$. Итак итоговое определение примет вид:

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

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

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

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

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

Page 84: Ru Haskell Book

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

sequence :: Kleisli m => [m a] -> m [a]sequence = foldr (lift2 (:)) (idK [])

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

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

Во второй команде вся функция вернула Nothing потому что при объединении списка встретилось зна-чение Nothing, это равносильно тому, что мы объединяем в один список, значения полученные из функций,которые могут не вычислить результат. Поскольку значение одного из элементов не определено, весь списокне определён.Посмотрим как работает эта функция на списках:

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

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

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

mapK :: Kleisli m => (a -> m b) -> [a] -> m [b]mapK f = sequence . map f

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

на самом деле в арсенале стандартных средств Haskell такого класса нет. Класс Kleisli строит замкнутыймир специальных функций a -> m b. Его цель построить язык в языке и сделать программирование со спе-циальными функциями таким же удобным как и с обычными функциями. Мы пользовались классом Kleisliисключительно в целях облегчения понимания этого мира. Впрочем никто не мешает нам определить этоткласс и пользоваться им в наших программах.А пока посмотрим, что есть в Haskell и как это соотносится с тем, что мы уже увидели. С помощью класса

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

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

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

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

class Functor f wherefmap :: (a -> b) -> f a -> f b

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

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

Нам только нужно заменить m на f и зависимость от Kleisli на зависимость от Functor:Итак в Haskell у нас есть базовая операция fmap применения обычной функции к значению из мира спе-

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

84 | Глава 6: Специальные функции

Page 85: Ru Haskell Book

Аппликативные функторыПосмотрим на определение класса Applicative:

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

Если присмотреться к типам методов этого класса, то мы заметим, что это наши старые знакомые idK и$$. Если для данного типа f определён экземпляр класса Applicative, то из контекста следует, что для неготакже определён и экземпляр класса Functor.Значит у нас есть функции fmap (или lift1) и <*> (или $$). С их помощью мы можем составить функции

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

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

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

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

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

Присмотримся к типам методов этого класса:return :: a -> m a

Их типа видно, что это ни что иное как функция idK. В классе Monad у неё точно такой же смысл. Теперьфункция >>=, она читается как функция связывания (bind).(>>=) :: m a -> (a -> m b) -> m b

Так возможно совпадение не заметно, но давайте ”перевернём” эту функцию:(=<<) :: Monad m => (a -> m b) -> m a -> m b(=<<) = flip (>>=)

Поменяв аргументы местами, мы получили знакомуюфункцию *$. Итак функция связывания это функцияприменения специальной функции к специальному значению. У неё как раз такой смысл.В Prelude определены экземпляры класса Monad для типов Maybe и []. Они определены по такому же

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

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

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

Свойства класса Functorfmap id x == x -- тождествоfmap f . fmap g == fmap (f . g) -- композиция

Первое свойство говорит о том, что если мы применяем fmap к функции тождества, то мы должны сноваполучить функцию тождества, или по другому можно сказать, что применение функции тождества к специ-альному значению не изменяет это значение. Второе свойство говорит о том, что последовательное примене-ние к специальному значению двух обычных функций можно записать в виде применения композиции двухобычных функций к специальному значению.Если всё это звучит туманно, попробуем переписать эти свойства в терминах композиции:

mf +> id == mf(mf +> g) +> h == mf +> (g >> h)

Первое свойство говорит о том, что тождественная функция не изменяет значение при композиции. Вто-рое свойство указывает на ассоциативность композиции одной специальной функции mf и двух обычныхфункций g и h.

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

Page 86: Ru Haskell Book

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

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

fmap f x == liftA f x -- связь с Functor

liftA id x == x -- тождествоliftA3 (.) f g x == f <*> (g <*> x) -- композицияliftA f (pure x) == pure (f x) -- гомоморфизм

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

(.) :: (b -> c) -> (a -> b) -> (a -> c)f :: m (b -> c)g :: m (a -> b)x :: m a

liftA3 (.) f g x :: m c

g <*> x :: m bf (g <*> x) :: m c

Слева в свойстве стоит liftA3, а не liftA2, потому что мы сначала применяем композицию (.) к двумфункциям f и g, а затем применяем составную функцию к значению x.Последнее свойство говорит о том, что если мы возьмём обычную функцию и обычное значение и подни-

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

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

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

class Functor f => Applicative f where-- | Поднимаем значение в мир специальных значений.pure :: a -> f a

-- | Применение специального значения-функции.(<*>) :: f (a -> b) -> f a -> f b

-- | Константная функция. Отбрасываем первое значение.(*>) :: f a -> f b -> f b(*>) = liftA2 (const id)

-- | Константная функция, Отбрасываем второе значение.(<*) :: f a -> f b -> f a(<*) = liftA2 const

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

Prelude Control.Applicative> Just 2 *> Just 3Just 3Prelude Control.Applicative> Nothing *> Just 3NothingPrelude Control.Applicative> (const id) Nothing Just 3Just 3Prelude Control.Applicative> [1,2] <* [1,2,3][1,1,1,2,2,2]

86 | Глава 6: Специальные функции

Page 87: Ru Haskell Book

Значение игнорируется, но способ комбинирования специальных функций учитывается. Так во второмвыражении не смотря на то, что мы не учитываем конкретное значение Nothing, мы учитываем, что если одиниз аргументов частично определённой функции не определён, то не определено всё значение. Сравните срезультатом выполнения следующего выражения.По той же причине в последнем выражении мы получили три копии первого списка. Так произошло

потому, что второй список содержал три элемента. К каждому из элементов была применена функция constx, где x пробегает по элементам списка слева от (<*).Аналогичный метод есть и в классе Monad:

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

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

m >> k = m >>= const kfail s = error s

Функция >> в классе Monad, которую мы прятали из-за символа композиции, является аналогом постоян-ной функции в классе Monad. Она работает так же как и *>. Функция fail используется для служебных нуждHaskell при выводе ошибок. Поэтому мы её здесь не рассматриваем. Для определения экземпляра классаMonad достаточно определить методы return и >>=.

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

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

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

class Functor f wherefmap :: (a -> b) -> f a -> f b

class Pointed f wherepure :: a -> f a

class (Functor f, Pointed f) => Applicative f where(<*>) :: f (a -> b) -> f a -> f b

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

class Applicative f => Monad f where(>>=) :: f a -> (a -> f b) -> f b

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

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

мента:

class Functor f wherefmap :: (a -> b) -> f a -> f b

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

class Functor f => Applicative f wherepure :: a -> f a

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

Page 88: Ru Haskell Book

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

liftA :: Applicative f => (a -> b) -> f a -> f bliftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f cliftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d...

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

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

Функция return является функцией id в мире специальных функций, а функция >>= является функциейприменения ($), с обратным порядком следования аргументов. Вспомним также класс Kleisli, на примерекотором мы узнали много нового из жизни специальных функций:

class Kleisli m whereidK :: a -> m a(*>) :: (a -> m b) -> (b -> m c) -> (a -> m c)

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

Частично определённые функцииa -> Maybe bdata Maybe a = Nothing | Just a

Многозначные функцииa -> [b]data [a] = [] | a : [a]

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

Kleisli и Monad.

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

s

sb

fa

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

В Haskell нельзя изменять значения. Новые сложные значения описываются в терминах базовых значе-ний. Но как же тогда мы сможем описать функцию с состоянием? Функцию, которая принимает на входзначение, составляет результат на основе внутреннего состояния и значения аргумента и обновляет состоя-ние. Поскольку мы не можем изменять состояние единственное, что нам остаётся – это принимать значениесостояния на вход вместе с аргументом и возвращать обновлённое состояние на выходе. У нас получитсятакой тип:

a -> s -> (b, s)

Функция принимает одно значение типа a и состояние типа s, а возвращает пару, которая состоит изрезультата типа b и обновлённого состояния. Если мы введём синоним:

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

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

88 | Глава 6: Специальные функции

Page 89: Ru Haskell Book

a -> State s b

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

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

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

s

s

cf*>ga

b

ss

s

cgfa

s

s

cgb

s

sb

fa

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

Функция runState просто извлекает функцию из оболочки State.На рис. 6.6 изображена схема функции с состоянием. В сравнении с обычной функцией у такой функции

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

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

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

instance Kleisli (State s) where...

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

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

env

bfa

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

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

a -> env -> b

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

Page 90: Ru Haskell Book

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

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

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

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

a -> Reader env b

Определите для функций с окружением экземпляр класса Kleisli. У нас возникнет цепочка функций,каждая из которых будет нуждаться в значении окружения. Поскольку окружение общее для всех функциймы всем функциям передадим одно и то же значение (рис. 6.9).

env

cf*>ga

b

env

cgfa

env

cgb

env

bfa

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

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

Функции-накопители при вычислении за ширмой накапливают некоторое значение. Функция-накопительпохожа на функцию с состоянием но без стрелки, по которой состояние подаётся в функцию (рис. 6.10).Функция-накопитель имеет тип: a -> (b, msg)

Msg

bfa

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

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

data Writer msg b = Writer (b, msg)

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

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

90 | Глава 6: Специальные функции

Page 91: Ru Haskell Book

a -> Writer msg b

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

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

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

class Monoid a wheremempty :: amappend :: a -> a -> a

В этом классе определено пустое значение mempty и бинарная функция соединения двух значений в одно.Этот класс очень похож на класс Category и Kleisli. Там тоже было значение, которое ничего не делает иоперация составления нового значения из двух простейших значений. Даже свойства класса похожи:mempty ‘mappend‘ f = ff ‘mappend‘ mempty = f

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

msg

cf*>ga

MsgG

MsgF

b

MsgF ++ MsgG++

cgfa

msg

cgb

msg

bfa

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

Первые два свойства говорят о том, что значение mempty и вправду является пустым элементом отно-сительно операции mappend. А третье свойство говорит о том, что порядок при объединении элементов неважен.Посмотрим на определение экземпляра для списков:

instance Monoid [a] wheremempty = []mappend = (++)

Итак пустой элемент это пустой список, а объединение это операция конкатенации списков. Проверим винтерпретаторе:*Kleisli> :m Data.MonoidPrelude Data.Monoid> [1 .. 4] ‘mappend‘ [4, 3 .. 1][1,2,3,4,4,3,2,1]Prelude Data.Monoid> ”Hello” ‘mappend‘ ” World” ‘mappend‘ mempty”Hello World”

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

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

Page 92: Ru Haskell Book

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

Представьте, что у нас нет класса Kleisli, а есть лишь Functor, Applicative и Monad. Напишите экзем-пляры для этих классов для всех рассмотренных в этой главе специальных функций (в том числе и для Readerи Writer). Экземпляры Functor и Applicative могут быть определены через Monad. Но для тренировки опре-делите экземпляры полностью. Сначала Functor, затем Applicative и в последнюю очередь Monad.

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

деревья:

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

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

data Tree a = Node a [Tree a]

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

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

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

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

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

instance Kleisli m => Monad m whereinstance Monad m => Kelisli m where

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

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

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

92 | Глава 6: Специальные функции

Page 93: Ru Haskell Book

Глава 7

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

В этой главе мы закрепим на примерах то, что мы узнали о монадах и функторах. Напомню, что с по-мощью монад и функторов мы можем комбинировать специальные функции вида (a -> m b) с другимиспециальными функциями.У нас есть функции тождества и применения:

class Functor f wherefmap :: (a -> b) -> f a -> f b

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

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

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

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

-- Композиция(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)

-- Константные функции(*>) :: Applicative f => f a -> f b -> f b(<*) :: Applicative f => f a -> f b -> f a

-- Применение обычных функций к специальным значениям(<$>) :: Functor f => (a -> b) -> f a -> f b

liftA :: Applicative f => (a -> b) -> f a -> f bliftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f cliftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

-- Преобразование элементов списка специальной функциейmapM :: Monad m => (a -> m b) -> [a] -> m [b]

Нам понадобится модуль с определениями типов и экземпляров монад для всех типов, которые мы рас-смотрели в предыдущей главе. Экземпляры для [] и Maybe уже определены в Prelude, а типы State, Reader иWriter можно найти в библиотеке mtl. Пока мы не знаем как устанавливать библиотеки определим эти типыи экземпляры для Monad самостоятельно. Возможно вы уже определили их, выполняя одно из упражненийпредыдущей главы, если это так сейчас вы можете сверить ответы. Определим модуль Types:

module Types(State(..), Reader(..), Writer(..),runState, runWriter, runReader,module Control.Applicative,

| 93

Page 94: Ru Haskell Book

module Control.Monad,module Data.Monoid)

where

import Data.Monoidimport Control.Applicativeimport Control.Monad

--------------------------------------------------- Функции с состоянием---- a -> State s b

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

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

instance Monad (State s) wherereturn a = State $ \s -> (a, s)ma >>= mf = State $ \s0 ->

let (b, s1) = runState ma s0in runState (mf b) s1

----------------------------------------------------- Функции с окружением---- a -> Reader env b

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

runReader :: Reader env a -> env -> arunReader (Reader f) = f

instance Monad (Reader env) wherereturn a = Reader $ const ama >>= mf = Reader $ \env ->

let b = runReader ma envin runReader (mf b) env

----------------------------------------------------- Функции-накопители---- Monoid msg => a -> Writer msg b

data Writer msg a = Writer (a, msg)deriving (Show)

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

instance Monoid msg => Monad (Writer msg) wherereturn a = Writer (a, mempty)ma >>= mf = Writer (c, msgA ‘mappend‘ msgF)

where (b, msgA) = runWriter ma(c, msgF) = runWriter $ mf b

Я пропустил определения для экземпляров классов Functor и Applicative, их можно получить из экзем-пляра для класса Monad с помощью стандартных функций liftM, return и ap из модуля Control.Monad.Нам встретилась новая запись в экспорте модуля. Для удобства мы экспортируем модули

Control.Applicative, Control.Monad и Data.Monoid целиком. Для этого мы написали ключевое словоmodule перед экспортируемым модулем. Теперь если мы в каком-нибудь другом модуле импортируеммодуль Types нам станут доступными все функции из этих модулей.Мы определили экземпляры для Functor и Applicative с помощью производных функций класса Monad.

94 | Глава 7: Примеры из мира специальных функций

Page 95: Ru Haskell Book

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

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

nextRandom :: Double -> DoublenextRandom = snd . properFraction . (105.947 * )

Функция properFraction возвращает пару, которая состоит из целой части и остатка числа. Взяв второйэлемент пары с помощью snd, мы выделяем остаток. Функция nextRandom представляет собой генераторслучайных чисел, который принимает значение с предыдущего шага и строит по нему следующее значение.Построим тип для случайных чисел:

type Random a = State Double a

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

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

addRandom :: Double -> Random DoubleaddRandom x = fmap (+x) next

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

*Random> runState (addRandom 5) 0.5(5.5,0.9735000000000014)*Random> runState (addRandom 5) 0.7(5.7,0.16289999999999338)*Random> runState (mapM addRandom [1 .. 5]) 0.5([1.5,2.9735000000000014,3.139404500000154,4.769488561516319,5.5250046269694195],0.6226652135290891)

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

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

addRandom2 :: Double -> Double -> Random DoubleaddRandom2 a b = liftA2 add next next

where add a b = \x y -> diap a 1 x + diap b 1 ydiap c r = \x -> x * 2 * r - r + c

Функция diap перемещает интервал от 0 до 1 в интервал от c-r до c+r. Обратите внимание на то как мысначала составили обычную функцию add, которая перемещает значения из интервала от 0 до 1 в нужныйдиапазон и складывает. И только в самый последний момент мы применили к этой функции случайныезначения. Посмотрим как работает эта функция:

*Random> runState (addRandom2 0 10) 0.5(10.947000000000003,0.13940450000015403)*Random> runState (addRandom2 0 10) 0.7(9.725799999999987,0.2587662999992979)

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

*Random> let res = fmap sum $ zipWithM addRandom2 [1..3] [11 .. 13]*Random> runState res 0.5(43.060125804029965,0.969511377766409)*Random> runState res 0.7(39.86034841613788,0.26599261421101517)

Функция zipWithM является аналогом функции zipWith. Она устроена также как и функция mapM, сначалаприменяется обычная функция zipWith, а затем функция sequence.С помощью типа Random мы можем определить функцию подбрасывания монетки:

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

Page 96: Ru Haskell Book

data Coin = Heads | Tailsderiving (Show)

dropCoin :: Random CoindropCoin = fmap drop’ next

where drop’ x| x < 0.5 = Heads| otherwise = Tails

У монетки две стороны орёл (Heads) и решка (Tails). Поскольку шансы на выпадание той или инойстороны равны, мы для определения стороны разделяем интервал от 0 до 1 в равных пропорциях.Подбросим монетку пять раз:

*Random> let res = sequence $ replicate 5 dropCoin

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

*Random> runState res 0.4([Heads,Heads,Heads,Heads,Tails],0.5184926967068364)*Random> runState res 0.5([Tails,Tails,Heads,Tails,Tails],0.6226652135290891)

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

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

type FSM s = State s s

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

Функция fsm принимает функцию переходов состояний transition и возвращает функцию, которая при-нимает состояние и возвращает конечный автомат. В качестве значения конечный автомат FSM будет возвра-щать текущее состояние.С помощью конечных автоматов можно описывать различные устройства. Лентой событий будет ввод

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

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

type Speaker = (SpeakerState, Level)

data SpeakerState = Sleep | Workderiving (Show)

data Level = Level Intderiving (Show)

Тип колонок складывается из двух значений: состояния и уровня громкости. Колонки могут быть вы-ключенными (Sleep) или работать на определённой громкости (Work). Считаем, что максимальный уровеньгромкости составляет 10 единиц, а минимальный ноль единиц. Границы диапазона громкости описываютсятакими функциями:

quieter :: Level -> Levelquieter (Level n) = Level $ max 0 (n-1)

louder :: Level -> Levellouder (Level n) = Level $ min 10 (n+1)

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

96 | Глава 7: Примеры из мира специальных функций

Page 97: Ru Haskell Book

data User = Button | Quieter | Louderderiving (Show)

Пользователь может либо нажать на кнопку вкл/выкл или повернуть реле громкости влево, чтобы при-глушить звук (Quieter) или вправо, чтобы сделать погромче (Louder). Будем считать, что колонки всегдавключены в розетку.Составим функцию переходов:

speaker :: User -> FSM Speakerspeaker = fsm $ trans

where trans Button (Sleep, n) = (Work, n)trans Button (Work, n) = (Sleep, n)trans Louder (s, n) = (s, louder n)trans Quieter (s, n) = (s, quieter n)

Мы считаем, что при выключении колонок реле остаётся некотором положении, так что при следующемвключении они будут работать на той же громкости. Реле можно крутить и в состоянии Sleep. Посмотримна типичную сессию работы колонок:

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

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

*FSM> runState res (Sleep, Level 2)([(Sleep,Level 2),(Work,Level 2),(Work,Level 3),(Work,Level 2),(Work,Level 1)],(Sleep,Level 1))*FSM> runState res (Sleep, Level 0)([(Sleep,Level 0),(Work,Level 0),(Work,Level 1),(Work,Level 0),(Work,Level 0)],(Sleep,Level 0))

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

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

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

safeSpeaker :: User -> FSM SpeakersafeSpeaker = fsm $ trans

where trans Button (Sleep, _) = (Work, Level 0)trans Button (Work, _) = (Sleep, Level 0)trans Quieter (Work, n) = (Work, quieter n)trans Louder (Work, n) = (Work, louder n)trans _ (Sleep, n) = (Sleep, n)

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

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

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

*FSM> runState res (Sleep, Level 10)([(Sleep,Level 10),(Work,Level 0),(Work,Level 1),(Work,Level 0),(Sleep,Level 0)],(Sleep,Level 0))

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

Page 98: Ru Haskell Book

Первое значение в списке является стартовым состоянием, которое мы задали. После этого колонки вклю-чаются и мы видим, что уровень громкости переключился на ноль. Затем мы увеличиваем громкость, сбав-ляем её и выключаем. Попытка изменить громкость выключенных колонок не проходит. Это видно по по-следнему элементу списка и итоговому состоянию колонок, которое находится во втором элементе пары.Предположим, что колонки работают с самого начала, тогда первым действием мы выключаем их. По-

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

*FSM> runState res (Work, Level 10)([(Work,Level 10),(Sleep,Level 0),(Sleep,Level 0),(Sleep,Level 0),(Work,Level 0)],(Work,Level 1))

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

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

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

data Exp = Var String| Lit Int| Neg Exp| Add Exp Exp| Mul Exp Expderiving (Show, Eq)

У нас есть тип Exp, который может быть либо переменной Var с данным строчным именем, либо целочис-ленной константой Lit, либо одной из трёх операций: вычитанием (Neg), сложением (Add) или умножением(Mul).Такие типы называют абстрактными синтаксическими деревьями (abstract syntax tree, AST). Они содержат

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

instance Num Exp wherenegate = Neg(+) = Add(*) = Mul

fromInteger = Lit . fromInteger

abs = undefinedsignum = undefined

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

var :: String -> Expvar = Var

n :: Int -> Expn = var . show

Функция var составляет переменную с данным именем, а функция n составляет переменную, у которойимя является целым числом. Сохраним эти определения в модуле Exp. Теперь у нас всё готово для составле-ния выражений:

*Exp> n 1Var ”1”*Exp> n 1 + 2Add (Var ”1”) (Lit 2)*Exp> 3 * (n 1 + 2)Mul (Lit 3) (Add (Var ”1”) (Lit 2))*Exp> - n 2 * 3 * (n 1 + 2)Neg (Mul (Mul (Var ”2”) (Lit 3)) (Add (Var ”1”) (Lit 2)))

98 | Глава 7: Примеры из мира специальных функций

Page 99: Ru Haskell Book

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

eval :: Exp -> Inteval (Lit n) = neval (Neg n) = negate $ eval neval (Add a b) = eval a + eval beval (Mul a b) = eval a * eval beval (Var name) = ???

Как быть с конструктором Var? Нам нужно откуда-то узнать какое значение связано с переменной. Функ-ция eval должна также принимать набор значений для всех переменных, которые используются в выражении.Этот набор значений мы будем называть окружением.Обратите внимание на то, что в каждом составном конструкторе мы рекурсивно вызываем функцию eval,

мы словно обходим всё дерево выражения. Спускаемся вниз, до самых листьев в которых расположены либозначения (Lit), либо переменные (Var). Нам было бы удобно иметь возможность пользоваться окружениемиз любого узла дерева. В этом нам поможет тип Reader.Представим что у нас есть значение типа Env и функция, которая позволяет читать значения переменных

по имени:

value :: Env -> String -> Int

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

eval :: Exp -> Reader Env Inteval (Lit n) = pure neval (Neg n) = liftA negate $ eval neval (Add a b) = liftA2 (+) (eval a) (eval b)eval (Mul a b) = liftA2 (*) (eval a) (eval b)eval (Var name) = Reader $ \env -> value env name

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

eval :: Exp -> Reader Env Inteval (Lit n) = pure neval (Neg n) = negateA $ eval neval (Add a b) = eval a ‘addA‘ eval beval (Mul a b) = eval a ‘mulA‘ eval beval (Var name) = Reader $ \env -> value env name

addA = liftA2 (+)mulA = liftA2 (*)negateA = liftA negate

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

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

data Map k a = ..

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

-- Создаём значения типа Map -- создаёмempty :: Map k a -- пустой MapfromList :: Ord k => [(k, a)] -> Map k a -- по списку (ключ, значение)

-- Узнаём значение по ключу(!) :: Ord k => Map k a -> k -> a

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

Page 100: Ru Haskell Book

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

-- Добавляем элементыinsert :: Ord k => k -> a -> Map k a -> Map k a

-- Удаляем элементыdelete :: Ord k => k -> Map k a -> Map k a

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

*Exp> :m +Data.Map*Exp Data.Map> :m -ExpData.Map> let v = fromList [(1, ”Hello”), (2, ”Bye”)]Data.Map> v ! 1”Hello”Data.Map> v ! 3”*** Exception: Map.find: element not in the mapData.Map> lookup 3 vNothingData.Map> let v1 = insert 3 ”Yo” vData.Map> v1 ! 3”Yo”

Функция lookup является стабильным аналогом функции !. В том смысле, что она определена с помощьюMaybe. Она не приведёт к падению программы, если для данного ключа не найдётся значение.Теперь мы можем определить функцию value:

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

...

type Env = M.Map String Int

value :: Env -> String -> Intvalue env name = maybe errorMsg $ M.lookup env name

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

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

runExp :: Exp -> [(String, Int)] -> IntrunExp a env = runReader (eval a) $ M.fromList env

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

*Exp> let env a b = [(”1”, a), (”2”, b)]*Exp> let exp = 2 * (n 1 + n 2) - n 1*Exp> runExp exp (env 1 2)5*Exp> runExp exp (env 10 5)20

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

eval :: Env -> Exp -> Inteval env x = case x of

Lit n -> nNeg n -> negate $ eval’ nAdd a b -> eval’ a + eval’ bMul a b -> eval’ a + eval’ bVar name -> value env namewhere eval’ = eval env

100 | Глава 7: Примеры из мира специальных функций

Page 101: Ru Haskell Book

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

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

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

class Monoid a wheremempty :: amappend :: a -> a -> a

mconcat :: [a] -> amconcat = foldr mappend mempty

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

instance Num a => Monoid a wheremempty = 0mappend = (+)

И умножения:

instance Num a => Monoid a wheremempty = 1mappend = (*)

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

newtype Sum a = Sum { getSum :: a }newtype Prod a = Prod { getProd :: a }

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

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

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

newtype Sum a = Sum a

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

data Sum a = Sum a

Единственное отличие заключается в том, что в случае newtype вычислитель не видит разницы междуSum a и a. Её видит лишь компилятор. Это означает, что на разворачивание и заворачивание такого значенияв тип обёртку не тратится никаких усилий. Такие типы подходят для решения двух задач:

• Более точная проверка типов.Например у нас есть типы, которые описывают физические величины, все они являются числами, но уних также есть и размерности. Мы можем написать:

type Velocity = Doubletype Time = Doubletype Length = Double

velocity :: Length -> Time -> Velocityvelocity leng time = leng / time

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

Page 102: Ru Haskell Book

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

newtype Velocity = Velocity Doublenewtype Time = Time Doublenewtype Length = Length Double

velocity :: Length -> Time -> Velocityvelocity (Length leng) (Time time) = Velocity $ leng / time

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

• Определение нескольких экземпляров одного класса для одного типа. Этот случай мы как раз и рас-сматриваем для класса Monoid. Нам нужно сделать два экземпляра для одного и того же типа Num a=> a.Сделаем две обёртки!

newtype Sum a = Sum anewtype Prod a = Prod a

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

instance Num a => Monoid (Sum a) wheremempty = Sum 0mappend (Sum a) (Sum b) = Sum (a + b)

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

instance Num a => Monoid (Prod a) wheremempty = Prod 1mappend (Prod a) (Prod b) = Prod (a * b)

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

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

data Passport = Person {surname :: String, -- ФамилияgivenName :: String, -- Имяnationality :: String, -- НациональностьdateOfBirth :: Date, -- Дата рожденияsex :: Bool, -- ПолplaceOfBirth :: String, -- Место рожденияauthority :: String, -- Место выдачи документаdateOfIssue :: Date, -- Дата выдачиdateOfExpiry :: Date -- Дата окончания срока} deriving (Eq, Show) -- действия

data Date = Date {day :: Int,month :: Int,year :: Int

} deriving (Show, Eq)

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

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

hello :: Passport -> Stringhello p = ”Hello, ” ++ givenName p ++ ”!”

102 | Глава 7: Примеры из мира специальных функций

Page 103: Ru Haskell Book

Для чтения мы просто подставляем в имя поля данное значение. В этой функции мы приветствуемчеловека и обращаемся к нему по имени. Для того, чтобы узнать его имя мы подсмотрели в паспорт, вполе givenName.• Обновление полей. Для обновления полей мы пользуемся таким синтаксисом:value { fieldName1 = newValue1, fieldName2 = newValue2, ... }

Мы присваиваем в значении value полю с именем fieldName новое значение newFieldValue. К примерупродлим срок действия паспорта на десять лет:prolongate :: Passport -> Passportprolongate p = p{ dateOfExpiry = newDate }

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

Вернёмся к типам Sum и Prod:newtype Sum a = Sum { getSum :: a }newtype Prod a = Prod { getProd :: a }

Этой записью мы определили два типа-обёртки. У нас есть две функции, которые заворачивают обычноезначение, это Sum и Prod. С помощью записей мы тут же в определении типа определили функции которыеразворачивают значения, это getSum и getProd.Вспомним определение для типа State:

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

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

Было бы гораздо лучше определить его так:newtype State s a = State{ runState :: s -> (a, s) }

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

сует лишь значение накопителя, наша функция будет возвращать значение единичного типа ().countBiFuns :: Exp -> IntcountBiFuns = getSum . execWriter . countBiFuns’

countBiFuns’ :: Exp -> Writer (Sum Int) ()countBiFuns’ x = case x of

Add a b -> tell (Sum 1) *> bi a bMul a b -> tell (Sum 1) *> bi a bNeg a -> un a_ -> pure ()where bi a b = countBiFuns’ a *> countBiFuns’ b

un = countBiFuns’

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

execWriter :: Writer msg a -> msgexecWriter (Writer (a, msg)) = msg

Первая функция countBiFuns извлекает значение из типов Writer и Sum. А вторая функция countBiFuns’вычисляет значение.Мы определили две вспомогательные функции tell, которая записывает сообщение в накопитель и

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

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

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

Page 104: Ru Haskell Book

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

помощью типа All мы можем проверить выполняется ли некоторое свойство для всех значений. А с помощьютипа Any мы можем узнать, что существует хотя бы один элемент, для которых это свойство выполнено.Посмотрим на определение экземпляров класса Monoid для этих типов:

newtype All = All { getAll :: Bool }

instance Monoid All wheremempty = All TrueAll x ‘mappend‘ All y = All (x && y)

В типе All мы накапливаем значения с помощью логического ”и”. Нейтральным элементом является кон-структор True. Итоговое значение накопителя будет равно True только в том случае, если все накапливаемыесообщения были равны True.В типе Any всё наоборот:

instance Monoid Any wheremempty = Any FalseAny x ‘mappend‘ Any y = Any (x || y)

Посмотрим как работают эти типы. Составим функцию, которая проверяет отсутствие оператора минусв выражении:noNeg :: Exp -> BoolnoNeg = not . getAny . execWriter . anyNeg

anyNeg :: Exp -> Writer Any ()anyNeg x = case x of

Neg _ -> tell (Any True)Add a b -> bi a bMul a b -> bi a b_ -> pure ()where bi a b = anyNeg a *> anyNeg b

Функция anyNeg проверяет есть ли в выражении хотя бы один конструктор Neg. В функции noNeg мыизвлекаем результат и берём его отрицание, чтобы убедиться в том что в выражении не встретилось ниодного конструктора Neg.*Exp> noNeg (n 2 + n 1 + 2 + 3)True*Exp> noNeg (n 2 - n 1 + 2 + 3)False

Накопление списковЭкземпляр класса Monoid определён и для списков. Предположим у нас есть дерево, в каждом узле кото-

рого находятся числа, давайте соберём все числа больше 5, но меньше 10. Деревья мы возьмём из модуляData.Tree:data Tree a = Node {

rootLabel :: a, -- значение меткиsubForest :: Forest a -- ноль или несколько дочерних деревьев

}

type Forest a = [Tree a]

Интересный тип. Тип Tree определён через Forest, а Forest определён через Tree. По этому типу мывидим, что каждый узел содержит некоторое значение типа a, и список дочерних деревьев.Составим дерево:

*Exp> :m Data.TreePrelude Data.Tree> let t a = Node a []Prelude Data.Tree> let list a = Node a []Prelude Data.Tree> let bi v a b = Node v [a, b]Prelude Data.Tree> let un v a = Node v [a]Prelude Data.Tree>Prelude Data.Tree> let tree1 = bi 10 (un 2 $ un 6 $ list 7) (list 5)Prelude Data.Tree> let tree2 = bi 12 tree1 (bi 8 tree1 tree1)

104 | Глава 7: Примеры из мира специальных функций

Page 105: Ru Haskell Book

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

type Diap a = (a, a)

inDiap :: Ord a => Diap a -> Tree a -> [a]inDiap d = execWriter . inDiap’ d

inDiap’ :: Ord a => Diap a -> Tree a -> Writer [a] ()inDiap’ d (Node v xs) = pick d v *> mapM_ (inDiap’ d) xs

where pick (a, b) v| (a <= v) && (v <= b) = tell [v]| otherwise = pure ()

Как и раньше у нас две функции, одна выполняет вычисления, другая извлекает результат из Writer. Вфункции pick мы проверяем число на принадлежность интервалу, если это так мы добавляем число к резуль-тату, а если нет пропускаем его, добавляя нейтральный элемент (в функции pure). Обратите внимание на токак мы обрабатываем список дочерних поддервьев. Функция mapM_ является аналогом функции mapM, Она ис-пользуется, если результат функции не важен, а важны те действия, которые происходят при преобразованиисписка. В нашем случае это накопление результата. Посмотрим на определение этой функции:

mapM_ :: Monad m => (a -> m b) -> [a] -> m ()mapM_ f = sequence_ . map f

sequence_ :: Monad m => [m a] -> m ()sequence_ = foldr (>>) (return ())

Основное отличие состоит в функции sequence_. Раньше мы собирали значения в список, а теперь отбра-сываем их с помощью константной функции >>. В конце мы возвращаем значение единичного типа ().Теперь сохраним в модуле Tree определение функции и вспомогательные функции создания деревьев

un, bi, и list и посмотрим как наша функция работает:

*Tree> inDiap (4, 10) tree2[10,6,7,5,8,10,6,7,5,10,6,7,5]*Tree> inDiap (5, 8) tree2[6,7,5,8,6,7,5,6,7,5]*Tree> inDiap (0, 3) tree2[2,2,2]

7.5 Краткое содержаниеМы посмотрели на примерах как применяются типы State, Reader и Writer. Мы узнали два новых эле-

мента пострения типов:

• Типы-обёртки, которые определяются через ключевое слово newtype.

• Записи, они являются произведением типов с именованными полями.

Также мы узнали несколько полезных типов:

• Map – хранение значений по ключу (из модуля Data.Map).

• Tree – деревья (из модуля Data.Tree).

• Типы для накопления результата (из модуля Data.Monoid).

Отметим, что экземпляр класса Monad определён и для функций. Мы можем записать функцию двух ар-гументов (a -> b -> c) как (a -> (->) b c). Тогда тип (->) b будет типом с одним параметром, как разто, что нужно для класса Monad. По смыслу экземпляр класса Monad для функций совпадает с экземпляромтипа Reader. Первый аргумент стрелочного типа b играет роль окружения.

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

Page 106: Ru Haskell Book

7.6 Упражнения• Напишите с помощью типа Random функцию игры в кости, два игрока бросают по очереди кости (двакубика с шестью гранями, грани пронумерованы от 1 до 6). Они бросают кубики 10 раз выигрывает тот,у кого в сумме выпадет больше очков. Функция принимает начальное состояние и выводит результатигры: суммарные баллы игроков.• Напишите с помощью типа Random функцию, которая будет создавать случайные деревья заданнойглубины. Значение в узле является случайным числом от 0 до 100, также число дочерних деревьев вкаждом узле случайно, оно изменяется от 0 до 10.• Опишите в виде конечного автомата поведение амёбы. Амёба может двигаться на плоскости по четырёмнаправлениям. Если она чувствует свет в определённой стороне, то она ползёт туда. Если по-близостинет света, она ползает в произвольном направлении. Амёба улавливает интенсивность света, если повсем четырём сторонам интенсивность одинаковая, она стоит на месте и греется.• Казалось бы, зачем нам сохранять вычисления в выражениях, почему бы нам просто не вычислить ихсразу? Если у нас есть описание выражения мы можем применить различные техники оптимизации, ко-торые могут сокращать число вычислений. Например нам известно, что двойное отрицание не влияетна аргумент, мы можем выразить это так:instance Num Exp where

negate (Neg a) = anegate x = Neg x......

Так мы сократили вычисления на две операции. Возможны и более сложные техники оптимизации.Мы можем учесть ноль и единицу при сложении и умножении или дистрибутивность сложения отно-сительно умножения.В этом упражнении вам предлагается провести подобную оптимизацию для логических значений. Унас есть абстрактное синтаксическое дерево:data Log = True

| False| Not Log| Or Log Log| And Log Log

Напишите функцию, которая оптимизирует выражение Log. Эта функция приводит Log к конъюнктив-ной нормальной форме (КНФ). Дерево в КНФ обладает такими свойствами: все узлы с Or находятсяближе к корню чем узлы с And и все узлы с And находятся ближе к корню чем узлы с Not. В КНФ выра-жения имеют вид:(True ‘And‘ Not False ‘And‘ True) ‘Or‘ True ‘Or‘ (True ‘And‘ False)(True ‘And‘ True ‘And‘ False) ‘Or‘ True

Как бы мы не шли от корня к листу сначала нам будут встречаться только операции Or, затем толькооперации And, затем только Not.КНФ замечательна тем, что её вычисление может пройти досрочно. КНФ можно представить так:data Or’ a = Or’ [a]data And’ a = And’ [a]data Not’ a = Not’ adata Lit = True’ | False’

type CNF = Or’ (And’ (Not’ Lit))

Сначала идёт список выражений разделённых конструктором Or (вычислять весь список не нужно, намнужно найти первый элемент, который вернёт True). Затем идёт список выражений, разделённых And(опять же его не надо вычислять целиком, нам нужно найти первое выражение, которое вернёт False).В самом конце стоят отрицания.В нашем случае приведение к КНФ состоит из двух этапов:

– Сначала построим выражение, в котором все конструкторы Or и And стоят ближе к корню чемконструктор Not. Для этого необходимо воспользоваться такими правилами:

106 | Глава 7: Примеры из мира специальных функций

Page 107: Ru Haskell Book

-- удаление двойного отрицанияNot (Not a) ==> a

-- правила де МорганаNot (And a b) ==> Or (Not a) (Not b)Not (Or a b) ==> And (Not a) (Not b)

– Делаем так чтобы все конструкторы Or были бы ближе к корню чем конструкторы And. Для этогомы воспользуемся правилом дистрибутивности:And a (Or b c) ==> Or (And a b) (And a c)

При этом мы будем учитывать коммутативность And и Or:And a b == And b aOr a b == Or b a

• Когда вы закончите определение функции:

transform :: Log -> CNF

Напишите функцию, которая будет сравнивать вычисление исходного выражения напрямую и вычис-ление через КНФ. Эта функция будет принимать исходное значение типа Log и будет возвращать двачисла, число операций необходимых для вычисления выражения:

evalCount :: Log -> (Int, Int)evalCount a = (evalCountLog a, evalCountCNF a)

evalCountLog :: Log -> IntevalCountLog a = ...

evalCountCNF :: Log -> IntevalCountCNF a = ...

При написании этих функций воспользуйтесь функциями-накопителями.• В модуле Data.Monoid определён специальный тип с помощью которого можно накапливать функции.Только функции должны быть специального типа. Они должны принимать и возвращать значения од-ного типа. Такие функции называют эндоморфизмами.Посмотрим на их определение:

newtype Endo a = Endo { appEndo :: a -> a }

instance Monoid (Endo a) wheremempty = Endo idEndo f ‘mappend‘ Endo g = Endo (f . g)

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

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

Page 108: Ru Haskell Book

Глава 8

Ленивые вычисления

В этой главе мы поговорим о том как в Haskell устроено вычисление программ. В самом начале мы го-ворили о том, что процесса вычисления значений нет. В том смысле, что у нас нет новых значений, у насничего не меняется.Вкратце вспомним то, что мы уже знаем о вычислениях. Сначала мы с помощью типов определяем мно-

жество всех возможных значений. Значения – это деревья в узлах которых записаны конструкторы, которыемы определяем в типах. Так например мы можем определить тип:

data Nat = Zero | Succ Nat

Этим типом мы определяем множество допустимых значений. В данном случае это цепочки конструкто-ров Succ, которые заканчиваются конструктором Zero:

Zero, Succ Zero, Succ (Succ Zero), ...

Затем начинаем давать им новые имена, создавая константы (простые имена-синонимы)

zero = Zeroone = Succ zerotwo = Succ one

и функции (составные имена-синонимы):

foldNat :: a -> (a -> a) -> Nat -> afoldNat z s Zero = zfoldNat z s (Succ n) = s (foldNat z s n)

add a = foldNat a Succmul a = foldNat one (add a)

Затем мы передаём нашу программу на проверку компилятору. Мы просим у него проверить не создаёмли мы случайно какие-нибудь бессмысленные выражения. Бессмысленные потому, что они пытаются создатьзначение, которое не вписывается в наши типы. Например если мы где-нибудь попробуем составить выра-жение:

add Zero mul

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

add Zero two

Компилятор согласится. И передаст выражение вычислителю. И тут мы говорили, что вычислитель начи-нает проводить расшифровку нашего описания. Он подставляет на место синонимов их определения, правыечасти из уравнений. Этот процесс мы называли редукцией. Вычислитель видит два синонима и одно значение.С какого синонима начать? С add или two?

108 | Глава 8: Ленивые вычисления

Page 109: Ru Haskell Book

8.1 Стратегии вычисленийЭтот вопрос приводит нас к понятию стратегии вычислений. Поскольку вычисляем мы только константы,

то наше выражение также можно представить в виде дерева. Только теперь у нас в узлах записаны не толькоконструкторы, но и синонимы. Процесс редукции можно представить как процесс очистки такого дерева отсинонимов. Посмотрим на дерево нашего значения:Оказывается у нас есть две возможности очистки синонимов.

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

Cверху-внизначинаем с корня, самого внешнего синонима и заменяем его на определение (с помощью уравненияна правую часть от знака равно), если на верху снова окажется синоним, мы опять заменим его наопределение и так пока на верху не появится конструктор, тогда мы спустимся в дочерние деревья ибудем повторять эту процедуру пока не дойдём до листьев дерева.Посмотрим как каждая из стратегий будет редуцировать наше выражение. Начнём со стратегии от ли-

стьев к корню (снизу-вверх):

add Zero two-- видим два синонима add и two-- раскрываем two, ведь он находится ниже всех синонимов=> add Zero (Succ one)-- ниже появился ещё один синоним, раскроем и его=> add Zero (Succ (Succ zero))-- появился синоним zero раскроем его=> add Zero (Succ (Suсс Zero))-- все узлы ниже содержат конструкторы, поднимаемся вверх до синонима-- заменяем add на его правую часть=> foldNat Succ Zero (Succ (Succ Zero))-- самый нижний синоним foldNat, раскроем его-- сопоставление с образцом проходит во втором уравнении для foldNat=> Succ (foldNat Succ Zero (Succ Zero))-- снова раскрываем foldNat=> Succ (Succ (foldNat Zero Zero))-- снова раскрываем foldNat, но на этот раз нам подходит-- первое уравнение из определения foldNat=> Succ (Succ Zero)-- синонимов больше нет можно вернуть значение-- результат:

Succ (Succ Zero)

В этой стратегии для каждой функции мы сначала вычисляем до конца все аргументы, потом подставляемрасшифрованные значения в определение функции.Теперь посмотрим на вычисление от корня к листьям (сверху-вниз):

add Zero two-- видим два синонима add и two, начинаем с того, что ближе всех к корню=> foldNat Succ Zero two-- теперь выше всех foldNat, раскроем его

Но для того чтобы раскрыть foldNat нам нужно узнать какое уравнение выбрать для этого нам нужнопонять какой конструктор находится в корне у второго аргумента, если это Zero, то мы выберем первоеуравнение, а если это Succ, то второе:

-- в уравнении для foldNat видим декомпозицию по второму-- аргументу. Узнаем какой конструктор в корне у two=> foldNat Succ Zero (Succ one)-- Это Succ нам нужно второе уравнение:=> Succ (foldNat Succ Zero one)-- В корне м ыполучили конструктор, можем спуститься ниже.-- Там мы видим foldNat, для того чтобы раскрыть его нам-- снова нужно понять какой конструктор в корне у второго аргумента:=> Succ (foldNat Succ Zero (Succ zero))

Стратегии вычислений | 109

Page 110: Ru Haskell Book

-- Это опять Succ переходим ко второму уравнению для foldNat=> Succ (Succ (foldNat Succ Zero zero))-- Снова раскрываем второй аргумент у foldNat=> Succ (Succ (foldNat Succ Zero Zero))-- Ага это Zero, выбираем первое уравнение=> Succ (Succ Zero)-- Синонимов больше нет можно вернуть значение-- результат:

Succ (Succ Zero)

В этой стратегии мы всегда раскрываем самый верхний уровень выражения, можно представить как мывытягиваем конструкторы от корня по цепочке. У этих стратегий есть специальные имена:

• вычисление по значению (call by value), когда мы идём от листьев к корню.

• вычисление по имени (call by need), когда мы идём от корня к листьям.

Преимущества и недостатки стратегийВ чём преимущества, той и другой стратегии.

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

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

isZero :: Nat -> BoolisZero Zero = TrueisZero _ = False

Она проверяет является ли нулём данное число, теперь представим как будет вычисляться выражение, втой и другой стратегии:

isZero (add Zero two)

Первая стратегия сначала вычислит все аргументы у add потом расшифрует add и только в самом концедоберётся до isZero. На это уйдёт восемь шагов (семь на вычисление add Zero two). В то время как вто-рая стратегия начнёт с isZero. Для вычисления isZero ей потребуется узнать какой конструктор в корне увыражения add Zero two. Она узнает это за два шага. Итого три шага. Налицо экономия усилий.Почему вторая стратегия экономит память? Поскольку мы всегда вычисляем аргументы функции, мы

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

sum :: Int -> [Int] -> Intsum [] res = ressum (x:xs) res = sum xs (res + x)

Посмотрим на то как вычисляет первая стратегия, с учётом того что мы вычисляем значения при подста-новке:

sum [1,2,3,4] 0=> sum [2,3,4] (0 + 1)=> sum [2,3,4] 1=> sum [3,4] (1 + 2)=> sum [3,4] 3=> sum [4] (3+3)=> sum [4] 6=> sum [] (6+4)=> sum [] 10=> 10

Теперь посмотрим на вторую стратегию:

110 | Глава 8: Ленивые вычисления

Page 111: Ru Haskell Book

sum [1,2,3,4] 0=> sum [2,3,4] 0+1=> sum [3,4] (0+1)+2=> sum [4] ((0+1)+2)+3=> sum [] (((0+1)+2)+3)+4=> (((0+1)+2)+3)+4=> ((1+2)+3)+4=> (3+3)+4=> 6+4=> 10

А теперь представьте, что мы решили посчитать сумму чисел от 1 до миллиона. Сколько вычислений нампридётся накопить! В этом недостаток второй стратегии.Ещё одно преимущество первой стратегии – предсказуемость вычислений, мы можем легко понять как

происходит вычисление. Сначала вычисляются самые маленькие выражения, затем они подставляются в вы-ражения по-больше и так пока мы не очистим выражение от синонимов. Вычисление по второй стратегиинапоминает вытягивание конструкторов из мутной воды выражения спинингом, никогда не знаешь какоевыражение схватит крючок первым. Может первым и знаешь, но вот кто пойдёт вторым-третьим не так ясно.Но есть и ещё один недостаток, рассмотрим выражение:

(\x -> add x (add x x)) (add Zero two)

Первая стратегия сначала редуцирует выражение add Zero two в то время как вторая подставит этовыражение в функцию и утроит свою работу!Но у второй стратегии есть одно очень веское преимущество, она может вычислять больше выражений

чем вторая. Определим значение бесконечность:

infinity :: Natinfinity = Succ infinity

Это рекурсивное определение, если мы попытаемся его распечатать мы получим бесконечную последо-вательность Succ. Чем не бесконечность? Теперь посмотрим на выражение:

isZero infinity

Первая стратегия захлебнётся, вычисляя аргумент функции isZero, в то время как вторая найдёт решениеза два шага.Подведём итоги. Плюсы вычисления по значению:

• Эффективный расход памяти в том случае если все составляющие выражения участвуют в вычислении.

• Предсказуемость.

• Она не может дублировать вычисления, как стратегия вычисления по имени.

Плюсы вычисления по имени:

• Меньше вычислений в том случае, если при вычислении выражения участвует лишь часть составляю-щих.

• Большая выразительность. Мы можем вычислить больше значений.

Какую из них выбрать? В Haskell пошли по второму пути. Всё-таки преимущество выразительности языкаоказалось самым существенным. Но для того чтобы избежать недостатков стратегии вычисления по именионо было модифицировано. Давайте посмотрим как.

Вычисление по необходимостиВернёмся к выражению:

(\x -> add x (add x x)) (add Zero two)

Стратегии вычислений | 111

Page 112: Ru Haskell Book

Проблема дублирования вычислений была решена с помощью графов. Раньше мы проводили редукциюдеревьев. Мы смотрели на дерево и заменяли в некотором узле синоним на его определение, мы разворачи-вали деревья, из деревьев получали новые деревья, и так до итогового значения. В улучшенном алгоритмеиспользуются направленные ациклические графы. Направленные графы, это графы со стрелками, стрелкиведут от аргументов к функциям, в которые эти аргументы подставляются. Ацикличность говорит об отсут-ствии циклов. Нет такой замкнутой последовательности стрелок, по которой можно выйти из одной вершиныи через несколько вершин вернуться в неё же. С помощью графов мы выражаем тот факт, что один и тот жеаргумент функции может использоваться сразу в нескольких местах выражения.Смысл в том, что мы запоминаем куда ведут аргументы функции. Так из аргумента функции \x -> add

x (add x x)) в правую часть функции ведут три стрелки. При этом

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

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

слоган гласит:

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

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

Устранение общих подвыраженийРассмотрим выражение:

(\x y -> add x y) (add Zero two) (add Zero two)

Сколько раз будет вычислено выражение add Zero two? Правильный ответ: два раза. Кажется, что у насдва идентичных выражения, разве вычисление по необходимости не занимается такими случаями? Таки-ми случаями занимается стратегия оптимизации кода, которая называется устранение общих подвыражений(common subexpression elimination). Вычисление по необходимости устраняет удвоение вычислений в аргу-ментах функции. Так в эквивалентном по смыслу выражении:

(\x -> add x x) (add Zero two)

подвыражение add Zero two будет вычислено лишь однажды. Отметим, что это выражение является записьюлокальных переменных через лямбда-функцию, то же самое можно написать так:

... = let x = add Zero twoin add x x

или так:

... = add x xwhere x = add Zero two

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

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

функцию:

infinity = Succ infinity

Если мы нарисуем это выражение в виде графа, мы получим цикл. На самом деле такое выражение пе-реписывается через специальную функцию fix, эту функцию называют комбинатором неподвижной точки(fix-point combinator), она принимает функцию и бесконечно применяет её к самой себе:

112 | Глава 8: Ленивые вычисления

Page 113: Ru Haskell Book

infinity = fix Succ

В этом выражении нет циклов, но посмотрим на определение fix

fix :: (a -> a) -> afix f = let x = f x in x

Раньше мы говорили, что локальные переменные в let и where выражениях могут быть переписаны спомощью лямбда функций, но если мы попытаемся проделать это с функцией fix, то мы ни к чему не придём.Эта функция вычисляется особым образом, определено правило:

fix f => f (fix f)

Это правило следует из выражения для локальной переменной в определении fix:

x = f x=> x = f (f x)=> x = f (f x)

...

Когда мы хотим вычислить fix f, мы проводим промежуточную редукцию по этому правилу. Напримервычислим выражение с функцией fix:

isZero infinity-- раскроем isZero, но для этого нам нужно узнать, что-- находится в первом аргументе, раскроем для этого infinity=> isZero (fix Succ)-- нам встретился fix применим специальное правило=> isZero (Succ (fix Succ))-- мы видим, что в корне аргумента функции isZero стоит-- конструктор Succ, значит нам подходит второе уравнение:=> False-- синонимов больше нет, можно вернуть значение-- результат:

False

С помощью функции fix можно выразить любую рекурсивную функцию. Посмотрим как на примерефункции foldNat, у нас есть рекурсивное определение:

foldNat :: a -> (a -> a) -> Nat -> afoldNat z s Zero = zfoldNat z s (Succ n) = s (foldNat z s n)

Необходимо привести его к виду:

x = f x

Слева и справа мы видим повторяются выражения foldNat z s, обозначим их за x:

x :: Nat -> ax Zero = zx (Succ n) = s (x n)

Теперь перенесём первый аргумент в правую часть, сопоставление с образцом превратится в case-выражение:

x :: Nat -> ax = \nat -> case nat of

Zero -> zSucc n -> s (x n)

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

x :: Nat -> ax = (\t -> \nat -> case nat of

Zero -> zSucc n -> s (t n)) x

Стратегии вычислений | 113

Page 114: Ru Haskell Book

Смотрите мы обозначили вхождение x в выражении справа за t и создали лямбда-функцию с таким ар-гументом. Так мы вынесли x из выражения.Получилось, мы пришли к виду комбинатора неподвижной точки:

x :: Nat -> ax = f x

where f = \t -> \nat -> case nat ofZero -> zSucc n -> s (t n)

Приведём в более человеческий вид:

foldNat :: a -> (a -> a) -> (Nat -> a)foldNat z s = fix f

where f t = \nat -> case nat ofZero -> zSucc n -> s (t n)

Это выражение не содержит рекурсии. Функция f принимает на вход функцию типа Nat -> a и возвращаетфункцию такого же типа. Если мы сравним с типом функции fix как раз и получится, что мы превращаемфункцию (Nat -> a) -> (Nat -> a) в значение Nat -> a, как раз то что нам нужно!

8.2 Реализация ленивых вычислений в ghcОсновной компилятор для Haskell это ghc. Посмотрим как ленивые вычисления реализованы в нём. Нам

потребуется узнать несколько новых терминов.Если выражение не содержит синонимов, то оно находится в нормальной форме (normal form, NF), далее

НФ. Выражение, у которого полностью вычислен хотя бы один конструктор, находится в слабой заголовочнойнормальной форме (weak-head normal form, WHNF), далее СЗНФ. Если выражение ещё не вычислялось, егоназывают отложенным, в английской литературе используют загадочное слово thunk, для краткости мыбудем обозначать его решёткой #.Итак выражение может находится в трёх состояниях:

• НФ, полностью вычислено.• СЗНФ, мы знаем один или более конструкторов от корня.• # мы ещё не добрались до него, выражение содержит лишь описание значения.

Запустим интерпретатор, поставим флаг +s подсчёта статистики вычислений и посчитаем сумму от од-ного до миллиарда, (Ниже запись 1e9 обозначает единицу, которая умножена на число 109):

Prelude> :set +sPrelude> let x = sum [1 .. 1e9](0.00 secs, 526724 bytes)

Чудеса, такая скорость! Теперь прибавим к результату единицу и посмотрим на ответ:

Prelude> x + 1<interactive>: out of memory (requested 2097152 bytes)

Как же так, мы так быстро вычислили сумму миллиарда чисел, а теперь когда мы попытались прибавитьединицу нам не хватило памяти! В чём подвох?Всё дело в ленивых вычислениях. В первом выражении мы с помощью let определили новый синоним.

На самом деле ничего не вычислялось, интерпретатор сохраняет описание способа вычисление и говорит”хорошо я сделаю это когда-нибудь потом”. Это потом наступает, когда результат нам действительно нужен,когда мы хотим на него посмотреть. Когда мы набрали x + 1 и нажали Enter, вычислитель зачесался и началработать, но к сожалению выяснилось, что он отложил на потом слишком много дел.В терминологии ghc выражение x находится в состоянии #. После нажатия Enter мы просим провести

редукцию и вычислить НФ. Редукция проводится по необходимости. Единственное место, где такая необхо-димость возникает это слева от знака равно в уравнениях, когда мы проводим сопоставление с образцом,или в case-выражениях.

114 | Глава 8: Ленивые вычисления

Page 115: Ru Haskell Book

Вспомним как мы вычисляли в самом начале выражение add Zero two с помощью стратегии вычисленияпо имени (в данном случае вычисление по имени и по необходимости совпадают, потому что мы нигде недублируем вычисления). Каждый раз мы хотели вычислить самое верхнее выражение, и каждый раз прирасшифровке синонимов сталкивались с необходимостью узнать какое уравнение нам выбрать. Для этогонам нужно было узнать какой конструктор находится в корне одного из аргументов. Мы приводили аргументк СЗНФ но не более того, и далее проводили подстановку значений в аргумент.Проведём это вычисление ещё раз, но теперь в терминах ghc. Мы будем проводить его ”вслепую”. Мы

будем обозначать все синонимы символом #:

вычислим:add Zero two

=> #=> # Zero # -- приведём к НФ=> add Zero #=> foldNat Succ Zero # -- какое уравнение?=> foldNat Succ Zero two=> foldNat Succ Zero (Succ #) -- выбираем 2 уравнение=> Succ (foldNat Succ Zero #) -- какое уравнение?=> Succ (foldNat Succ Zero one)=> Succ (foldNat Succ Zero (Succ #)) -- выбираем 2 уравнение=> Succ (Succ (foldNat Succ Zero #)) -- какое уравнение?=> Succ (Succ (foldNat Succ Zero zero))=> Succ (Succ (foldNat Succ Zero Zero)) -- выбираем 1 уравнение=> Succ (Succ (foldNat Succ Zero Zero))=> Succ (Succ Zero) -- НФ

Каждый # содержит ссылку на способ вычисление или синоним. Видите, мы расшифровываем значенияаргументов лишь для того, чтобы узнать какое уравнение выбрать.Такая экономия усилий позволяет определять все возможные значения и затем выбирать из них лишь, те

что нужны. Вычислитель пропустит всё лишнее.

Ленивый поиск корней уравненияПриведём пример. Поиск корней уравнения с помощью метода неподвижной точки. У нас есть функция

f :: a -> a, и нам нужно найти решение уравнения:

f x = x

Можно начать с какого-нибудь стартового значения, и подставлять, подставлять, подставлять его в f дотех пор, пока значение не перестанет изменяться. Так мы найдём решение.

xх1 = f x0x2 = f x1x3 = f x2...до тех пор пока abs (x[N] - x[N-1]) <= eps

Первое наблюдение: функция принимает не произвольные значения, а те для которых имеет смысл опе-рации: минус, поиск абсолютного значения и сравнение на больще/меньше. Тип нашей функции:

f :: (Ord a, Num a) => a -> a

Ленивые вычисления позволяют нам отделить шаг генерации решений, от шага проверки сходимости.Сначала мы сделаем список всех подстановок функции f, а затем найдём в этом списке два соседних элементарасстояние между которыми достаточно мало. Итак первый шаг, генерируем всю последовательность:

xNs = iterate f x0

Мы воспользовались стандартной функцией iterate из Prelude. Теперь ищем два соседних числа:

converge :: (Ord a, Num a) => a -> [a] -> aconverge eps (a:b:xs)

| abs (a - b) <= eps = a| otherwise = converge eps (b:xs)

Реализация ленивых вычислений в ghc | 115

Page 116: Ru Haskell Book

Поскольку список бесконечный мы можем не проверять случаи для пустого списка. Итоговое решение:

roots :: (Ord a, Num a) => a -> a -> (a -> a) -> aroots eps x0 f = converge eps $ iterate f x0

За счёт ленивых вычислений функции converge и iterate работают синхронно. Функция converge запра-шивает новое значение и iterate передаёт его, но только одно! Найдём решение какого-нибудь уравнения.Запустим интерпретатор. Мы ленимся и не создаём новый модуль для такой ”большой” функции. Опреде-ляем её сразу в интерпретаторе.

Prelude> let converge eps (a:b:xs) = if abs (a-b)<=eps then a else converge eps (b:xs)Prelude> let roots eps x0 f = converge eps $ iterate f x0

Найдём корень уравнения:

x(x− 2) = 0

x2 − 2x = 0

1

2x2 = x

Prelude> roots 0.001 5 (\x -> x*x/2)

Метод завис, остаётся только нажать ctrl+c для остановки. На самом деле есть одно условие для сходи-мости метода. Метод сойдётся, если модуль производной функции f меньше единицы. Иначе есть возмож-ность, что мы будем бесконечно генерировать новые подстановки. Вычислим производную нашей функции:

d

dx

1

2x2 = x

Нам следует ожидать решения в интервале от минус единицы до единицы:

Prelude> roots 0.001 0.5 (\x -> x*x/2)3.0517578125e-5Prelude> (\x -> x*x/2) $ roots 0.001 0.5 (\x -> x*x/2)4.656612873077393e-10

Мы нашли решение, корень равен нулю. В этой записи Ne-5 означает N · 10−5

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

случаев в Haskell предусмотрена функция seq. Она имеет тип:

seq :: a -> b -> b

Функция seq, говорит вычислителю: сначала приведи мой первый аргумент к СЗНФ, а затем верни второй.Корень проблемы заключается в том, что у нас есть не только наши собственные типы, но и встроенные типы,такие как целые и действительные числа. Для чисел редукция выражений, означает вычисление реальногочисленного значения. Поскольку у нас нет конструкторов чисел, мы не можем вычислитель заставить ихредуцировать заранее.Для решения этой проблемы была придумана функция seq. Давайте определим функцию sum’, которая

будет сразу вычислять значение.

sum’ :: Num a => [a] -> asum’ = iter 0

where iter res [] = resiter res (a:as) = let res’ = res + a

in res’ ‘seq‘ iter res’ as

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

Strict> sum’ [1 .. 1e9]

И мы ждём, и ждём, и ждём.

116 | Глава 8: Ленивые вычисления

Page 117: Ru Haskell Book

Компиляция модулейИ мы ждём и ждём и ждём. Но переполнения памяти не происходит. Это хорошо. Но давайте прервём

вычисления. Нажмём ctrl+c. Функция sum’ вычисляется, но вычисляется очень медленно. Мы можем суще-ственно ускорить её, если скомпилируем модуль Strict. До сих пор мы всегда интерпретировали модули. Впроцессе компиляции функция оптимизируется специальным образом, она может стать на порядок быстрее.Для того чтобы скомпилировать модуль нужно переключиться в его текущую директорию и вызвать ком-

пилятор ghc с флагом --make:

ghc --make Strict

Появились два файла Strict.hi и Strict.o. Первый файл называется интерфейсным он описывает какиев модуле определения, а второй файл называется объектным. Он содержит скомпилированный код модуля.Теперь мы можем загрузить модуль Strict в интерпретатор и сравнить выполнение двух функций:

Strict> sum’ [1 .. 1e6]5.000005e11(0.00 secs, 89133484 bytes)Strict> sum [1 .. 1e6]5.000005e11(0.57 secs, 142563064 bytes)

Обратите внимание на прирост скорости. Умение понимать в каких случаях стоит ограничить лень оченьважно. И в программах на Haskell тоже.Также компилировать модули можно из интерпретатора. Для этого воспользуемся командой :!, она вы-

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

Strict> :! ghc --make Strict[1 of 1] Compiling Strict ( Strict.hs, Strict.o )

Отметим наличие специальной функции применения, которая просит перед применением привести ар-гумент к СЗНФ, эта функция определена в Prelude:

($!) :: (a -> b) -> a -> bf $! a = a ‘seq‘ f a

С этой функцией мы модем определить функцию sum так:

sum’ :: Num a => [a] -> asum’ = iter 0

where iter res [] = resiter res (a:as) = flip iter as $! res + a

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

product’:

product’ :: Num a => [a] -> aproduct’ = iter 1

where iter res [] = resiter res (a:as) = let res’ = res * a

in res’ ‘seq‘ iter res’ as

Смотрите функция sum изменилась лишь в двух местах. Это говорит о том, что пора задуматься о том,а нет ли такой общей функции, которая включает в себя и то и другое поведение. Такая функция есть иназывается она foldl’, вот её определение:

foldl’ :: (a -> b -> a) -> a -> [b] -> afoldl’ op init = iter init

where iter res [] = resiter res (a:as) = let res’ = res ‘op‘ a

in res’ ‘seq‘ iter res’ as

Мы вынесли в аргументы функции бинарную операцию и начальное значение. Всё остальное осталосьпрежним. Эта функция живёт в модуле Data.List. Теперь мы можем определить функции sum’ и prod’:

Реализация ленивых вычислений в ghc | 117

Page 118: Ru Haskell Book

sum’ = foldl’ (+) 0product’ = foldl’ (*) 1

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

foldl :: (a -> b -> a) -> a -> [b] -> afoldl op init = iter init

where iter res [] = resiter res (a:as) = iter (res ‘op‘ a) as

Такая функция называется функцией с хвостовой рекурсией (tail-recursive function). Рекурсия хвостоваятогда, когда рекурсивный вызов функции является последним действием, которое выполняется в функции.Посмотрите на второе уравнение функции iter. Мы вызываем функцию iter рекурсивно последним делом. Вязыках с вычислением по значению часто хвостовая рекурсия имеет преимущество за счёт экономии памяти(тот момент который мы обсуждали в самом начале). Но как видно из этого раздела в ленивых языках это нетак. Библиотечная функция sum будет накапливать выражения перед вычислением с риском исчерпать всюдоступную память, потому что она определена через foldl.

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

не приводится к нормальной форме. Мы лишь просим вычислитель узнать какой конструктор находится вкорне у данного выражения. Например в выражении isZero $! infinity знак $! ничем не отличается отпростого применения мы и так будем приводить аргумент infinity к СЗНФ, когда нам понадобится узнатькакое из уравнений для isZero выбрать, ведь в аргументе функции есть сопоставление с образцом.Рассмотрим пример. Определим такой тип данных:

data TheDouble = TheDouble Doublederiving (Show, Eq)

instance Num TheDouble where(+) = inTheDouble2 (+)(-) = inTheDouble2 (-)(*) = inTheDouble2 (*)

abs = inTheDouble1 abssignum = inTheDouble1 signumfromInteger = TheDouble . fromInteger

inTheDouble1 f (TheDouble a) = TheDouble $ f ainTheDouble2 op (TheDouble a) (TheDouble b) = TheDouble $ op a b

Теперь посчитаем сумму чисел с помощью нашей функции sum’ и сравним результат с обычной функциейsum и с вычислениями sum’ на просто Double. Сохраним тип TheDouble в модуле Strict, скомпилируеммодуль для скорости и загрузим в интерпретатор:

Prelude Strict> :! ghc --make Strict[1 of 1] Compiling Strict ( Strict.hs, Strict.o )Prelude Strict> :l StrictOk, modules loaded: Strict.(0.00 secs, 576936 bytes)

Теперь посмотрим на вычисления:

Prelude Strict> sum’ [1::Double .. 1e6]5.000005e11(0.05 secs, 88609416 bytes)Prelude Strict> sum [1::Double .. 1e6]5.000005e11(0.56 secs, 142566848 bytes)Prelude Strict> sum’ $ map TheDouble [1::Double .. 1e6]TheDouble 5.000005e11(0.52 secs, 237439212 bytes)

118 | Глава 8: Ленивые вычисления

Page 119: Ru Haskell Book

Смотрите после того как мы завернули все числа в TheDouble вычисления стали проходить с такой жескоростью, что и в случае обычной функции sum. Это происходит потому, что теперь численный тип сидит вобёртке, когда мы попробуем вычислить СЗНФ в функции seq, мы не узнаем ничего нового кроме конструк-тора обёртки:

вычислим СЗНФ:TheDouble 1 + TheDouble 2

=> inTheDouble2 (+) (TheDouble 1) (TheDouble 2)=> TheDouble $ (TheDouble 1) + (TheDouble 2)=> TheDouble ((TheDouble 1) + (TheDouble 2))-- мы узнали, что в корне сидит конструктор TheDouble,-- дальше нам идти не нужно, вернём значение:

TheDouble #

И теперь суммы по прежнему накапливаются, но в конструкторе TheDouble. Кстати на этом примереможно понять разницу между data и newtype, если мы определим тип TheDouble через newtype вычислениябудут происходить быстрее.Посмотрим на один типичный пример. Вычисление среднего для списка чисел. Среднее равно сумме

всех элементов списка, разделённой на длину списка. Для того чтобы вычислить значение за один проходмы будем одновременно вычислять и сумму элементов и значение длины. Также мы понимаем, что нам ненужно откладывать вычисления, воспользуемся функцией foldl’:

mean :: [Double] -> Doublemean = division . foldl’ count (0, 0)

where count (sum, leng) a = (sum+a, leng+1)division (sum, leng) = sum / fromIntegral leng

Проходим по списку, копим сумму в первом элементе пары и длину во втором. В самом конце делимпервый элемент на второй. Обратите внимание на функцию fromIntegral она преобразует значения из це-лых чисел, в какие-нибудь другие из класса Num. Сохраним это определение в модуле Strict скомпилируеммодуль и загрузим в интерпретатор, не забудьте импортировать модуль Data.List, он нужен для функцииfoldl’. Посмотрим, что у нас получилось:

Prelude Strict> mean [1 .. 1e7]5000000.5(49.65 secs, 2476557164 bytes)

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

Prelude Strict> sum’ [1 .. 1e7]5.0000005e13(0.50 secs, 881855740 bytes)

В 100 раз быстрее. Теперь представьте, что у нас 10 таких функций как mean они разбросаны по всемукоду и делают своё чёрное ленивое дело. Причина такого поведения кроется в том, что мы опять завернулизначение в другой тип, на этот раз в пару. Когда вычислитель дойдёт до seq, он остановится на выражении(#,#) вместо двух чисел. Он вновь будет накапливать отложенные вычисления, а не значения.Перепишем mean, теперь мы будем вычислять значения пары по отдельности и попросим вычислитель

привести к СЗНФ каждое из них перед вычислением итогового значения:

mean’ :: [Double] -> Doublemean’ = division . iter (0, 0)

where iter res [] = resiter (sum, leng) (a:as) =

let s = sum + al = leng + 1

in s ‘seq‘ l ‘seq‘ iter (s, l) as

division (sum, leng) = sum / fromIntegral leng

Такой вот монстр. Функция seq право ассоциативна поэтому скобки будут группироваться в нужномпорядке. В этом определении мы просим вычислитель привести к СЗНФ числа, а не пары чисел, как в прошлойверсии. Для чисел СЗНФ совпадает с НФ, и всё должно пройти гладко, но сохраним это определение ипроверим результат:

Реализация ленивых вычислений в ghc | 119

Page 120: Ru Haskell Book

Prelude Strict> :! ghc --make Strict[1 of 1] Compiling Strict ( Strict.hs, Strict.o )Prelude Strict> :load StrictOk, modules loaded: Strict.(0.00 secs, 0 bytes)Prelude Strict> mean’ [1 .. 1e7]5000000.5(0.65 secs, 1083157384 bytes)

Получилось! Скорость чуть хуже чем у sum’, но не в сто раз.

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

через заросли seq, а пройти через ворота, возможно прихватив телегу значений. Для этого существуют такназываемые взрывные образцы (bang patterns).

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

такую фразу:{-# LANGUAGE BangPatterns #-}

Эта запись активирует расширение языка с именем BangPatterns. Ядро языка Haskell фиксировано стан-дартом, но каждый разработчик компилятора может вносить свои дополнения. Они подключаются черездирективу LANGUAGE:{-# LANGUAGE

Расширение1,Расширение2,Расширение3 #-}

Мы заключаем директиву в специальные комментарии с решёткой, говорим LANGUAGE а затем через за-пятую перечисляем имена расширений, которые нам понадобятся. Расширения активны только в рамкахданного модуля. Например если мы импортируем функции из модуля, в котором включены расширения, тоэти расширения не распространяются дальше на другие модули.

Ещё один способ сказать seqПосмотрим на функцию, которая использует взрывные образцы:

iter (!sum, !leng) a = (step + a, leng + 1)

В декомпозиции пары перед переменными у нас появились восклицательные знаки. Они говорят вычис-лителю о том, чтобы он так уж и быть сделал ещё одно усилие и заглянул в корень значений переменных,которые были переданы в эту функцию.Вычислитель говорит ладно-ладно сделаю. А там числа! И получается, что они не накапливаются. С по-

мощью взрывных образцов мы можем переписать функцию mean’ через foldl’, а не выписывать её целиком:mean’’ :: [Double] -> Doublemean’’ = division . foldl’ iter (0, 0)

where iter (!sum, !leng) a = (sum + a, leng + 1)division (sum, leng) = sum / fromIntegral leng

Проверим в интерпретаторе*Strict> :! ghc --make Strict[1 of 1] Compiling Strict ( Strict.hs, Strict.o )*Strict> :l StrictOk, modules loaded: Strict.(0.00 secs, 581304 bytes)Prelude Strict> mean’’ [1 .. 1e7]5000000.5(0.78 secs, 1412862488 bytes)Prelude Strict> mean’ [1 .. 1e7]5000000.5(0.65 secs, 1082640204 bytes)

Функция работает чуть медленнее, чем исходная версия, но не сильно.

120 | Глава 8: Ленивые вычисления

Page 121: Ru Haskell Book

Кто взрывается?

Почему образцы называются взрывными? И кто взрывается? Давайте проведём несколько тестовых взры-вов в интерпретаторе:

Prelude Strict> undefined*** Exception: Prelude.undefinedPrelude Strict> error ”Here is the Bang”*** Exception: Here is the Bang

Взрыв это остановка программы с ошибкой во время вычислений. Программа прошла проверку типов, новнезапно останавливается в процессе вычислений, возможно с кратким сообщением. Подорвать программуможно с помощью встроенных функций undefined и error, также можно забыть рассмотреть какой-нибудьслучай при декомпозиции аргументов и программа подорвётся сама. Например так:

Prelude Strict> let bangList [] = True(0.00 secs, 527636 bytes)Prelude Strict> bangList [1,2,3]*** Exception: <interactive>:1:4-21: Non-exhaustive patterns in function bangListPrelude Strict> bangList []True(0.00 secs, 0 bytes)

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

Если в функции возникает необходимость привести взрывное значение к СЗНФ, то вся функциявозвращает взрывное значение.

Слово ”возникает” выделено, потому что такой возможности может и не произойти, благодаря ленивымвычислениям. Наша функция может быть заминирована в аргументах. Но ленивый вычислитель просто ни-когда не узнает о них. Возможно в процессе вычислений окажется, что эти аргументы не нужны. Посмотримна несколько примеров:

Prelude> const 1 undefined1Prelude> let maybeBang x = if sin x > 0 then x else undefinedPrelude> maybeBang 11.0Prelude> maybeBang 22.0Prelude> maybeBang 33.0Prelude> maybeBang 4*** Exception: Prelude.undefinedPrelude> maybeBang 5*** Exception: Prelude.undefined

В функции const второй аргумент не нужен для вычислений поэтому взрыва не произошло. ФункцияmaybeBang заминирована, но за счёт природной лени if-выражений взрыв происходит не всегда.Функции, которые гарантированно взрываются, если в них передать undefined, называют строгими

(strict). Различают строгость по аргументам. Например функция const строгая по первому аргументу, нонестрогая по второму. Функция tail строгая. Функция id строгая, потому что она возвращает undefined,если ей передать undefined.Есть тонкость связанная с частичным применением. Посмотрим на определение функции логического и:

(&&) :: Bool -> Bool -> Bool(&&) False _ = False(&&) True x = x

Эта функция вернёт нестрогую функцию из первого уравнения но строгую из второго. Это видно еслипереписать это определение так:

Реализация ленивых вычислений в ghc | 121

Page 122: Ru Haskell Book

(&&) :: Bool -> Bool -> Bool(&&) False = const False(&&) True = id

Итак кто взрывается мы выяснили. Теперь посмотрим почему взрывными называют образцы. Взрывнойобразец превращает нестрогую функцию в строгую. Например определим функцию:

constBang :: a -> b -> aconstBang a !b = a

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

Prelude Strict> constBang 1 undefined*** Exception: Prelude.undefined

Взрыв произошёл, функция стала строгой. Если приглядеться к этой функции, то можно заметить, чтоэто функция seq. Взрывным образцом мы заставляем вычислитель зайти в заминированный аргумент. Новычислитель зайдёт лишь на уровень ниже, обратите внимание на то, что это выражение не взорвётся:

Prelude Strict> constBang 1 (Just undefined)1

seq по умолчаниюС помощью взрывных образцов мы можем создавать типы данных, в которых приведение к СЗНФ прово-

дится всегда на уровень ниже чем обычно. Для этого в определении типа нужно поставить восклицательныйзнак перед тем подтипом, который будет приводиться к СЗНФ, если вычислитель дошёл до конструктора.Например изменим наш тип TheDouble так:

data TheDouble = TheDouble !Double

Появился восклицательный знак перед подтипом Double. Теперь, если вычислитель получит где-нибудьвыражение (TheDouble #) он обязательно раскроет и аргумент, но также только на один уровень! В данномслучае этого достаточно, проверим в интерпретаторе:

Prelude Strict> sum’ $ map TheDouble [1 .. 1e7]TheDouble 5.0000005e13(1.04 secs, 1683465592 bytes)

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

два типа:

data Strict a = Strict !adata Lazy a = Lazy a

Теперь поэкспериментируем с подрывом функции seq, мы будем передавать заминированное значениепервым аргументом, но с разными обёртками. Функция seq заставит вычислитель привести к СЗНФ первыйаргумент перед возвращением второго. Первой командой мы отключаем подсчёт статистики, чтобы она намне мешалась. Сначала проверяем подорвётся ли seq на обычном значении undefined.

Prelude Strict> :unset +sPrelude Strict> seq undefined ”Hello””*** Exception: Prelude.undefined

Как и ожидалось seq подорвался. Спрячем бомбу в ленивый конструктор:

Prelude Strict> seq (Lazy undefined) ”Hello””Hello”

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

122 | Глава 8: Ленивые вычисления

Page 123: Ru Haskell Book

Prelude Strict> seq (Strict undefined) ”Hello””*** Exception: Prelude.undefined

Взрыв! Восклицательный знак в определении конструктора привёл к тому, что после того как вычисли-тель получил (Strict #) ему пришлось на свою погибель заглянуть глубже. Но попробуем завернуть значениев строгом конструкторе в ленивый конструктор:Prelude Strict> seq (Strict $ Lazy undefined) ”Hello””Hello”

Получили значение. Напоследок убедимся в том, что цепочка строгих конструкторов приводит к взрыву:Prelude Strict> seq (Strict $ Strict $ Strict $ Strict undefined) ”Hello””*** Exception: Prelude.undefined

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

функции seq. Функцию seq мы можем применять, а можем и не применять. Но кажется, что в декомпозициимы не можем уйти от необходимости проведения хотя бы одной редукции. Оказывается можем, в Haskell дляэтого предусмотрены специальные ленивые образцы (lazy patterns). Они обозначаются знаком тильда:lazyHead :: [a] -> alazyHead ~(x:xs) = x

Перед скобками сопоставления с образцом пишется символ тильда. Этим мы говорим вычислителю: до-верься мне, здесь точно такой образец, можешь даже не проверять дальше. Он и правда дальше не пойдёт.Например если мы напишем такое определение:lazySafeHead :: [a] -> Maybe alazySafeHead ~(x:xs) = Just xlazySafeHead [] = Nothing

Если мы подставим в эту функцию пустой список мы получим ошибку времени выполнения, вычислительдоверился нам в первом уравнении, а мы его обманули. Сохраним в модуле Strict и проверим:Prelude Strict> :! ghc --make Strict[1 of 1] Compiling Strict ( Strict.hs, Strict.o )

Strict.hs:67:0:Warning: Pattern match(es) are overlapped

In the definition of ‘lazySafeHead’: lazySafeHead [] = ...Prelude Strict> :l StrictOk, modules loaded: Strict.Prelude Strict> lazySafeHead [1,2,3]Just 1Prelude Strict> lazySafeHead []Just *** Exception: Strict.hs:(67,0)-(68,29): Irrefutablepattern failed for pattern (x : xs)

При компиляции нам даже сообщили о том, что образцы в декомпозиции пересекаются. Но мы былиупрямы и напоролись на ошибку, если мы поменяем образцы местами, то всё пройдёт гладко:Prelude Strict> :! ghc --make Strict[1 of 1] Compiling Strict ( Strict.hs, Strict.o )Prelude Strict> :l StrictOk, modules loaded: Strict.Prelude Strict> lazySafeHead []Nothing

Отметим, что сопоставление с образцом в let и where выражениях является ленивым. Функцию lazyHeadмы могли бы написать и так:lazyHead a = x

where (x:xs) = a

lazyHead a =let (x:xs) = ain x

Реализация ленивых вычислений в ghc | 123

Page 124: Ru Haskell Book

Посмотрим как используются ленивые образцы при построении потоков, или бесконечных списков. Мыбудем представлять функции одного аргумента потоками значений с одинаковымшагом. Так мы будем пред-ставлять непрерывные функции дискретными сигналами. Считаем, что шаг дискретизации (или шаг междусоседними точками) нам известен.

f : R→ R ⇒ fn = f(nτ), n = 0, 1, 2, . . .

Где τ – шаг дискретизации, а n пробегает все натуральные числа. Определим функцию решения диффе-ренциальных уравнений вида:

dx

dt= f(t)

x(0) = x

Символ x означает начальное значение функции x. Перейдём к дискретным сигналам:

xn − xn−1

τ= fn, x0 = x

Где τ – шаг дискретизации, а x и f – это потоки чисел, индекс n пробегает от нуля до бесконечностипо всем точкам функции, превращённой в дискретный сигнал. Такой метод приближения дифференциаль-ных уравнений называют методом Эйлера. Теперь мы можем выразить следующий элемент сигнала черезпредыдущий.

xn = xn−1 + τfn, x0 = x

Закодируем это уравнение:

-- шаг дискретизацииdt :: Fractional a => adt = 1e-3

-- метод Эйлераint :: Fractional a => a -> [a] -> [a]int x0 (f:fs) = x0 : int (x0 + dt * f) fs

Смотрите в функции int мы принимаем начальное значение x0 и поток всех значений функции пра-вой части уравнения, поток значений функции f(t). Мы помещаем начальное значение в первый элементрезультата, а остальные значения получаем рекурсивно.Определим две вспомогательные функции:

time :: Fractional a => [a]time = [0, dt ..]

dist :: Fractional a => Int -> [a] -> [a] -> adist n a b = ( / fromIntegral n) $

foldl’ (+) 0 $ take n $ map abs $ zipWith (-) a b

Функция time пробегает все значения отсчётов шага дискретизации по времени. Это тождественная функ-ция представленная в виде потока с шагом dt.Функция проверки результата dist принимает два потока и по ним считает расстояние между ними. Эта

функция говорит, что расстояние между двумя потоками в n первых точках равно сумме модулей разностимежду значениями потоков. Для того чтобы оценить среднее расхождение, мы делим в конце результат начисло точек.Также импортируем для удобства символьный синоним для fmap из модуля Control.Applicative.

import Control.Applicative((<$>))...

Проверим функцию int. Для этого сохраним все новые функции в модуле Stream.hs. Загрузим модульв интерпретатор и вычислим производную какой-нибудь функции. Найдём решение для правой части кон-станты и проверим, что у нас получилась тождественная функция:

*Stream> dist 1000 time $ int 0 $ repeat 17.37188088351104e-17

124 | Глава 8: Ленивые вычисления

Page 125: Ru Haskell Book

Функции практически совпадают, порядок ошибки составляет 10−16. Так и должно быть для линейныхфункций. Посмотрим, что будет если в правой части уравнения стоит тождественная функция:*Stream> dist 1000 ((\t -> t^2/2) <$> time) $ int 0 time2.497500000001403e-4

Решение этого уравнения равно функции t2

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

экспонента будет решением такого уравнения:dx

dt= x

x(t) = x(0) +

∫ t

0

x(τ)dτ

Опишем это уравнение в Haskell:e = int 1 e

Наше описание копирует исходное математическое определение. Добавим это уравнение в модуль Streamи проверим результаты:*Stream> dist 1000 (map exp time) e^CInterrupted.

К сожалению вычисление зависло. Нажмём ctrl+c и разберёмся почему. Для этого распишем вычислениепотока чисел e:

e -- раскроем e=> int 1 e -- раскроем int, во втором варгументе

-- int стоит декомпозиция,=> int 1 e@(f:fs) -- для того чтобы узнать какое уравнение

-- для int выбрать нам нужно раскрыть-- второй аргумент, узнать корневой-- конструктор, раскроем второй аргумент:

=> int 1 (int 1 e)=> int 1 (int 1e@(f:fs)) -- такая же ситуация=> int 1 (int 1 (int 1 e))

Проблема в том, что первый элемент решения мы знаем, мы передаём его первым аргументом и присо-единяем к решению, но справа от знака равно. Но для того чтобы перейти в правую часть вычислителю нужнопроверить все аргументы, в которых есть декомпозиция. И он начинает проверять, но слишком рано. Намбы хотелось, чтобы он сначала присоединил к решению первый аргумент, а затем выполнял бы вычисленияследующего элемента.C помощью ленивых образцов мы можем отложить декомпозицию второго аргумента на потом:

int :: Fractional a => a -> [a] -> [a]int x0 ~(f:fs) = x0 : int (x0 + dt * f) fs

Теперь мы видим:*Stream> dist 1000 (map exp time) e4.988984990735441e-4

Вычисления происходят. С помощью взаимно-рекурсивных функций мы можем определить функции си-нус и косинус:sinx = int 0 cosxcosx = int 1 (negate <$> sinx)

Эти функции описывают точку, которая бегает по окружности. Вот математическое определение:

dx

dt= y

dy

dt= −x

x(0) = 0

y(0) = 1

Проверим в интерпретаторе:

Реализация ленивых вычислений в ghc | 125

Page 126: Ru Haskell Book

*Stream> dist 1000 (sin <$> time) sinx1.5027460329809257e-4*Stream> dist 1000 (cos <$> time) cosx1.9088156807382827e-4

Так с помощью ленивых образцов нам удалось попасть в правую часть уравнения для функции int, не рас-крывая до конца аргументы в левой части. С помощью этого мы могли ссылаться в сопоставлении с образцомна значение, которое ещё не было вычислено.

8.3 Краткое содержаниеВ этой главе мы узнали о том как происходят вычисления в Haskell. Мы узнали, что они ленивые. Всё

вычисляется как можно позже и как можно меньше. Такие вычисления называются вычислениями по необ-ходимости.Также мы узнали о вычислениях по значению и вычислениях по имени.• В вычислениях по значению редукция проводится от листьев дерева выражения к корню• В вычислениях по имени редукция проводится от корня дерева выражения к листьям.Вычисление по необходимости является улучшением вычисления по имени. Мы не дублируем выражения

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

форме. Это значит что оно вычислено. Может находится в слабой заголовочной нормальной форме. Это значит,что мы знаем хотя бы один конструктор в корне выражения. Также возможно выражение ещё не вычислялось,тогда оно является отложенным (thunk).Суть ленивых вычислений заключается в том, что они происходят синхронно. Если у нас есть композиция

двух функций:

g (f x)

Внутренняя функция f не начнёт вычисления до тех пор пока значения не понадобятся внешней функцииg. О последствиях этого мы остановимся подробнее в следующей главе. Значения могут потребоваться присопоставлении с образцом. Когда мы хотим узнать какое из уравнений нам выбрать.Иногда ленивые вычисления не эффективны по расходу памяти. Это происходит когда выражение состоит

из большого числа подвыражений, которые будут вычислены в любом случае. В Haskell у нас есть способыборьбы с ленью. Это функция seq и взрывные образцы.Функция seq:

seq :: a -> b -> b

Сначала приводит к слабой заголовочной форме свой первый аргумент, а затем возвращает второй.Взрывные образцы выполняют те же функции, но они используются в декомпозиции аргументов или в объ-явлении типа.Мы можем не только бороться с ленью, но и поощрять её. Лень поощряется ленивыми образцами. Они от-

меняют приведение к слабой заголовочной нормальной форме при декомпозиции аргументов. Они пишутсякак обычные образцы, но со знаком тильда:lazyHead ~(x:xs) = x

Мы говорим вычислителю: поверь мне, это значение может иметь только такой вид, потом посмотришьтак ли это, когда значения тебе понадобятся. Поэтому ленивые образцы проходят сопоставление с образцомв любом случае.Сопоставление с образцом в let и where выражениях является ленивым. Функцию lazyHead мы могли бы

написать и так:lazyHead a = x

where (x:xs) = a

lazyHead a =let (x:xs) = ain x

Также мы узнали как компилируются модули в ghc. Это делается с помощью команды:ghc --make ИмяМодуля

Модули можно компилировать и из интерпретатора, выполнив командуPrelude>:! ghc --make ИмяМодуля

126 | Глава 8: Ленивые вычисления

Page 127: Ru Haskell Book

8.4 Упражнения• Потренируйтесь в понимании того как происходят ленивые вычисления. Вычислите на бумаге следу-ющие выражения (если это возможно):

– sum $ take 3 $ filter (odd . fst) $zip [1 ..] [1, undefined, 2, undefined, 3, undefined, undefined]

– take 2 $ foldr (+) 0 $ map Succ $ repeat Zero

– take 2 $ foldl (+) 0 $ map Succ $ repeat Zero

– take 2 $ sinx (sinx из примера о потоках)• Перепишите с помощью fix несколько стандартных функций для списков. Например map, foldr, foldl,zip, repeat, cycle, iterate.Старайтесь найти наиболее краткое выражение, пользуйтесь функциями высшего порядка и частичнымприменением. Например рассмотрим функцию repeat:repeat :: a -> [a]repeat a = a : repeat a

Запишем с fix:repeat a = fix $ \xs -> a : xs

Заметим, что мы можем избавиться от аргумента xs с помощью сечения:repeat a = fix (a:)

Но мы можем пойти ещё дальше, если вспомним, что функция двух аргументов (:) является функциейот одного аргумента (:) :: a -> ([a] -> [a]), которая возвращает функцию одного аргумента:repeat = fix . (:)

Смотрите в этом выражении мы составили композицию двух функций. Функция (:) примет первыйаргумент и вернёт функцию, как раз то, что и нужно для fix.• Посмотрите на такую функцию вычисления суммы всех чётных и нечётных чисел в списке.sum2 :: [Int] -> (Int, Int)sum2 [] c = csum2 (x:xs) c = sum2 xs (tick x c)

tick :: Int -> (Int, Int) -> (Int, Int)tick x (c0, c1) | even x = (c0, c1 + 1)

| otherwise = (c0 + 1, c1)

Эта функция очень медленная. Кто-то слишком много ленится. Узнайте кто, и ускорьте функцию.• Можно ли ускорить функцию решения дифференциальных уравнений с помощью функции seq? Поэкс-периментируйте с этим примером.• На самом деле в функции int мы не решаем дифференциальные уравнения. Мы находим интегралфункции. Дифференциальные уравнения получаются когда мы начинаем использовать рекурсию вме-сте с этой функцией. В данном случае метод Эйлера совпадает с методом интегрирования с помощьюпрямоугольников. Это самый простой метод интегрирования.Попробуйте улучшить точность интегрирования с помощью метода трапеций и метода Симпсона.В методе трапеций интегрирование происходит по двум точкам. Соответственно наша функция int2будет принимать два начальных значения. Метод трапеций можно сформулировать так:

xn+1 − xnτ

=fn + fn−1

2

В методе Симпсона используются три точки:

xn+1 − xnτ

=fn + 4fn−1 + fn−2

6

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

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

Page 128: Ru Haskell Book

Глава 9

Ленивые чудеса

В прошлой главе мы узнали, что такое ленивые вычисления. В этой главе мы посмотрим чем они хо-роши. С ними можно делать невозможные вещи. Обращаться к ещё не вычисленным значениям, работать сбесконечными данными.Мы пишем программу, чтобы решить какую-нибудь сложную задачу. Часто так бывает, что сложная задача

оказывается сложной до тех пор пока её не удаётся разбить на отдельные независимые подзадачи. Мы решаемзадачи по-меньше, потом собираем из них решения, из этих решений собираем другие решения и вот ужеготова программа. Но мы решаем задачу не на листочке, нам необходимо объяснить её компьютеру. И тотязык, на котором мы пишем программу, оказывает сильное влияние на то как мы будем решать задачу. Мы неможем разбить программу на независимые подзадачи, если в том языке на котором мы собираемся объяснятьзадачу компьютеру нет средств для того, чтобы собрать эти решения вместе.Об этом говорит Джон Хьюз (John Huges) в статье ”Why functional programming matters”. Он приводит та-

кую метафору. Если мы делаем стул и у нас нет хорошего клея. Единственное что нам остаётся это вырезатьиз дерева стул целиком. Это невероятно трудная задача. Гораздо проще сделать отдельные части и потомсобрать вместе. Функциональные языки программирования предоставляют два новых вида ”клея”. Это функ-ции высшего порядка и ленивые вычисления. В статье можно найти много примеров. Некоторые из них мырассмотрим в этой главе.С функциями высших порядков мы уже знакомы, они позволяют склеивать небольшие решения. С их

помощью мы можем параметризовать функцию другой функцией (поведением). Они дают нам возможностьвыделять сложные закономерности и собирать их в функции. Ленивые вычисления же предназначены длясклеивания больших программ. Они синхронизируют выполнение подзадач, избавляя нас от необходимостивыполнять это вручную.Эта идея разбиения программы на независимые части приводит нас к понятию модульности. Когда мы

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

9.1 Численные методыРассмотрим несколько численных методов. Все эти методы построены на понятии сходимости. У нас есть

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

Так же как мы делали это в прошлой главе, когда искали корни уравнения методом неподвижной точки. Этипримеры взяты из статьи ”Why functional programming matters” Джона Хьюза.

ДифференцированиеНайдём производную функции в точке. Посмотрим на математическое определение производной:

f ′(x) = limh→0

f(x+ h)− f(x)

h

Производная это предел последовательности таких отношений, при h стремящемся к нулю. Если пределсходится, то производная определена. Для того чтобы решить эту задачу мы начнём с небольшого значе-ния h и будем постепенно уменьшать его, вычисляя промежуточные значения производной. Как только ониперестанут сильно изменяться мы будем считать, что мы нашли предел последовательностиЭтот процесс напоминает то, что мы делали при поиске корня уравнения методом неподвижной точки.

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

128 | Глава 9: Ленивые чудеса

Page 129: Ru Haskell Book

converge :: (Ord a, Num a) => a -> [a] -> aconverge eps (a:b:xs)

| abs (a - b) <= eps = a| otherwise = converge eps (b:xs)

Теперь осталось только создать последовательность значений производных. Напишем функцию, котораявычисляет промежуточные решения:easydiff :: Fractional a => (a -> a) -> a -> a -> aeasydiff f x h = (f (x + h) - f x) / h

Мы возьмём начальное значение шага и будем последовательно уменьшать его вдвое:halves = iterate (/2)

Соберём все части вместе:diff :: (Ord a, Fractional a) => a -> a -> (a -> a) -> a -> adiff h0 eps f x = converge eps $ map (easydiff f x) $ iterate (/2) h0

where easydiff f x h = (f (x + h) - f x) / h

Сохраним эти определения в отдельном модуле и найдём производную какой-нибудь функции. Проте-стируем решение на экспоненте. Известно, что производная экспоненты равна самой себе:*Numeric> let exp’ = diff 1 1e-5 exp*Numeric> let test x = abs $ exp x - exp’ x*Numeric> test 21.4093421286887065e-5*Numeric> test 51.767240203776055e-5

ИнтегрированиеТеперь давайте поинтегрируем функции одного аргумента. Интеграл это площадь кривой под графиком

функции. Если бы кривая была прямой, то мы могли бы вычислить интеграл по формуле трапеций:easyintegrate :: Fractional a => (a -> a) -> a -> a -> aeasyintegrate f a b = (f a + f b) * (b - a) / 2

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

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

integrate :: Fractional a => (a -> a) -> a -> a -> [a]integrate f a b = easyintegrate f a b :

zipWith (+) (integrate a mid) (integrate mid b)where mid = (a + b)/2

Первое решение является площадью под прямой, которая соединяет концы отрезка. Потом мы делим от-резок пополам, строим последовательность приближений и складываем частичные суммы с помощью функ-ции zipWith.Эта версия функции хоть и наглядная, но не эффективная. Функция f вычисляется заново при каждом ре-

курсивном вызове. Было бы хорошо вычислять её только для новых значений. Для этого мы будем передаватьзначения с предыдущего шага:integrate :: Fractional a => (a -> a) -> a -> a -> [a]integrate f a b = integ f a b (f a) (f b)

where integ f a b fa fb = (fa+fb)*(b-a)/2 :zipWith (+) (integ f a m fa fm)

(integ f m b fm fb)where m = (a + b)/2

fm = f m

Численные методы | 129

Page 130: Ru Haskell Book

В этой версии мы вычисляем значения в функции f лишь один раз для каждой точки. Запишем итоговоерешение:int :: (Ord a, Fractional a) => a -> (a -> a) -> a -> a -> aint eps f a b = converge eps $ integrate f a b

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

ex = 1 +

∫ x

0

etdt

Посмотрим, так ли это для нашего алгоритма:*Numeric> let exp’ = int 1e-5 exp 0*Numeric> let test x = abs $ exp x - 1 - exp’ x*Numeric> test 28.124102876649886e-6*Numeric> test 54.576306736225888e-6*Numeric> test 101.0683757864171639e-5

Алгоритм работает. В статье ещё рассмотрены методы повышения точности этих алгоритмов. Что инте-ресно для улучшения точности не надо менять существующий код. Функция принимает последовательностьпромежуточных решений и преобразует её.

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

(Douglas McIlroy) ”Power Series, Power Serious”. Степенной ряд представляет собой функцию, которая опре-деляется списком коэффициентов:

F (x) = f0 + f1x+ f2x2 + f3x

3 + f4x4 + ....

Степенной ряд содержит бесконечное число слагаемых. Для вычисления нам потребуются функции сло-жения и умножения. Ряд F (x) можно записать и по-другому:

F (x) = F0(x)= f0 + xF1(x)= f0 + x(f1 + xF2(x))

Это определение очень похоже на определение списка. Ряд есть коэффициент f0 и другой ряд F1(x)умноженный на x. Поэтому для представления рядов мы выберем конструкцию похожую на список:data Ps a = a :+: Ps a

deriving (Show, Eq)

Но в нашем случае списки бесконечны, поэтому у нас лишь один конструктор. Далее мы будем писатьпросто f + xF1, без скобок для аргумента.Определим вспомогательные функции для создания рядов:

p0 :: Num a => a -> Ps ap0 x = x :+: p0 0

ps :: Num a => [a] -> Ps aps [] = p0 0ps (a:as) = a :+: ps as

Обратите внимание на то, как мы дописываем бесконечный хвост нулей в конец ряда. Теперь давайтеопределим функцию вычисления ряда. Мы будем вычислять лишь конечное число степеней.eval :: Num a => Int -> Ps a -> a -> aeval 0 _ _ = 0eval n (a :+: p) x = a + x * eval (n-1) p x

В первом случае мы хотим вычислить ноль степеней ряда, поэтому мы возвращаем ноль, а во второмслучае значение ряда a+xP складывается из числа a и значения ряда P умноженного на заданное значение.

130 | Глава 9: Ленивые чудеса

Page 131: Ru Haskell Book

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

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

СложениеРекурсивное представление ряда f + xF позволяет нам очень кратко выражать операции, которые мы

хотим определить. Теперь у нас нет бесконечного набора коэффициентов, у нас всего лишь одно число и ещёодин ряд. Операции существенно упрощаются. Так сложение двух бесконечных рядов имеет вид:

F +G = (f + xF1) + (g + xG1) = (f + g) + x(F1 +G1)

Переведём на Haskell:(f :+: fs) + (g :+: gs) = (f + g) :+: (fs + gs)

УмножениеУмножим два ряда:

F ∗G = (f + xF1) ∗ (g + xG1) = fg + x(fG1 + F1 ∗G)

Переведём:(.*) :: Num a => a -> Ps a -> Ps ak .* (f :+: fs) = (k * f) :+: (k .* fs)

(f :+: fs) * (g :+: gs) = (f * g) :+: (f .* gs + fs * (g :+: gs))

Дополнительная операция (.*) выполняет умножение всех коэффициентов ряда на число.

Класс NumСоберём определения для методов класса Num вместе:

instance Num a => Num (Ps a) where(f :+: fs) + (g :+: gs) = (f + g) :+: (fs + gs)(f :+: fs) * (g :+: gs) = (f * g) :+: (f .* gs + fs * (g :+: gs))negate (f :+: fs) = negate f :+: negate fsfromInteger n = p0 (fromInteger n)

(.*) :: Num a => a -> Ps a -> Ps ak .* (f :+: fs) = (k * f) :+: (k .* fs)

Методы abs и signum не определены для рядов. Обратите внимание на то, как рекурсивное определениерядов приводит к рекурсивным определениям функций для рядов. Этот приём очень характерен для Haskell.Поскольку наш ряд это число и ещё один ряд за счёт рекурсии мыможем воспользоваться операцией, которуюмы определяем, на ”хвостовом” ряде.

ДелениеРезультат деления Q удовлетворяет соотношению:

F = Q ∗G

Переписав F , G и Q в нашем представлении, получим

f + xF1 = (q + xQ1) ∗G = qG+ xQ1 ∗G = q(g + xG1) + xQ1 ∗G= qg + x(qG1 +Q1 ∗G)

Следовательно

q = f/gQ1 = (F1 − qG1)/G

Если g = 0 деление имеет смысл только в том случае, если и f = 0. Переведём на Haskell:

Степенные ряды | 131

Page 132: Ru Haskell Book

class Fractional a => Fractional (Ps a) where(0 :+: fs) / (0 :+: gs) = fs / gs(f :+: fs) / (g :+: gs) = q :+: ((fs - q .* gs)/(g :+: gs))

where q = f/g

fromRational x = p0 (fromRational x)

Производная и интегралПроизводная одного члена ряда вычисляется так:

d

dxxn = nxn−1

Из этого выражения по свойствам производнойd

dx(f(x) + g(x)) =

d

dxf(x) +

d

dxg(x)

d

dx(k ∗ f(x)) = k ∗ d

dxf(x)

мы можем получить формулу для всего ряда:d

dxF (x) = f1 + 2f2x+ 3f3x

2 + 4f4x3 + . . .

Для реализации нам понадобится вспомогательная функция, которая будет обновлять значение допол-нительного множителя n в выражении nxn−1:diff :: Num a => Ps a -> Ps adiff (f :+: fs) = diff’ 1 fs

where diff’ n (g :+: gs) = (n * g) :+: (diff’ (n+1) gs)

Также мы можем вычислить и интеграл степенного ряда:int :: Fractional a => Ps a -> Ps aint (f :+: fs) = 0 :+: (int’ 1 fs)

where int’ n (g :+: gs) = (g / n) :+: (int’ (n+1) gs)

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

примеру уравнение для ex выглядит так:dy

dx= y

Проинтегрируем с начальным условием y(0) = 1:

y(x) = 1 +

∫ x

0

y(t)dt

Теперь переведём на Haskell:expx = 1 + int expx

Кажется невероятным, но это и есть определение экспоненты. Так же мы можем определить и функциидля синуса и косинуса:

ddx sinx = cosx, sin(0) = 0,ddx cosx = − sinx, cos(0) = 1

Что приводит нас к:sinx = int cosxcosx = 1 - int sinx

И это работает! Вычисление этих функций возможно за счёт того, что вне зависимости от аргументафункция int вернёт ряд, у которого первый коэффициент равен нулю. Это значение подхватывается и ис-пользуется на следующем шаге рекурсивных вычислений.Через синус и косинус мы можем определить тангенс:

tanx = sinx / cosx

132 | Глава 9: Ленивые чудеса

Page 133: Ru Haskell Book

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

мемоизацией (memoization). Она заключается в том, что мы запоминаем все значения, с которыми вызываласьфункция и, если с данным значением функция уже вычислялась, просто используем значение из памяти, аесли значение ещё не вычислялось, вычисляем его и сохраняем.В ленивых языках программирования для мемоизации функций часто используется такой приём. Мы со-

храняем все значения функции в некотором контейнере, а затем обращаемся к элементам. При этом значениясохраняются в контейнере и не перевычисляются. Это происходит за счёт ленивых вычислений. Что интерес-но вычисляются не все значения, а лишь те, которые нам действительно нужны, те которые мы извлекаем изконтейнера хотя бы один раз.Посмотрим на такой классический пример. Вычисление чисел Фибоначчи. Каждое последующее число

ряда Фибоначчи равно сумме двух предыдущих. Наивное определение выглядит так:

fib :: Int -> Intfib 0 = 0fib 1 = 1fib n = fib (n-1) + fib (n-2)

В этом определении число вычислений растёт экспоненциально. Для того чтобы вычислить fib n намнужно вычислить fib (n-1) и fib (n-2), для того чтобы вычислить каждое из них нам нужно вычислитьещё два числа, и так вычисления удваиваются на каждом шаге. Если мы вызовем в интерпретаторе fib 40,то вычислитель зависнет. Что интересно в этой функции вычисления пересекаются, они могут быть пере-использованы. Например для вычисления fib (n-1) и fib (n-2) нужно вычислить fib (n-2) (снова), fib(n-3), fib (n-3) (снова) и fib (n-4).Если мы сохраним все значения функции в списке, каждый вызов функции будет вычислен лишь один

раз:

fib’ :: Int -> Intfib’ n = fibs !! n

where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

Попробуем вычислить для 40:

*Fib> fib’ 40102334155*Fib> fib’ 4040700852629

Вычисления происходят мгновенно. Если задача состоит из множества подзадач, которые самоподобныи для вычисления последующих подзадач используются решения из предыдущих, стоит задуматься об ис-пользовании мемоизации. Такие задачи называются задачами динамического программирования. Вычислениечисел Фибоначчи яркий пример задачи динамического программирования.Рассмотрим такую задачу. Дана прямоугольная ”карта местности”, в каждой клетке целым числом ука-

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

• Из каждой клетки карты вода стекает не более чем в одном из четырёх возможных направлений (”се-вер”, ”юг”, ”запад”, ”восток”).• Если у клетки нет соседе с высотой меньше её собственной высоты, то эта клетка – водосток, и вода изнеё никуда дальше не течёт.• Иначе вода из текущей клетки стекает на соседнюю клетку с минимальной высотой.• Если таких соседей несколько, то вода стекает по первому из возможных направлений из списка ”насевер”, ”на запад”, ”на восток”, ”на юг”.

Все клетки из которых вода стекает в один и тот же водосток принадлежат к одному бассейну водосбо-ра. Необходимо отметить на карте все бассейны. Решение этой задачи встретилось мне в статье ДмитрияАстапова ”Рекурсия+мемоизация = динамическое программирование”. Здесь оно и приводится с незначи-тельными изменениями.Карта местности представлена в виде двумерного массива, в каждой клетке которого отмечена высота

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

Водосборы | 133

Page 134: Ru Haskell Book

1 2 3 4 5 6 a a a b b b7 8 9 2 4 5 a a b b b b3 5 3 3 6 7 -> c c d b b e6 4 5 5 3 1 f g d b e e2 2 4 5 3 7 f g g h h e

Для представления двумерного массива мы воспользуемся типом Array из стандартного модуляData.Array. Тип Array имеет два параметра:

darta Array i a

Первый указывает на индекс, а второй на содержание. Подразумевается, что этот тип является экземпля-ром класса Ix, который описывает целочисленные индексы:

class Ord a => Ix a whererange :: (a, a) -> [a]index :: (a, a) -> a -> IntinRange :: (a, a) -> a -> BoolrangeSize :: (a, a) -> Int

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

import Data.Array

type Coord = (Int, Int)type HeightMap = Array Coord Inttype SinkMap = Array Coord Coord

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

flow :: HeightMap -> SinkMap

Мы будем решать эту задачу рекурсивно. Представим, что мы знаем водостоки для всех точек кромеданной. Для каждой точки мы можем узнать в какую сторону из неё стекает вода. При этом водосток дляследующей точки такой же как и для текущей. Если же из данной точки вода никуда не течёт, то она самаявляется водостоком. Мы определим эту функцию через комбинатор неподвижной точки fix.:

flow :: HeightMap -> SinkMapflow arr = fix $ \result -> listArray (bounds arr) $

map (\x -> maybe x (result !) $ getSink arr x) $range $ bounds arr

getSink :: HeightMap -> Coord -> Maybe Coord

Мы ищем решение в виде неподвижной точки функции, которая принимает карту стоков и возвращаеткарту стоков. Функция getSink по данной точке на карте вычисляет соседнюю точку, в которую стекает вода.Эта функция частично определена, поскольку для водостоков нет такой соседней точки, в которую бы утекалавода. Функция listArray конструирует значение типа Array из списка значений. Первым аргументом онапринимает диапазон значений для индексов. Размеры массива совпадают с размерами карты высот, поэтомупервым аргументом мы передаём bounds arr.Теперь разберёмся с тем как заполняются значения в список. Сначала мы создаём список координат

исходной карты высот с помощью выражения:

range $ bounds arr

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

\x -> maybe x (result !) $ getSink arr x

134 | Глава 9: Ленивые чудеса

Page 135: Ru Haskell Book

Мы принимаем текущую координату и с помощью функции getSink находим соседнюю точку, в которуюубегает вода. Если такой точки нет, то в следующем выражении мы вернём исходную точку, поскольку в этомслучае она и будет водостоком, а если такая соседняя точка всё-таки есть мы спросим результат из будущего.Мы обратимся к результату (result !), посмотрим каким окажется водосток для соседней точки и вернёмэто значение. Поскольку за счёт ленивых вычислений значения результирующего массива вычисляются лишьодин раз, после того как мы найдём водосток для данной точки этим результатом смогут воспользоватьсявсе соседние точки. При этом порядок обращения к значениям из будущих вычислений не играет роли.Осталось только определить функцию поиска ближайшего стока и функцию разметки.

getSink :: HeightMap -> Coord -> Maybe CoordgetSink arr (x, y)

| null sinks = Nothing| otherwise = Just $ snd $ minimum $ map (\i -> (arr!i, i)) sinkswhere sinks = filter p [(x+1, y), (x-1, y), (x, y-1), (x, y+1)]

p i = inRange (bounds arr) i && arr ! i < arr ! (x, y)

В функции разметки мы воспользуемся ассоциативным массивом из модуля Data.Map. Функция nub измодуля Data.List убирает из списка повторяющиеся элементы. Затем мы составляем список пар из коорди-нат водостоков и меток и в самом конце размечаем исходный массив:

label :: SinkMap -> LabelMaplabel a = fmap (m M.! ) a

where m = M.fromList $ flip zip [’a’ .. ] $ nub $ elems a

9.4 Краткое содержаниеЛенивые вычисления повышают модульность программ. Мы можем в одной части программы создать все

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

9.5 УпражненияМы побывали на выставке ленивых программ. Присмотритесь ещё раз к решениям задач этой главы и

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

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

Page 136: Ru Haskell Book

Глава 10

Структурная рекурсия

Структурная рекурсия определяет способ построения и преобразования значений по виду типа (по со-ставу его конструкторов). Функции, которые преобразуют значения мы будем называть свёрткой (fold), афункции которые строят значения – развёрткой (unfold). Эта рекурсия встречается очень часто, мы уже поль-зовались ею и не раз, но в этой главе мы остановимся на ней поподробнее.

10.1 СвёрткаСвёртку значения можно представить как процесс, который заменяет в дереве значения все конструкторы

на подходящие по типу функции.

Логические значенияВспомним определение логических значений:

data Bool = True | False

У нас есть два конструктора-константы. Любое значение типа Bool может состоять либо из одного кон-структора True, либо из одного конструктора False. Функция свёртки в данном случае принимает две кон-станты одинакового типа a и возвращает функцию, которая превращает значение типа Bool в значениетипа a, заменяя конструкторы на переданные значения:

foldBool :: a -> a -> Bool -> afoldBool true false = \b -> case b of

True -> trueFalse -> false

Мы написали эту функцию в композиционном стиле для того, чтобы подчеркнуть, что функция преобра-зует значение типа Bool. Определим несколько знакомых функций через функцию свёртки, начнём с отри-цания:

not :: Bool -> Boolnot = foldNat False True

Мы поменяли конструкторы местами, если на вход поступит True, то мы вернём False и наоборот. Теперьпосмотрим на ”и” и ”или”:

(||), (&&) :: Bool -> Bool -> Bool

(||) = foldNat (const True) id(&&) = foldNat id (const False)

Определение функций ”и” и ”или” через свёртки подчёркивает, что они являются взаимно обратными.Смотрите, эти функции принимают значение типа Bool и возвращают функцию Bool -> Bool. Фактическифункция свёртки для Bool является if-выражением, только в этот раз мы пишем условие в конце.

136 | Глава 10: Структурная рекурсия

Page 137: Ru Haskell Book

Натуральные числаУ нас был тип для натуральных чисел Пеано:

data Nat = Zero | Succ Nat

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

data Nat whereZero :: NatSucc :: Nat -> Nat

Если мы заменим конструктор Zero на значение типа a, то конструктор Succ нам придётся заменять нафункцию типа a -> a, иначе мы не пройдём проверку типов. Представим, что Nat это класс:

data Nat a wherezero :: asucc :: a -> a

Из этого определения следует функция свёртки:

foldNat :: a -> (a -> a) -> (Nat -> a)foldNat zero succ = \n -> case n of

Zero -> zeroSucc m -> succ (foldNat zero succ m)

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

isZero :: Nat -> BoolisZero = foldNat True (const False)

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

isZero Zero=> True -- заменили конструктор Zero

isZero (Succ (Succ (Succ Zero)))=> const False (const False (const False True))

-- заменили и Zero и Succ=> False

Что интересно за счёт ленивых вычислений на самом деле во втором выражении произойдёт лишь одназамена. Мы не обходим всё дерево, нам это и не нужно, а смотрим лишь на первый конструктор, если тамSucc, то произойдёт замена на постоянную функцию, которая игнорирует свой второй аргумент и рекурсив-ного вызова функции свёртки не произойдёт, совсем как в исходном определении!

even, odd :: Nat -> Bool

even = foldNat True notodd = foldNat False not

Эти функции определяют чётность числа, сдесь мы пользуемся тем свойством, что not (not a) == a.Определим сложение и умножение:

add, mul :: Nat -> Nat -> Nat

add a = foldNat a Succmul a = foldNat Zero (add a)

Свёртка | 137

Page 138: Ru Haskell Book

MaybeВспомним определение типа для результата частично определённых функций:

data Maybe a = Nothing | Just a

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

data Maybe a b whereNothing :: bJust :: a -> b

Этот класс принимает два параметра, поскольку исходный тип Maybe принимает один. Теперь несложнодогадаться как будет выглядеть функция свёртки, мы просто получим стандартную функцию maybe. Дадимопределение экземпляра функтора и монады через свёртку:

instance Functor Maybe wherefmap f = maybe Nothing (Just . f)

instance Monad Maybe wherereturn = Justma >>= mf = maybe Nothing mf ma

СпискиФункция свёртки для списков это функция foldr. Выведем её из определения типа:

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

Представим, что это класс:

class [a] b wherecons :: a -> b -> bnil :: b

Теперь получить определение для foldr совсем просто:

foldr :: (a -> b -> b) -> b -> [a] -> bfoldr cons nil = \x -> case x of

a:as -> a ‘cons‘ foldr cons nil as[] -> nil

Мы обходим дерево значения, заменяя конструкторы методами нашего воображаемого класса. Опреде-лим несколько стандартных функций для списков через свёртку.Первый элемент списка:

head :: [a] -> ahead = foldr const (error ”empty list”)

Объединение списков:

(++) :: [a] -> [a] -> [a]a ++ b = foldr (:) b a

В этой функции мы реконструируем заново первый список но в самом конце заменяем пустой список вхвосте a на второй аргумент, так и получается объединение списков. Обратите внимание на эту особенность,скорость выполнения операции (++) зависит от длины первого списка. Поэтому между двумя выражениями

((a ++ b) ++ c) ++ da ++ (b ++ (c ++ d))

Нет разницы в итоговом результате, но есть огромная разница по скорости вычисления! Второй гораздобыстрее. Убедитесь в этом! Реализуем объединение списка списков в один список:

concat :: [[a]] -> [a]concat = foldr (++) []

138 | Глава 10: Структурная рекурсия

Page 139: Ru Haskell Book

Через свёртку можно реализовать и функцию преобразования списков:

map :: (a -> b) -> [a] -> [b]map f = foldr ((:) . f) []

Если смысл выражения ((:) . f) не совсем понятен, давайте распишем его типы:

af - b

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

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

filter :: (a -> Bool) -> [a] -> [a]filter p = foldr (\a as -> foldBool (a:as) as (p a)) []

Тут у нас целых две функции свёртки. Если значение предиката p истинно, то мы вернём все элементысписка, а если ложно отбросим первый элемент. Через foldr можно даже определить функцию с хвостовойрекурсией foldl. Но это не так просто. Всё же попробуем. Для этого вспомним определение:

foldl :: (a -> b -> a) -> a -> [b] -> afoldl f s [] = sfoldl f s (a:as) = foldl f (f s a) as

Нам нужно привести это определение к виду foldr, нам нужно выделить два метода воображаемогокласса списка cons и nil:

foldr :: (a -> b -> b) -> b -> [a] -> bfoldr cons nil = \x -> case x of

a:as -> a ‘cons‘ foldr cons nil as[] -> nil

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

foldl :: (a -> b -> a) -> [b] -> a -> afoldl f = \x -> case x of

[] -> \s -> sa:as -> \s -> foldl f as (f s a)

Мы поменяли местами порядок следования аргументов (второго и третьего). Выделим тождественнуюфункцию в первом уравнении case-выражения и функцию композиции во втором.

foldl :: (a -> b -> a) -> [b] -> a -> afoldl f = \x -> case x of

[] -> ida:as -> foldl f as . (flip f a)

Теперь выделим функции cons и nil:

foldl :: (a -> b -> a) -> [b] -> a -> afoldl f = \x -> case x of

[] -> nila:as -> a ‘cons‘ foldl f aswhere nil = id

cons = \a b -> b . flip f a= \a -> ( . flip f a)

Теперь запишем через foldr:

foldl :: (a -> b -> a) -> a -> [b] -> afoldl f s xs = foldr (\a -> ( . flip f a)) id xs s

Кажется мы ошиблись в аргументах, ведь foldr принимает три аргумента. Дело в том, что в функцииfoldr мы сворачиваем списки в функции, последний аргумент предназначен как раз для результирующейфункции. Отметим, что из определения можно исключить два последних аргумента с помощью функцииflip.

Свёртка | 139

Page 140: Ru Haskell Book

Вычислительные особенности foldl и foldrЕсли посмотреть на выражение, которое получается в результате вычисления foldr и foldl можно понять

почему они так называются.В левой свёртке foldl скобки группируются влево, поэтому на конце l (left):

foldl f s [a1, a2, a3, a4] =(((s ‘f‘ a1) ‘f‘ a2) ‘f‘ a3) ‘f‘ a4

В правой свёртке foldr скобки группируются вправо, поэтому на конце r (right):

foldr f s [a1, a2, a3, a4]a1 ‘f‘ (a2 ‘f‘ (a3 ‘f‘ (a4 ‘f‘ s)))

Кажется, что если функция f ассоциативна

(a ‘f‘ b) ‘f‘ c = a ‘f‘ (b ‘f‘ c)

то нет разницы какую свёртку применять. Разницы нет по смыслу, но может быть существенная разница вскорости вычисления. Рассмотрим функцию concat, ниже два определения:

concat = foldl (++) []concat = foldr (++) []

Какое выбрать? Результат и в том и в другом случае одинаковый (функция ++ ассоциативна). Стоит вы-брать вариант с правой свёрткой. В первом варианте скобки будут группироваться влево, это чудовищноскажется на производительности. Особенно если в конце небольшие списки:

Prelude> let concatl = foldl (++) []Prelude> let concatr = foldr (++) []Prelude> let x = [1 .. 1000000]Prelude> let xs = [x,x,x] ++ map return x

Последним выражением мы создали список списков, в котором три списка по миллиону элементов, а вконце миллион списков по одному элементу. Теперь попробуйте выполнить concatl и concatr на списке xs.Вы заметите разницу по скорости печати. Также для сравнения можно установить флаг: :set +s.Также интересной особенностью foldr является тот факт, что за счёт ленивых вычислений foldr не нужно

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

Prelude> foldr (&&) undefined $ True : True : repeat FalseFalse

За счёт ленивых вычислений мы отбросили оставшуюся (бесконечную) часть списка. По этим примерамможет показаться, что левая свёртка такая не нужна совсем, но не все операции ассоциативны. Иногда полез-но собирать результат в обратном порядке, например так в Prelude определена функция reverse, котораяпереворачивает список:

reverse :: [a] -> [a]reverse = foldl (flip (:)) []

ДеревьяМы можем определить свёртку и для деревьев. Вспомним тип:

data Tree a = Node a [Tree a]

Запишем в виде класса:

data Tree a b wherenode :: a -> [b] -> b

В этом случае есть одна тонкость. У нас два рекурсивных типа: само дерево и внутри него – список. Дляпреобразования списка мы воспользуемся функцией map:

140 | Глава 10: Структурная рекурсия

Page 141: Ru Haskell Book

foldTree :: (a -> [b] -> b) -> Tree a -> bfoldTree node = \x -> case x of

Node a as -> node a (map (foldTree node) as)

Найдём список всех меток:

labels :: Tree a -> [a]labels = foldTree $ \a bs -> a : concat bs

Мы объединяем все метки из поддеревьев в один список и присоединяем к нему метку из текущего узла.Сделаем дерево экземпляром класса Functor:

instance Functor Tree wherefmap f = foldTree (Node . f)

Очень похоже на map для списков. Вычислим глубину дерева:

depth :: Tree a -> Intdepth = foldTree $ \a bs 0 -> 1 + foldr max 0 bs

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

10.2 РазвёрткаС помощью развёртки мы постепенно извлекаем значение рекурсивного типа из значения какого-нибудь

другого типа. Этот процесс очень похож на процесс вычисления по имени. Сначала у нас есть отложен-ное вычисление # или thunk. Затем мы применяем к нему функцию редукции и у нас появляется корневойконструктор. А в аргументах конструктора снова сидят #. Мы применяем редукцию к ним. И так пока не”развернём” всё значение.

СпискиДля разворачивания списков в Data.List есть специальная функция unfoldr. Присмотримся сначала к

её типу:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a]

Функция развёртки принимает стартовый элемент, а возвращает значение типа пары от Maybe. ТипомMaybe мы кодируем конструкторы списка:

data [a] b where(:) :: a -> b -> b -- Maybe (a, b)[] :: b -- Nothing

Конструктор пустого списка не нуждается в аргументах, поэтому его мы кодируем константой Nothing.Объединение принимает два аргумента голову и хвост, поэтому Maybe содержит пару из головы и следующегоэлемента для разворачивания. Закодируем это определение:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a]unfoldr f = \b -> case (f b) of

Just (a, b’) -> a : unfoldr f b’Nothing -> []

Или мы можем записать это более кратко с помощью свёртки maybe:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a]unfoldr f = maybe [] (\(a, b) -> a : unfoldr f b)

Смотрите, перед нами коробочка (типа b) с подарком (типа a), мы разворачиваем, а там пара: подарок(типа a) и ещё одна коробочка. Тогда мы начинаем разворачивать следующую коробочку и так далее поцепочке, пока мы не развернём не обнаружим Nothing, это означает что подарки кончились.Типичный пример развёртки это функция iterate. У нас есть стартовое значение типа a и функция по-

лучения следующего элемента a -> a

Развёртка | 141

Page 142: Ru Haskell Book

iterate :: (a -> a) -> a -> [a]iterate f = unfoldr $ \s -> Just (s, f s)

Поскольку Nothing не используется цепочка подарков никогда не оборвётся. Если только нам не будетлень их разворачивать. Ещё один характерный пример это функция zip:

zip :: [a] -> [b] -> [(a, b)]zip = curry $ unfoldr $ \x -> case x of

([] , _) -> Nothing(_ ,[]) -> Nothing(a:as , b:bs) -> Just ((a, b), (as, bs))

Если один из списков обрывается, то прекращаем разворачивать. А если оба содержат голову и хвост, томы помещаем в голову списка пару голов, а в следующий элемент для разворачивания пару хвостов.

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

альтернативы. Например рассмотрим потоки:

data Stream a = a :& Stream a

Они такие же как и списки, только без конструктора пустого списка. Функция развёртки для потоковимеет вид:

unfoldStream :: (b -> (a, b)) -> b -> Stream aunfoldStream f = \b -> case f b of

(a, b’) -> a :& unfoldStream f b’

И нам не нужно пользоваться Maybe. Напишем функции генерации потоков:

iterate :: (a -> a) -> a -> Stream aiterate f = unfoldStream $ \a -> (a, f a)

repeat :: a -> Stream arepeat = unfoldStream $ \a -> (a, a)

zip :: Stream a -> Stream b -> Stream (a, b)zip = curry $ unfoldStream $ \(a :& as, b :& bs) -> ((a, b), (as, bs))

Натуральные числаЕсли присмотреться к натуральным числам, то можно заметить, что они очень похожи на списки. Списки

без элементов. Это отражается на функции развёртки. Для натуральных чисел мы будем возвращать не паруа просто слкедующий элемент для развёртки:

unfoldNat :: (a -> Maybe a) -> a -> NatunfoldNat f = maybe Zero (Succ . unfoldNat f)

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

fromInt :: Int -> NatfromInt = unfoldNat f

where f n| n == 0 = Nothing| n > 0 = Just (n-1)| otherwise = error ”negative number”

Обратите внимание на то, что в этом определении не участвуют конструкторы для Nat, хотя мы и строимзначение типа Nat. Конструкторы для Nat как и в случае списков кодируются типом Maybe. Развёртка ис-пользуется гораздо реже свёртки. Возможно это объясняется необходимостью кодирования типа результатанекоторым промежуточным типом. Определения теряют в наглядности. Смотрим на функцию, а там Maybeи не сразу понятно что мы строим: натуральные числа, списки или ещё что-то.

142 | Глава 10: Структурная рекурсия

Page 143: Ru Haskell Book

10.3 Краткое содержаниеВ этой главе мы познакомились с особым видом рекурсии. Мы познакомились со структурной рекурсией.

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

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

• Cвёрткой (fold) мы получаем значение некоторого произвольного типа из данного рекурсивного типа.При этом все конструкторы заменяются на функции, которые возвращают новый тип.• Развёрткой (unfold) мы получаем из произвольного типа значение данного рекурсивного типа. Мы слов-но разворачиваем его из значения, этот процесс очень похож на ленивые вычисления.

Мы узнал некоторые стандартные функции структурной рекурсии: cond или if-выражения, maybe, foldr,unfoldr.

10.4 Упражнения• Определите развёртку для деревьев из модуля Data.Tree.• Определите с помощью свёртки следующие функции:sum, prod :: Num a => [a] -> aor, and :: [Bool] -> Boollength :: [a] -> Intcycle

unzip :: [(a,b)] -> ([a],[b])unzip3 :: [(a,b,c)] -> ([a],[b],[c])

• Определите с помощью развёртки следующие функции:infinity :: Natmap :: (a -> b) -> [a] -> [b]iterateTree :: (a -> [a]) -> a -> Tree azipTree :: Tree a -> Tree b -> Tree (a, b)

• Поэкспериментируйте в интерпретаторе с только что определёнными функциями и теми функциями,что мы определяли в этой главе.• Рассмотрим ещё один стандартный тип. Он определён в Prelude. Это тип Either (дословно – один издвух). Этот тип принимает два параметра:data Either a b = Left a | Right b

Значение может быть либо значением типа a, либо значением типа b. Часто этот тип используют какMaybe с информацией об ошибке. Конструктор Left хранит сообщение об ошибке, а конструктор Rightзначение, если его удалось вычислить.Например мы можем сделать такие определения:headSafe :: [a] -> Either String aheadSafe [] = Left ”Empty list”headSafe (x:_) = Right x

divSafe :: Fractional a => a -> a -> Either String adivSafe a 0 = Left ”division by zero”divSafe a b = Right (a/b)

Для этого типа также определена функция свёртки она называется either. Не подглядывая в Prelude,определите её.

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

Page 144: Ru Haskell Book

• Список является частным случаем дерева. Список это дерево, в каждом узле которого, лишь одниндочерний узел. Деревья из модуля Data.Tree похожи на списки, но есть в них одно существенноеотличие. Они всегда содержат хотя бы один элемент. Пустой список не может быть представлен в видетакого дерева. Например это различие сказывается, еслим вы захотите определить функцию-аналогtakeWhile для деревьев.Определите деревья, которые не страдают от этого недостатка. Определите для них функции свёрт-ки/развёртки, а также функции, которые мы определили для стандартных деревьев. Определите функ-цию takeWhile (в рекурсивном виде и в виде развёртки) и сделайте их экземпляром класса Monad,похожий на экземпляр для списков.

144 | Глава 10: Структурная рекурсия

Page 145: Ru Haskell Book

Глава 11

IO

Пока мы не написали ещё ни одной программы, которой можно было бы пользоваться вне интерпретато-ра. Предполагается, что программа как-то взаимодействует с пользователем (ожидает ввода с клавиатуры)и изменяет состояние компьютера (выводит сообщения на экран, записывает данные в файлы). Но пока чтомы не знаем как взаимодействовать с окружающим миром.Самое время узнать! Сначала мы посмотрим какие проблемы связаны с реализацией взаимодействия с

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

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

В этом смысле у нас ничего не изменяется. По-другому это называется функциональной чистотой (referentialtransparency). Это свойство говорит о том, что мы свободно можем заменить в тексте программы любойсиноним на его определение и это никак не скажется на результате.Функция является чистой, если её выход зависит только от её входов. В любой момент выполнения про-

граммы для одних и тех же входов будет один и тот же выход. Это свойство очень ценно. Оно облегчаетпонимание поведения функции. Оно говорит о том, что функция может зависеть от других функций толь-ко явно. Если мы видим, что другая функция используется в данной функции, то она используется в этойфункции. У нас нет таинственных глобальных переменных в которые мы можем записывать данные из од-ной функции и читать их с помощью другой. Мы вообще не можем ничего записывать и ничего читать. Мыне можем изменять состояния, мы можем лишь давать новые имена или строить новые выражения из ужесуществующих.Но в этот статичный мир описаний не вписывается взаимодействие с пользователем. Предположим, что

мы хотим написать такую программу: мы набираем на клавиатуре имя файла, нажимаем Enter и программапоказывает на экране содержимое этого файла, затем мы набираем текст, нажимаем Enter и текст дописыва-ется в конец файла, файл сохраняется. Это описание предполагает упорядоченность действий. Мы не можемсначала сохранить текст, затем прочитать обновления. Тогда текст останется прежним.Ещё один пример. Предположим у нас есть функция getChar, которая читает букву с клавиатуры. И

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

let c = getCharin print $ c : c : []

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

print $ getChar : getChar : []

Это выражение уже говорит о том, что читать с клавиатуры необходимо дважды! А ведь мы сделали обыч-ное преобразование, заменили вхождения синонима на его определение, но смысл изменился. Взаимодей-ствие с пользователем нарушает чистоту функций, нечистые функции называются функциями с побочнымиэффектами.Как быть? Можно ли внести в мир описаний порядок выполнения, сохранив преимущества функциональ-

ной чистоты? Долгое время этот вопрос был очень трудным для чистых функциональных языков. Как можнопользоваться языком, который не позволяет сделать такие базовые вещи как ввод/вывод?

| 145

Page 146: Ru Haskell Book

11.2 Монада IOБыло найдено очень интересное решение. Из него и выросли монады. В Haskell есть один специальный тип

IO от английского input-output (ввод-вывод). Его экземпляр для класса Monad обрабатывается специальнымобразом.

IO cf>>ga

IO cgfa

IO cgbIO bfa

ПослеДо

Рис. 11.1: Композиция для монады IO

Посмотрим на рис. 11.1. Это рисунок для класса Kleisli. Композиция специальных функций типа a ->IO b вносит порядок вычисления. Считается, что сначала будет вычислена функция слева от композиции, азатем функция справа от композиции. Но это правило работает только для функций, которые действительновзаимодействуют с пользователем, например читают из файла или выводят что-нибудь на экран. Так мы мо-жем любую чистую функцию поднять в мир специальных функций с помощью класса Functor, но на порядкевыполнения чистых функций это правило не скажется.Теперь перейдём к классу Monad. Там композиция заменяется на применение или операция связывания:

ma >>= mf

Для типа IO эта запись говорит о том, что сначала будет выполнено выражение ma и результат будет под-ставлен в выражение mf и только затем будет выполнено mf. Оператор связывания для специальных функцийвида:

a -> IO b

раскалывает наш статический мир на ”до” и ”после”. При этом у типа IO есть такая особенность. Нетфункции с типом:

IO a -> a

Однажды попав в сети IO, мы не можем из них выбраться. Но это не так страшно. Тип IO дробит нашстатический мир на кадры. Но мы спокойно можем создавать статические чистые функции и поднимать ихв мир IO лишь там где это действительно нужно.Рассмотрим такой пример, программа читает с клавиатуры начальное значение, затем загружает файл

настроек. Потом запускается, какая-то сложная функция и в самом конце мы выводим результат на экран.Схематично мы можем записать эту программу так:

program = liftA2 algorithm readInit (readConfig ”file”) >>= print

-- функции с побочными эффектами

146 | Глава 11: IO

Page 147: Ru Haskell Book

readInit :: IO IntreadConfig :: String -> IO Configprint :: Show a => a -> IO ()

-- большая и сложная, но !чистая! функцияalgorithm :: Int -> Config -> Result

Функция readInit читает начальное значение, функция readConfig читает из файла наcтройки, функ-ция print выводит значение на экран, если это значение можно преобразовать в строку. Функция algorithmэто большая функция, которая вычисляет какие-то данные. Фактически наше программа это и есть функцияalgorithm. В этой схеме мы добавили взаимодействие с пользователем лишь в одном месте, вся функцияalgorithm построена по правилам мира описаний. Так мы внесли порядок выполнения в программу, сохра-нив возможность определения чистых функций.Если у нас будет ещё один ”кадр”, ещё одно действие, например как только функция algorithm закончила

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

program =liftA2 algorithm2 readInit

(liftA2 algorithm1 readInit (readConfig ”file”))>>= print

-- функции с побочными эффектамиreadInit :: IO IntreadConfig :: String -> IO Configprint :: Show a => a -> IO ()

-- большие и сложные, но !чистые! функцииalgorithm1 :: Int -> Config -> Result1algorithm2 :: Int -> Result1 -> Result2

Теперь у нас два кадра, программа выполняется в два этапа. Каждый из них разделён участками взаимо-действия с пользователем. Но тип IO присутствует лишь в первых шести строчках, остальные два миллионастрок написаны в мире описаний, исключительно чистыми функциями, которые поднимаются в мир специ-альных функций с помощью функций liftA2 и стыкуются с помощью операции связывания >>=.Попробуем тип IO в интерпретаторе. Мы будем пользоваться двумя стандартными функциями getChar и

print

-- читает символ с клавиатурыgetChar :: IO Char

-- выводит значение на экранprint :: IO ()

Функция print возвращает значение единичного типа, завёрнутое в тип IO, поскольку нас интересует несамо значение а побочный эффект, который выполняет эта функция, в данном случае это вывод на экран.Закодируем два примера из первого раздела. В первом мы читаем один символ и печатаем его дважды:

Prelude> :m Control.ApplicativePrelude Control.Applicative> let res = (\c -> c:c:[]) <$> getChar >>= printPrelude Control.Applicative> resq”qq”

Мы сначала вызываем функцию getChar удваиваем результат функцией \c -> c:c:[] и затем выводимна экран.Во втором примере мы дважды запрашиваем символ с клавиатуры а затем печатаем их:

Prelude Control.Applicative> let res = liftA2 (\a b -> a:b:[]) getChar getChar >>= printPrelude Control.Applicative> resqw”qw”

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

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

Как пишутся программы | 147

Page 148: Ru Haskell Book

main :: IO ()

Если модуль называется Main или в нём нет директивы module ... where и в модуле есть функция main:: IO (), то после компиляции будет сделан исполняемый файл. Его можно запускать независимо от ghci.Просто нажимаем дважды мышкой или вызываем из командной строки.Напишем программу Hello world. Единственное, что она делает это выводит на экран приветствие:

main :: IO ()main = print ”Hello World!”

Теперь сохраним эти строчки в файле Hello.hs, перейдём в директорию файла и скомпилируем файл:ghc --make Hello

Появились объектный и интерфейсный файлы, а также появился третий бинарный файл. Это либо Helloбез расширения (в Linux) или Hello.exe (в Windows). Запустим этот файл:$ ./Hello”Hello World!”

Получилось! Это наша первая программа. Теперь напишем программу, которая принимает три символас клавиатуры и выводит их в обратном порядке:import Control.Applicative

f :: Char -> Char -> Char -> Stringf a b c = reverse $ [a,b,c]

main :: IO ()main = print =<< f <$> getChar <*> getChar <*> getChar

Сохраним в файле ReverseIO.hs и скомпилируем:ghc --make ReverseIO -o rev3

Дополнительным флагом -o мы попросили компилятор чтобы он сохранил исполняемый файл под име-нем rev3. Теперь запустим в командной строке:$ ./rev3qwe”ewq”

Набираем три символа и нажимаем ввод. И программа переворачивает ответ. Обратите внимание на то,что с помощью print мы выводим не просто строку на экран, а строку как значение. Поэтому добавляютсядвойные кавычки. Для того чтобы выводить строку существует функция putStr. Заменим print на putStr,перекомпилируем и посмотрим что получится:$ ghc --make ReverseIOstr -o rev3str[1 of 1] Compiling Main ( ReverseIOstr.hs, ReverseIOstr.o )Linking rev3str ...$ ./rev3str123321$

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

11.4 Типичные задачи IOВывод на экранНам уже встретилось несколько функций вывода на экран. Это функции: print (вывод значения из эк-

земпляра класса Show), putStr (вывод строки) и putStrLn (вывод строки с переносом). Каждый раз когда мынабираем какое-нибудь выражение в строке интерпретатора и нажимаем Enter, интерпретатор применяет квыражению функцию print и мы видим его на экране.Из простейших функций вывода на экран осталось не рассмотренной лишь функция putChar, но я думаю

вы без труда догадаетесь по типу и имени чем она занимается:

148 | Глава 11: IO

Page 149: Ru Haskell Book

putChar :: Char -> IO ()

Функции вывода на экран также можно вызывать в интерпретаторе:

Prelude> putStr ”Hello” >> putChar ’ ’ >> putStrLn ”World!”Hello World!

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

Ввод пользователяМы уже умеем принимать от пользователя буквы. Это делается функцией getChar. Функцией getLine мы

можем прочитать целую строчку. Строка читается до тех пор пока мы не нажмём Enter.

Prelude> fmap reverse $ getLineHello-hello!”!olleh-olleH”

Есть ещё одна функция для чтения строк, она называется getContents. Основное отличие от getLineзаключается в том, что содержание не читается сразу, а откладывается на потом, когда содержание дей-ствительно понадобится. Это ленивый ввод. Для задачи чтения символов с терминала эта функция можетпоказаться странной. Но часто в символы вводятся не вручную, а передаются из другого файла. Напримересли мы направим на ввод данные из-какого-нибудь большого-большого файла, файл не будет читаться сра-зу, и память не будет заполнена не нужным пока содержанием. Вместо этого программа отложит считываниена потом и будет заниматься им лишь тогда, когда оно понадобится в вычислениях. Это может существенноснизить расход памяти. Мы читаем файл в 2Гб моментально (мы делаем вид, что читаем его). А на самомделе сохраняем себе задачу на будущее: читать ввод, когда придёт пора.

Чтение и запись файловДля чтения и записи файлов есть три простые функции:

type FilePath = String

-- чтение файлаreadFile :: FilePath -> IO String

-- запись строки в файлwriteFile :: FilePath -> String -> IO ()

-- добавление строки в конеци файлаappendFile :: FilePath -> String -> IO ()

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

main = msg1 >> getLine >>= read >>= appendwhere read file = readFile file >>= putStrLn >> return file

append file = msg2 >> getLine >>= appendFile filemsg1 = putStr ”input file: ”msg2 = putStr ”input text: ”

В самом левом вызове getLine мы читаем имя файла, затем оно используется в локальной функцииread. Там мы читаем содержание файла (readLine), выводим его на экран (putStrLn), и в самом конце мывозвращаем из функции имя файла. Оно нам понадобится в следующей части программы, в которой мыбудем читать новые записи и добавлять их в файл. Новая запись читается функцией getLine в локальнойфункции append.Сохраним в модуле File.hs и посмотрим, что у нас получилось. Перед этим создадим в текущей дирек-

тории тестовый пустой файл под именем test. В него мы будем добавлять новые записи.

Типичные задачи IO | 149

Page 150: Ru Haskell Book

*Prelude> :l File[1 of 1] Compiling File ( File.hs, interpreted )Ok, modules loaded: File.*File> maininput file: test

input text: Hello!*File> maininput file: testHello!input text: Hi)*File> maininput file: testHello!Hi)

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

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

программы, они работали в интерактивном режиме, но чаще всего программы принимают какие-нибудьначальные данные, установки или флаги. Читать начальные данные можно с помощью функций из модуляSystem.Environment.Узнать, что передаётся в программу можно функцией getArgs :: IO [String]. Она возвращает список

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

module Main where

import System.Environment

main = getArgs >>= mapM_ putStrLn . zipWith f [1 .. ]where f n a = show n ++ ”: ” ++ a

В локальной функции f мы присоединяем к строке номер через двоеточие. Функцией mapM_ мы пробегаемпо списку строк, отображая их с помощью функции putStrLn. Обратите внимание на краткость программы,с помощью функции композиции мы легко составили функцию, которая приписывает к аргументам числа, азатем выводит их на экран.Скомпилируем программу в интерпретаторе и вызовем её.

*Main> :! ghc --make Args[1 of 1] Compiling Main ( Args.hs, Args.o )Linking Args ...*Main> :! ./Args hey hey hey 23 54 ”qwe qwe qwe” fin1: hey2: hey3: hey4: 235: 546: qwe qwe qwe7: fin

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

вается при вызове. И отвечает в зависимости от настроения программы. Настроение задаётся аргументомпрограммы.

module Main where

import Control.Applicativeimport System.Environment

main = putStrLn =<< reply <$> getProgName <*> getArgs

150 | Глава 11: IO

Page 151: Ru Haskell Book

reply :: String -> [String] -> Stringreply name (x:_) = hi name ++ case x of

”happy” -> ”What a lovely day. What’s up?””sad” -> ”Ooohh. Have you got some news for me?””neutral” -> ”How are you?”

reply name _ = reply name [”neutral”]

hi :: String -> Stringhi name = ”Hi! My name is ” ++ name ++ ”.\n”

В функции reply мы составляем реплику программы. Она зависит от имени программы и поступающихна вход аргументов. Посмотрим, что у нас получилось:

*Main> :! ghc --make HowAreYou.hs -o ninja[1 of 1] Compiling Main ( HowAreYou.hs, HowAreYou.o )Linking ninja ...*Main> :! ./ninja happyHi! My name is ninja.What a lovely day. What’s up?*Main> :! ./ninja sadHi! My name is ninja.Ooohh. Have you got some news for me?

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

которая живёт в модуле System.

system :: String -> IO ExitCode

Она принимает строку и запускает её в терминале. Так же как мы делали это с помощью приставки :! винтерпретаторе. Значение типа ExitCode говорит о результате выполнения строки. Он может быть успешным,тогда функция вернёт ExitSuccess и закончиться ошибкой, тогда мы сможем узнать код ошибки по значениюExitFailure Int.

Случайные значенияФункции для создания случайных значений определены в модуле System.Random1. Сначала давайте раз-

берёмся как генерируются случайные числа. Стандартные случайные числа очень похожи на те, что были унас, когда мы рассматривали примеры специальных функций. У нас есть генератор случайных чисел типа gи с помощью функции next мы можем получить обновлённый генератор и случайное целое число:

next :: g -> (Int, g)

Не правда ли этот тип очень похож на тип результата функций с состоянием. В качестве состояния теперьвыступает генератор случайных чисел g. Это поведение описывается классом RandomGen:

class RandomGen g wherenext :: g -> (Int, g)split :: g -> (g, g)geтRange :: g -> (Int, Int)

Функция next обновляет генератор и возвращает случайное значение типа Int. Функция split раска-лывает один генератор на два. Функция genRange возвращает диапазон значений генерируемых случайныхчисел. Первое значение в паре результата genRange должно быть всегда меньше второго. Для этого классаопределён один экземпляр, это тип StdGen. Мы можем создать первый генератор по целому числу с помощьюфункции mkStdGen:

mkStdGen :: Int -> StdGen

Давайте посмотрим как это происходит в интерпретаторе:1Модуль System.Random входит в библиотеку random.Если в вашей поставке ghc его не оказалось, вы можете установить его вручную

через интернет, набрав в командной строке cabal install random.

Типичные задачи IO | 151

Page 152: Ru Haskell Book

Prelude> :m System.RandomPrelude System.Random> let g0 = mkStdGen 0Prelude System.Random> let (n0, g1) = next g0Prelude System.Random> let (n1, g2) = next g1Prelude System.Random> n02147482884Prelude System.Random> n12092764894

Мы создали первый генератор, а затем начали получать новые. Для того, чтобы получать новые случайныечисла, нам придётся таскать везде за собой генератор случайных чисел. Мы можем обернуть его в функциюс состоянием и пользоваться методами классов Functor, Applicative и Monad. Обновление генератора будетпроисходить за ширмой, во время применения функций. Но у нас есть и другой путь.Вместо монады State мы можем воспользоваться монадой IO. Если нам лень определять генератор слу-

чайных чисел, мы можем попросить компьютер определить его за нас. В этом случае мы взаимодействуем скомпьютером, мы запрашиваем глобальное для системы случайное значение, поэтому возвращаемое значе-ние будет завёрнуто в тип IO. Для этого определены функции:

getStdGen :: IO StdGennewStdGen :: IO StdGen

Функция getStdGen запрашивает глобальный для системы генератор случайных чисел. ФункцияnewStdGen не только запрашивает генератор, но также и обновляет его. Мы пользуемся этими функци-ями так же как и mkStdGen, только теперь мы спрашиваем первый аргумент у компьютера, а не передаём еговручную. Также есть ещё одна полезная функция:

getStdRandom :: (StdGen -> (a, StdGen)) -> IO a

Посмотрим, что получится, если передать в неё функцию next:

Prelude System.Random> getStdRandom next1386438055Prelude System.Random> getStdRandom next961860614

И не надо обновлять никаких генераторов. Но вместо одного неудобства мы получили другое. Теперьзначение завёрнуто в оболочку IO.Генератор StdGen делает случайные числа из диапазона всех целых чисел. Что если мы хотим получить

только числа из некоторого интервала? И как получить случайные значения других типов? Для этого суще-ствует класс Random. Он является удобной надстройкой над классом RandomGen. Посмотрим на его основныеметоды:

class Random a whererandomR :: RandomGen g => (a, a) -> g -> (a, g)random :: RandomGen g => g -> (a, g)

Метод randomR принимает диапазон значений, генератор случайных чисел и возвращает случайное числоиз указанного диапазона и обновлённый генератор. Метод random является синонимом метода next из классаRandomGen, только теперь мы можем получать не только целые числа.Есть и дополнительные методы. Есть методы, которые позволяют генерировать список всех возможных

случайных значений для данного генератора:

randomRs :: RandomGen g => (a, a) -> g -> [a]randoms :: RandomGen g => g -> [a]

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

randomRIO :: (a, a) -> IO arandomIO :: IO a

Эти функции выполняют тоже, что и основные функции класса, но им не нужен генератор случайныхчисел, они создают его с помощью функции getStdRandom. Экземпляры Random определены для Bool, Char,Double, Float, Int и Integer. Например так мы можем подбросить кости десять раз:

152 | Глава 11: IO

Page 153: Ru Haskell Book

Prelude System.Random> fmap (take 10 . randomRs (1, 6)) getStdGen[5,6,5,5,6,4,6,4,4,4]Prelude System.Random> fmap (take 10 . randomRs (1, 6)) getStdGen[5,6,5,5,6,4,6,4,4,4]

Обратите внимание на то, что функция getStdGen не обновляет генератор случайных чисел. Мы запра-шиваем глобальное состояние. Поэтому, дважды подбросив кубик, мы получили одни и те же результаты.Генератор будет обновляться, если воспользоваться функцией newStdGen:

Prelude System.Random> fmap (take 10 . randomRs (1, 6)) newStdGen[1,1,5,6,5,2,5,5,5,3]Prelude System.Random> fmap (take 10 . randomRs (1, 6)) newStdGen[5,4,6,5,5,5,1,5,5,2]

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

Prelude System.Random> fmap (take 5 . randomRs (’a’, ’z’)) newStdGen”maclg”Prelude System.Random> fmap (take 5 . randomRs (’a’, ’z’)) newStdGen”nfjoa”

ЦитатникНапишем небольшую программу, которая будет выводить на экран в случайном порядке цитаты. Цитаты

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

module Main where

import Control.Applicativeimport System.Random

main =format . (quotes !! ) <$> randomRIO (0, length quotes - 1)>>= putStrLn

format (a, b) = b ++ space ++ a ++ spacewhere space = ”\n\n”

quotes = [(”Бьёрн Страуструп”,”Есть лишь два вида языков программирования: те, \

\ на которые вечно жалуются, и те, которые никогда \\ не используются.”),

(”Мохатма Ганди”, ”Ты должен быть теми изменениями, которые\\ ты хочешь видеть вокруг.”),

(”Сократ”, ”Я знаю лишь то, что ничего не знаю.”),(”Китайская народная мудрость”, ”Сохранив спокойствие в минуту\\ гнева, вы можете избежать сотни дней сожалений”),

(”Жан Батист Мольер”, ”Медленно растущие деревья приносят лучшие плоды”),(”Антуан де Сент-Экзюпери”, ”Жить это значит медленно рождаться”),(”Альберт Эйнштейн”, ”Фантазия важнее знания.”),(”Тони Хоар”, ”Внутри любой большой программы всегда есть\\ маленькая, что рвётся на свободу”),

(”Пифагор”, ”Не гоняйся за счастьем, оно всегда находится в тебе самом”),(”Лао Цзы”, ”Путешествие в тысячу ли начинается с одного шага”)]

Функция format приводит цитату к виду приятному для чтения. Попробуем программу в интерпретаторе:

Prelude> :! ghc --make Quote -o hi[1 of 1] Compiling Main ( Quote.hs, Quote.o )Linking hi ...Prelude> :! ./hiПутешествие в тысячу ли начинается с одного шага

Лао Цзы

Типичные задачи IO | 153

Page 154: Ru Haskell Book

Prelude> :! ./hiНе гоняйся за счастьем, оно всегда находится в тебе самом

Пифагор

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

так. Это типы Maybe и Either. Если функции не удалось вычислить значение она возвращает специальноезначение Nothing или Left reason, по которому следующая функция может опознать ошибку и предпринятькакие-нибудь действия. Так обрабатываются ошибки в чистых функциях. В этом разделе мы узнаем о том,как обрабатываются ошибки, которые происходят при взаимодействии с внешним миром, ошибки, которыепроисходят внутри типа IO.Ошибки функций с побочными эффектами обрабатываются с помощью специальной функции catch, она

определена в Prelude:

catch :: IO a -> (IOError -> IO a) -> IO a

Эта функция принимает значение, которое содержит побочные эффекты и функцию, которая обрабаты-вает исключительные ситуации. К примеру если мы попытаемся прочитать данные из файла, к которому унас нет доступа, произойдёт ошибка. Мы можем не дать программе упасть и обработать ошибку с помощьюфункции catch.Например программа, в которой мы дописывали данные в файл, упадёт, если мы передадим не существу-

ющий файл. Но мы можем исправить это поведение с помощью функции catch. Мы можем перезапускатьпрограмму, если произошла ошибка:

module FileSafe where

import Control.Applicativeimport Control.Monad

main = try ‘catch‘ const main

try = msg1 >> getLine >>= read >>= appendwhere read file = readFile file >>= putStrLn >> return file

append file = msg2 >> getLine >>= appendFile filemsg1 = putStr ”input file: ”msg2 = putStr ”input text: ”

Часто функции двух аргументов называют так, чтобы при инфиксной форме записи получалась фразаиз английского языка. Так если мы запишем catch в инфиксной форме получится очень наглядное выраже-ние. Функция обработки ошибок реагирует на любую ошибку перезапуском программы. Попробуем взломатьпрограмму:

*FileSafe> maininput file: fsldfksldinput file: sd;fls;dfl;vll; d;fld;finput file: dflks;ldkf ldkfldkfldinput file: lsdkfksdlf ksdkflsdfkls;dfkinput file: bfkinput file: testHello!Hi)input text: HowHow

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

main = try ‘catch‘ const (msg >> main)where msg = putStrLn ”Wrong filename, try again.”

А что делать если нам хочется различать ошибки по типу и предпринимать различные действия в зави-симости от типа ошибки? Ошибки распознаются с помощью специальных предикатов, которые определеныв модуле System.IO.Error. Рассмотрим некоторые из них.

154 | Глава 11: IO

Page 155: Ru Haskell Book

Например с помощью с помощью предиката isDoesNotExistErrorType мы можем опознать ошибки,которые случились из-за того, что один из аргументов функции не существует. С помощью предикатаisPermissionErrorType мы можем узнать, что ошибка произошла из-за того, что мы пытались получить до-ступ к данным, на которые у нас нет прав. Мы можем, немного изменив функцию-обработчик исключений,выводить более информативные сообщения об ошибках перед перезапуском:

main = try ‘catch‘ handler

handler :: IOError -> IO ()handler = ( >> main) . putStrLn . msg2 . msg1

msg1 e| isDoesNotExistErrorType e = ”File does not exist. ”| isPermissionErrorType e = ”Access denied. ”| otherwise = ””

msg2 = (++ ”Try again.”)

В модуле System.IO.Error вы можете найти ещё много разных предикатов.

Потоки текстовых данныхОбмен данными, чтение и запись происходят с помощью потоков. Каждый поток имеет дескриптор

(handle), через него мы можем общаться с потоком, например считывать данные или записывать. Функциидля работы с потоками данных определены в модуле System.IO.В любой момент в системе открыты три стандартных потока:

• stdin – стандартный ввод• stdout – стандартный вывод• stderr – поток ошибок и отладочных сообщений

Например когда мы выводим строку на экран, на самом деле мы записываем строку в поток stdout. Акогда мы читаем символ с клавиатуры, мы считываем его из потока stdin.Файлы также являются потоками. При открытии файлу присваивается дескриптор через который, мы

можем обмениваться данными. Файл может быть открыт для чтения, записи, дополнения (записи в конецфайла) или чтения и записи. Файл открывается функцией:

openFile :: FilePath -> IOMode -> IO Handle

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

• ReadMode – чтение• WriteMode – запись• AppendMode – добавление (запись в конец файла)• ReadWriteMode – чтение и запись

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

-- запись символаhPutChar :: Handle -> Char -> IO ()

-- запись строкиhPutStr :: Handle -> String -> IO ()

-- запись строки с переносом кареткиhPutStrLn :: Handle -> String -> IO ()

-- запись значенияhPrint :: Show a => Handle -> a -> IO ()

Типичные задачи IO | 155

Page 156: Ru Haskell Book

Все функции принимают первым аргументом дескриптор потока. Дескриптор должен позволять записы-вать данные. Например для дескриптора, открытого в режиме ReadMode, выполнение этих функций приведётк ошибке.Из потоков также можно читать данные. Эти функции похожи на те, что мы уже рассмотрели:

-- чтение одного символаhGetChar :: Handle -> IO Char

-- чтение строкиhGetLine :: Handle -> IO String

-- ленивое чтение строкиhGetContents :: Handle -> IO String

Как только, мы закончим работу с файлом, его необходимо закрыть. Нам нужно освободить дескриптор.Сделать это можно функцией hClose:

hClose :: Handle -> IO ()

Стандартные функции ввода/вывода, которые мы рассмотрели ранее определены через функции работыс дескрипторами. Например так мы выводим строку на экран:

putStr :: String -> IO ()putStr s = hPutStr stdout s

А так читаем строку с клавиатуры:

getLine :: IO StringgetLine = hGetLine stdin

В этих функциях используются дескрипторы стандартных потоков данных stdin и stdout. Отметим функ-цию withFile:

withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r

Она открывает файл в заданном режиме выполняет функцию на его дескрипторе и и закрывает файл.Например через эту функцию определены функции readFile и appendFile:

appendFile :: FilePath -> String -> IO ()appendFile f txt = withFile f AppendMode (\hdl -> hPutStr hdl txt)

writeFile :: FilePath -> String -> IO ()writeFile f txt = withFile f WriteMode (\hdl -> hPutStr hdl txt)

11.5 Форточка в мир побочных эффектовВ самом начале главы я сказал о том, что из мира IO нет выхода. Нет функции с типом IO a -> a. На

самом деле выход есть. Функция с таким типом живёт в модуле System.IO.Unsafe:

unsafePerformIO :: IO a -> a

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

будет только читаться и во время выполнения программы файл не может быть изменён другой программой,то мы можем считать, что его значение окажется неизменным на протяжении работы программы. Это говорито том, что нам не важно когда читать данные. Поэтому здесь мы вроде бы ничем не рискуем. ”Вроде бы”потому что ответственность за постоянство файла лежит на наших плечах.Эта функция часто используется при вызове функций С через Haskell. В Haskell есть возможность вызывать

функции, написанные на C. Но по умолчанию такие функции заворачиваются в тип IO. Если функция являетсячистой в С, то она будет чистой и при вызове через Haskell. Мы можем поручиться за её чистоту и вычислительнам поверит. Но если мы его обманули, мы пожнём плоды своего обмана.

156 | Глава 11: IO

Page 157: Ru Haskell Book

Отладка программРаз уж речь зашла о ”грязных” возможностях языка стоит упомянуть функцию trace из модуля

Debug.Trace. Посмотрим на её тип:trace :: String -> a -> a

Это служебная функция эхо-печати. Когда дело доходит до вычисления функции trace на экран выводит-ся строка, которая была передана в неё первым аргументом, после чего функция возвращает второй аргумент.Это функция id с побочным эффектом вывода сообщения на экран. Ею можно пользоваться для отладки. На-пример так можно вернуть значение и распечатать его:echo :: Show a => a -> aecho a = trace (show a) a

11.6 Краткое содержаниеНаконец-то мы научились писать программы! Программы, которые можно исполнять за пределами ин-

терпретатора. Для этого нам пришлось познакомиться с типом IO. Экземпляр класса Monad для этого типаинтерпретируется специальным образом. Он вносит упорядоченность в выполнение программы. В нашемстатическом мире описаний появляются такие понятия как ”сначала”, ”затем”, ”до” и ”после”. Но они несмогут нанести много вреда.Вычисление операций с побочными эффектами разбивает программу на кадры. Но это не мешает нам

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

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

11.7 УпражненияСтарайтесь свести присутствие функций с побочными эффектами к минимуму. Идеальный случай, когда

тип IO встречается только в функции main. Часто программы устроены более хитрым образом и функциис побочными эффектами пытаются расползтись по всему коду. Но даже в этом случае программу можноразделить на две части: в одной живут подлинные источники побочных эффектов, такие как чтение файлов,генерация случайных значений, а в другой – чистые функции. Старайтесь устроить программу так, чтобыона была максимально чистой. Чистые функции гораздо проще комбинировать, понимать, изменять.• Это упражнение даёт вам возможность почувствовать преимущества чистого кода. Вспомните функ-цию поиска корней методом неподвижной точки (этот пример встречался в главе о ленивых вычисле-ниях). Напишите на основе этого примера программу, которая будет распечатывать решение и после-довательность приближений. Последовательность приближений состоит из текущего значения корняи расстоянии между корнями.Напишите два варианта программы, в одном вы измените алгоритм так, чтобы печать происходилаодновременно с вычислениями (не пользуясь функцией из модуля Debug.Trace). А в другом вариан-те алгоритм останется прежним. Но теперь вместо решения найдите список первых приближений дорешения. А затем передайте его в отдельную функцию печати результатов.В первом варианте алгоритм смешан с печатью. А во втором программа распадается на две части, частьвычислений и часть вывода результатов на экран. Сравните два подхода.• Напишите программу для угадывания чисел. Компьютер загадал число в заданном диапазоне и про-сит вас угадать его. Если вы ошибаетесь он подсказывает: ”холодно-горячо” или ”больше-меньше”.Программа принимает два аргумента, которые определяют диапазон возможных значений для неиз-вестного числа.• С помощью стандартных функций для генерации случайных чисел напишите программу, которая про-водит состязание по игре в кости. Программа принимает аргументом суммарное число очков необходи-мых для победы. Двое игроков бросают по очереди кости побеждает тот, кто первым наберёт заданнуюсумму.Сделайте так чтобы результаты выводились постепенно. С каждым нажатием на Enter вы подбрасы-ваете кости (два шестигранных кубика). После каждого раунда программа выводит промежуточныерезультаты.

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

Page 158: Ru Haskell Book

• Напишите программу, которая принимает два аргумента: набор слов разделённых пробелами и файл.А выводит она строки файла, в которых встречается данное слово.Воспользуйтесь стандартными функциями из модуля Data.List

-- разбиение строки на подстроки по переносам кареткиlines :: String -> [String]

-- разбиение строки на подстроки по пробеламwords :: String -> [String]

-- возвращает True только в том случае, если-- первый список полностью содержится во второмisInfixOf :: Eq a => [a] -> [a] -> Bool

158 | Глава 11: IO

Page 159: Ru Haskell Book

Глава 12

Поиграем

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

12.1 Стратегия написания программОписание задачиРешение задачи начинается с описания проблемы и наброска решения. Мы хотим создать программу,

в которой можно будет играть в пятнашки. Если вам не знакома это игра, то взгляните на рис. 12.1. Играначинается с позиции, в которой все фишки перемешаны. Необходимо, переставляя фишки, вернуться висходное положение. В исходном положении фишки идут по порядку.

151413

1211109

8765

4321

6121415

37102

51113

8419

Рис. 12.1: Случайное и конечное состояние игры пятнашки

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

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

игры и спрашивает ход следующий ход. Потом она распознаёт ход, и показывает обновлённое поле. И такдалее. Нам нужно как-то организовать этот диалог.При этом в программе можно выделить две независимые части. Одна отвечает за сам диалог. Она прини-

мает реплики пользователя и отображает поле для игры. А другая часть отвечает за правила игры пятнашки:как ходы влияют на поле, какое положение является победным, как перемешивать фишки.

| 159

Page 160: Ru Haskell Book

У нас будет два отдельных модуля: один для описания игры, назовём его Game, а другой для описаниядиалога с пользователем. Мы назовём его Loop (петля или цикл), поскольку диалог это зацикленная проце-дура получения реплики и реакции на реплику.Такой вот набросок-ориентир. После этого можно приступать к реализации. Но с чего начать?

Каркас. Типы и классыВ Haskell программы обычно начинают строить с каркаса, т.е. с типов и классов. Нам нужно выделить

основные сущности и подумать какие типы подходят для их описания лучше всего.В нашей задаче есть поле с фишками и ходы. Мы делаем ходы и фишки двигаются. Поле – это матрица

или двумерный массив. У нас есть два индекса, которые пробегают значения от нуля до трёх. В каждойячейке массива хранятся фишки. Фишки обозначаются целыми числами:type Pos = (Int, Int)type Label = Int

type Board = Array Pos Label

Пустую фишку мы будем также обозначать числом. Физически когда мы ходим, мы меняем положениеодной фишки. Но в нашем описании мы меняем местами две фишки, поскольку пустая фишка также обозна-чается номером. Когда мы ходим, мы меняем положение пустой фишки, одним ходом мы можем сместитьеё вверх, вниз, влево или вправо. Введём специальный тип для обозначения ходов:data Move = Up | Down | Left | Right

Для того чтобы при каждом ходе не искать пустую клетку, давайте сохраним её текущее положение. ТипGame будет содержать текущее положение пустой клетки и положение фишек:data Game = Game {

emptyField :: Pos,gameBoard :: Board }

Вот и все типы для описания игры. Сохраним их в модуле Game. Теперь подумаем о типах для диалогас пользователем. В этом модуле наверняка будет много функций с типом IO, потому что в нём происходитвзаимодействие с игроком. Но, что является каркасом для диалога?Если мы хотим с кем-нибудь общаться, необходимо чтобы у нас был с собеседником общий язык, он и

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

• Сделать ход• Начать новую игру• Выйти из игры

Если пользователь делает ход мы показываем новое положение поля, если он начинает новую игру мыпоказываем ему новую перемешанную позицию, давайте у нас будет разная степень перемешанности фи-гур. При перемешивании мы стартуем из победного положения и начинаем случайным образом делать хо-ды. Чем больше ходов мы сделаем тем сложнее будет собрать игру. Поэтому пользователь будет указыватьчисло шагов для перемешивания при запросе новой игры. Если пользователь попросит закончить игру мыпопрощаемся и выйдем из игры.На основе этих рассуждений вырисовывается следующий тип для сообщений:

data Query = Quit | NewGame Int | Play Move

Значение типа Query (запрос) может быть константа Quit (выход), запрос новой игры NewGame с числом,которое указывает на сложность новой игры, также игрок может просто сделать ход Play Move.А каков формат наших ответов? Все наши ответы на самом деле будут вызовами функции putStrLn мы

будем отвечать пользователю изменениями экрана. Поэтому у нас нет специального типа для ответов. Итаку нас есть каркас, который можно начинать покрывать значениями. На этом этапе у нас есть два модуля. Этомодуль Loop:module Loop where

import Game

data Query = Quit | NewGame Int | Play Move

160 | Глава 12: Поиграем

Page 161: Ru Haskell Book

И модуль Game:

module Game where

import Data.Array

data Move = Up | Down | Left | Rightderiving (Enum)

type Label = Int

type Pos = (Int, Int)

type Board = Array Pos Label

data Game = Game {emptyField :: Pos,gameBoard :: Board }

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

от синонимов от корня к листьям или сверху вниз. Оказывается таким способом можно писать программы.Более того в функциональном программировании это очень распространённый подход. Мы начинаем соспецификации задачи (неформального описания) и потихоньку вытягиваем из него выражения языка Haskell.Начинаем мы с корня, с самой верхней функции. Эта функция будет состоять из подвыражений. Когда мынапишем верхнюю функцию, мы перейдём к подвыражениям. И так мы будем спускаться пока не напишемвсю программу.Кажется, что такой подход очень не надёжен. Ведь мы сможем запустить программу только когда напи-

шем её целиком. На каждом промежуточном шаге у нас есть неопределённые подвыражения. Получается,что очень долгое время мы будем писать программу, не зная работает она или нет.Оказывается, что в Haskell есть решение этой проблемы. Нам поможет взрывное значение undefined. Мы

будем писать только тип функции (и мысленно будем говорить, пусть она делает то-то), а вместо определе-ния будем писать undefined. При этом конечно мы не сможем выполнять программу, вычислитель подорвёт-ся на первом же значении, но мы сможем узнать осмысленна ли наша программа с точки зрения компилятора,проходит ли она проверку типов. В Haskell это большой плюс. Если программа прошла проверку типов, тоскорее всего она будет работать.Такой подход написания программ называется написанием сверху вниз. Мы начинаем с самой верхней

функции и потихоньку вычищаем все undefined. Если вспомнить ленивые вычисления, то там роль undefinedвыполняли отложенные вычисления, мы обозначали их как #.В чём преимущества такого подхода? Посмотрим на дерево рис. 12.2. Если мы идём сверху вниз, то в

самом начале у нас лишь одна задача, потом их становится всё больше и больше. Они дробятся, но источ-ник у них один. Мы всегда знаем, что нам нужно чтобы закончить нашу задачу. Написать это, это и этоподвыражение. Беда только в том, что это подвыражение содержит ещё больше подвыражений. Но сложныеподвыражения мы можем оставить на потом и заняться другими. А потом, когда мы их доделаем может вдругоказаться, что это сложное выражение нам и не нужно.

Рис. 12.2: Дерево задач

Стратегия написания программ | 161

Page 162: Ru Haskell Book

Если же мы начинаем идти из листьев, то у нас много отправных точек, которые должны сойтись в однойцели. При этом они могут и не сойтись, мы можем застрять в одной точке и потратить слишком многовремени. И на остальные задачи у нас не хватит сил или мы можем потратить много времени на решениезадачи, которая совсем не нужна для итогового решения. Также как и в вычислениях по значению, мы можемзастрять на вычислении бесконечного значения, даже если в итоговом ответе нам понадобится лишь егомалая часть.Ещё один плюс решения сверху вниз состоит в экономии усилий. Мы можем написать всю программу в

виде функций, которые состоят лишь из определений типов. И утрясти общую схему программы на типах.Мы не тратим время на реализацию, а смотрим как программа выглядит ”вцелом”. Если общий набросокнас устраивает мы можем начать заполнять дыры и детализировать отдельные выражения. Так мы будемдетализировать-детализировать пока не придём к первоначальному решению. Далее если у нас останетсявремя мы можем сменить реализацию некоторых частей. Но общая схема останется прежней, она уже усто-ялась на уровне типов.Но давайте приступим к реализации нашей игры. Самая верхняя функция, будет запускать программу.

Назовём её play. Это функция взаимодействия с пользователем она ведёт диалог, поэтому её тип будет IO ():

play :: IO ()play = undefined

Итак у нас появилась корневая функция. Что мы будем в ней делать? Для начала мы поприветствуем игро-ка (функция greetings). Затем предложим ему начать игру (функция setup), после чего запустим цикл игры(функция gameLoop). Приветствие это просто надпись на экране, поэтому тип у него будет IO (). Предложе-ние игры вернёт стартовую позицию для игры, поэтому тип будет IO Game. Цикл игры принимает состояниеи продолжает диалог. В типах это выражается так:

play :: IO ()play = greetings >> setup >>= gameLoop

greetings :: IO ()greetings = undefined

setup :: IO Gamesetup = undefined

gameLoop :: Game -> IO ()gameLoop = undefined

Сохраним эти определения в модуле Loop и загрузим модуль с программой в интерпретатор:

Prelude> :l Loop[1 of 2] Compiling Game ( Game.hs, interpreted )[2 of 2] Compiling Loop ( Loop.hs, interpreted )Ok, modules loaded: Game, Loop.*Loop>

Модуль загрузился. Он потянул за собой модуль Game, потому что мы воспользовались типом Move изэтого модуля. Программа прошла проверку типов, значит она осмысленна и мы можем двигаться дальше.У нас три варианта дальнейшей детализации это функции greetings, setup и gameLoop. Мы пока пропу-

стим greetings там мы напишем какое-нибудь приветствие и сообщим игроку куда он попал и как ходить.В функции setup нам нужно начать первую игру. Для начала игры нам нужно узнать её сложность, т.е. на

сколько ходов перемешивать позицию. Это значит, что нам нужно спросить у игрока целое число. Мы спро-сим число функцией getLine, а затем попробуем его распознать. Если пользователь ввёл не число, то мыпопросим его повторить ввод. Функция readInt :: String -> Maybe Int распознаёт число. Она возвращаетцелое число завёрнутое в Maybe, потому что строка может оказаться не числом. Затем это число мы исполь-зуем в функции shuffle (перемешать), которая будет возвращать позицию, которая перемешана с заданнойглубиной.

-- в модуль Loop

setup :: IO Gamesetup = putStrLn ”Начнём новую игру?” >>

putStrLn ”Укажите сложность (положительное целое число): ” >>getLine >>= maybe setup shuffle . readInt

162 | Глава 12: Поиграем

Page 163: Ru Haskell Book

readInt :: String -> Maybe IntreadInt = undefined

-- в модуль Game:

shuffle :: Int -> IO Gameshuffle = undefined

Функция shuffle возвращает состояние игры Game, которое завёрнуто в IO. Оно завёрнуто в IO, потомучто перемешивать позицию мы будем случайным образом, это значит, что мы воспользуемся функциями измодуля Random. Мы хотим чтобы каждая новая игра начиналась с новой позиции, поэтому скорее всего где-тов недрах функции shuffle мы воспользуемся newStdGen, которая и потянет за собой тип IO.Игра перемешивается согласно правилам, поэтому функцию shuffle мы поселим в модуле Game. А функ-

ция readInt это скорее элемент взаимодействия с пользователем, ведь в ней мы распознаём число в строчномответе, она останется в модуле Loop.Проверим работает ли наша программа:

*Loop> :r[1 of 2] Compiling Game ( Game.hs, interpreted )[2 of 2] Compiling Loop ( Loop.hs, interpreted )Ok, modules loaded: Game, Loop.*Loop>

Работает! Можно спускаться по дереву выражения ниже. Сейчас нам предстоит написать одну из самыхсложных функций, это функция gameLoop.

12.2 ПятнашкиЦикл игрыФункция цикла игры принимает текущую позицию. При этом у на два варианта. Возможно игра пришла

в конечное положение (isGameOver) и мы можем сообщить игроку о том, что он победил (showResults), еслиэто не так, то мы покажем текущее положение (showGame), спросим ход (askForMove) и среагируем на ход(reactOnMove).

-- в модуль Loop

gameLoop :: Game -> IO ()gameLoop game

| isGameOver game = showResults game >> setup >>= gameLoop| otherwise = showGame game >> askForMove >>= reactOnMove game

showResults :: Game -> IO ()showResults = undefined

showGame :: Game -> IO ()showGame = undefined

askForMove :: IO QueryaskForMove = undefined

reactOnMove :: Game -> Query -> IO ()reactOnMove = undefined

-- в модуль Game

isGameOver :: Game -> BoolisGameOver = undefined

Как определить закончилась игра или нет это скорее дело модуля Game. Все остальные функции принадле-жат модулю Loop. Функция askForMove возвращает реплику пользователя и тут же направляет её в функциюreactOnMove. Функции showGame и showResults ничего не возвращают, они только меняют состояния экрана.После того как игра закончится мы предложим игроку начать новую.

Пятнашки | 163

Page 164: Ru Haskell Book

Обратите внимание на то, как даже не дав определение функции, мы всё же очерчиваем её смысл вобъявлении типа. Так посмотрев на функцию askForMove и сопоставив тип с именем, мы можем понять, чтоэта функция предназначена для запроса значения типа Query, для запроса реплики пользователя. А по типуфункции showGame мы можем понять, что она проводит какой-то побочный эффект, судя по имени что-топоказывает, из типа видно что показывает значение типа Game или текущую позицию.

Отображение позиции

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

showResults :: Game -> IO ()showResults g = showGame g >> putStrLn ”Игра окончена.”

Теперь определим функцию showGame. Если тип Game является экземпляром класса Show, то определениеокажется совсем простым:

-- в модуль Loop

showGame :: Game -> IO ()showGame = putStrLn . show

-- в модуль Game

instance Show Game whereshow = undefined

Реакция на реплики пользователя

Теперь нужно определить функции askForMove и reactOnMove. Первая функция требует установить про-токол реплик пользователя, в каком виде он будет набирать значения типа Query. Нам пока лень об этомдумать и мы перейдём к функции reactOnMove. Вспомним её тип:

reactOnMove :: Game -> Query -> IO ()

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

reactOnMove :: Game -> Query -> IO ()reactOnMove game query = case query of

Quit ->NewGame n ->Play m ->

Рассмотрим каждый из случаев. В первом случае пользователь говорит, что ему надоело и он уже наиг-рался. Чтож попрощаемся и вернём значение единичного типа.

...Quit -> quit

...

quit :: IO ()quit = putStrLn ”До встречи.” >> return ()

В следующем варианте пользователь хочет начать всё заново. Так начнём!

NewGame n -> gameLoop =<< shuffle n

Мы вызвали функцию перемешивания shuffle с заданным уровнем сложности. И рекурсивно вызвалицикл игры с новой позицией. Всё началось по новой. В третьей альтернативе пользователь делает ход, на этомы должны обновить позицию запустить цикл игры с новым значением:

164 | Глава 12: Поиграем

Page 165: Ru Haskell Book

-- в модуль LoopPlay m -> gameLoop $ move m game

-- в модуль Gamemove :: Move -> Game -> Gamemove = undefined

Функция move обновляет согласно правилам текущую позицию. Соберём все определения вместе:

reactOnMove :: Game -> Query -> IO ()reactOnMove game query = case query of

Quit -> quitNewGame n -> gameLoop =<< shuffle nPlay m -> gameLoop $ move m game

Слушаем игрока

Теперь всё же вернёмся к функции askForMove, научимся слушать пользователя. Сначала мы скажемкакую-нибудь вводную фразу, предложение ходить (showAsk) затем запросим строку стандартной функциейgetLine, потом нам нужно будет распознать (parseQuery) в строке значение типа Query. Если распознать егонам не удастся, мы напомним пользователю как с нами общаться (remindMoves) и попросим сходить вновь:

askForMove :: IO QueryaskForMove = showAsk >>

getLine >>= maybe askAgain return . parseQuerywhere askAgain = wrongMove >> askForMove

parseQuery :: String -> Maybe QueryparseQuery = undefined

wrongMove :: IO ()wrongMove = putStrLn ”Не могу распознать ход.” >> remindMoves

showAsk :: IO ()showAsk = undefined

remindMoves :: IO ()remindMoves = undefined

Механизм распознавания похож на случай с распознаванием числа. Значение завёрнуто в тип Maybe. И всамом деле функция определена лишь частично, ведь не все строки кодируют то, что нам нужно.Функции parseQuery и remindMoves тесно связаны. В первой мы распознаём ввод пользователя, а во вто-

рой напоминаем пользователю как мы закодировали его запросы. Тут стоит остановиться и серьёзно поду-мать. Как закодировать значения типа Query, чтобы пользователю было удобно набирать их? Но давайтеотвлечёмся от этой задачи, она слишком серьёзная. Оставим её на потом, а пока проверим не ушли ли мыслишком далеко, возможно наша программа потеряла смысл. Проверим типы!

*Loop> :r[1 of 2] Compiling Game ( Game.hs, interpreted )[2 of 2] Compiling Loop ( Loop.hs, interpreted )Ok, modules loaded: Game, Loop.

Приведём код в порядокНам осталось дописать функции распознавания запросов и несколько маленьких функций с фразами и

модуль Loop будет готов. Но перед тем как сделать это давайте упорядочим функции. Видно, что у нас выде-лилось несколько задач по типу общения с пользователем. У нас есть задачи, в которых мы что-то показываемпользователю, меняем состояние экрана и есть задачи, в которых мы просим от пользователя какие-то дан-ные, ожидаем запросы функцией getLine. Также в самом верху выражения программы у нас расположеныфункции, которые координируют действия остальных, это третья группа. Сгруппируем функции по этомупринципу.

Пятнашки | 165

Page 166: Ru Haskell Book

Основные функцииplay :: IO ()play = greetings >> setup >>= gameLoop

gameLoop :: Game -> IO ()gameLoop game

| isGameOver game = showResults game >> setup >>= gameLoop| otherwise = showGame game >> askForMove >>= reactOnMove game

setup :: IO Gamesetup = putStrLn ”Начнём новую игру?” >>

putStrLn ”Укажите сложность (положительное целое число): ” >>getLine >>= maybe setup shuffle . readInt

Запросы от пользователя (getLine)reactOnMove :: Game -> Query -> IO ()reactOnMove game query = case query of

Quit -> quitNewGame n -> gameLoop =<< shuffle nPlay m -> gameLoop $ move m game

askForMove :: IO QueryaskForMove = showAsk >>

getLine >>= maybe askAgain return . parseQuerywhere askAgain = wrongMove >> askForMove

parseQuery :: String -> Maybe QueryparseQuery = undefined

readInt :: String -> Maybe IntreadInt = undefined

Ответы пользователю (putStrLn)greetings :: IO ()greetings = undefined

showResults :: Game -> IO ()showResults g = showGame g >> putStrLn ”Игра окончена.”

showGame :: Game -> IO ()showGame = putStrLn . show

showAsk :: IO ()showAsk = undefined

quit :: IO ()quit = putStrLn ”До встречи.” >> return ()

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

Формат запросовМожно вывести с помощью deriving экземпляр класса Read для типа Query и читать их функцией read.

Но это плохая идея, потому что пользователь нашей программы может и не знать Haskell. Лучше введёмсокращённые имена для всех значений. Например такие:

left -- Play Leftright -- Play Rigthup -- Play Updown -- Play Down

quit -- Quitnew n -- NewGame n

166 | Глава 12: Поиграем

Page 167: Ru Haskell Book

Можно обратить внимание на то, что все команды начинаются с разных букв. Воспользуемся этим и дадимпользователю возможность набирать команды одной буквой. Это приводит на с к таким определениям дляфункций разбора значения и напоминания ходов:parseQuery :: String -> Maybe QueryparseQuery x = case x of

”up” -> Just $ Play Up”u” -> Just $ Play Up”down” -> Just $ Play Down”d” -> Just $ Play Down”left” -> Just $ Play Left”l” -> Just $ Play Left”right” -> Just $ Play Right”r” -> Just $ Play Right”quit” -> Just $ Quit”q” -> Just $ Quit

’n’:’e’:’w’:’ ’:n -> Just . NewGame =<< readInt n’n’:’ ’:n -> Just . NewGame =<< readInt n_ -> Nothing

remindMoves :: IO ()remindMoves = mapM_ putStrLn talk

where talk = [”Возможные ходы пустой клетки:”,” left или l -- налево”,” right или r -- направо”,” up или u -- вверх”,” down или d -- вниз”,”Другие действия:”,” new int или n int -- начать новую игру, int - целое число,”,

”указывающее на сложность”,” quit или q -- выход из игры”]

Проверим работоспособность:Prelude> :l Loop[1 of 2] Compiling Game ( Game.hs, interpreted )[2 of 2] Compiling Loop ( Loop.hs, interpreted )

Loop.hs:46:28:Ambiguous occurrence ‘Left’It could refer to either ‘Prelude.Left’,

imported from ‘Prelude’ at Loop.hs:1:8-11(and originally defined in ‘Data.Either’)

or ‘Game.Left’,imported from ‘Game’ at Loop.hs:5:1-11(and originally defined at Game.hs:10:25-28)

Loop.hs:47:28:Ambiguous occurrence ‘Left’

...

...Failed, modules loaded: Game.*Game>

По ошибкам видно, что произошёл конфликт имён. Конструкторы Left и Right уже определены в Prelude.Это конструкторы типа Either. Давайте скроем их, добавим в модуль такую строчку:import Prelude hiding (Either(..))

Теперь проверим:*Game> :r[2 of 2] Compiling Loop ( Loop.hs, interpreted )Ok, modules loaded: Game, Loop.*Loop>

Всё работает, можно двигаться дальше.

Пятнашки | 167

Page 168: Ru Haskell Book

Последние штрихиВ модуле Loop нам осталось определить несколько маленьких функций. Поиск по слову undefined гово-

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

greetings :: IO ()readInt :: String -> Maybe IntshowAsk :: IO ()

Самая простая это функция showAsk, она приглашает игрока сделать ход:

showAsk :: IO ()showAsk = putStrLn ”Ваш ход: ”

Теперь функция распознавания целого числа:

import Data.Char (isDigit)...

readInt :: String -> Maybe IntreadInt n

| all isDigit n = Just $ read n| otherwise = Nothing

В первой альтернативе мы с помощью стандартной функции isDigit :: Char -> Bool проверяем, чтострока состоит из одних только чисел. Если все символы числа, то мы пользуемся функцией из модуля Readи читаем целое число, иначе возвращаем Nothing.Последняя функция, это функция приветствия. Когда игрок входит в игру он сталкивается с её результа-

тами. Определим её так:

-- в модуль Loop

greetings :: IO ()greetings = putStrLn ”Привет! Это игра пятнашки” >>

showGame initGame >>remindMoves

-- в модуль Game

initGame :: GameinitGame = undefined

Сначала мы приветствуем игрока, затем показываем состояние (initGame), к которому ему нужно стре-миться, и напоминаем как делаются ходы. На этом определении мы раскрыли все выражения в модуле Loop,нам остался лишь модуль Game.

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

ны уже определились в ходе описания диалога с пользователем. Нам нужно уметь составлять начальноесостояние initGame, уметь составлять перемешанное состояние игры shuffle, нам нужно уметь реагиро-вать на ходы move, определять какая позиция является выигрышной isGameOver и уметь показывать фишкив красивом виде. Приступим!

initGame :: Gameshuffle :: Int -> IO GameisGameOver :: Game -> Boolmove :: Move -> Game -> Game

instance Show Game whereshow = undefined

Таков наш план.

168 | Глава 12: Поиграем

Page 169: Ru Haskell Book

Начальное состояниеНачнём с самой простой функции составим начальное состояние:

initGame :: GameinitGame = Game (3, 3) $ listArray ((0, 0), (3, 3)) $ [0 .. 15]

Мы будем кодировать фишки цифрами от нуля до 14, а пустая клетка будет равна 15. Это просто согла-шения о внутреннем представлении фишек, показывать мы их будем совсем по-другому.С этим значением мы можем легко определить функцию определения конца игры. Нам нужно только

добавить deriving (Eq) к типу Game. Тогда функция isGameOver примет вид:isGameOver :: Game -> BoolisGameOver = ( == initGame)

Делаем ходНапишем функцию:

move :: Move -> Game -> Game

Она обновляет позицию после хода. В пятнашках не во всех позициях доступны все ходы. Если пустышканаходится на краю, мы не можем вывести её за пределы доски. Это необходимо как-то учесть. Каждый ходзадаёт направление обмена фишками. Если у нас есть текущее положение пустышки и ход, то по ходу мы мо-жем узнать направление, а по направлению ту фишку, которая займёт место пустышки после хода. При этомнам необходимо проверять находится ли та фишка, которую мы хотим поместить на пустое место в пределахдоски. Например если пустышка расположена в самом верху и мы хотим сделать ход Up, т.е. передвинуть еёещё выше, то положение игры не должно измениться.import Prelude hiding (Either(..))

newtype Vec = Vec (Int, Int)

move :: Move -> Game -> Gamemove m (Game id board)

| within id’ = Game id’ $ board // updates| otherwise = Game id boardwhere id’ = shift (orient m) id

updates = [(id, board ! id’), (id’, emptyLabel)]

-- определение того, что индексы внутри доскиwithin :: Pos -> Boolwithin (a, b) = p a && p b

where p x = x >= 0 && x <= 3

-- смещение положение по направдениюshift :: Vec -> Pos -> Posshift (Vec (va, vb)) (pa, pb) = (va + pa, vb + pb)

-- направление ходаorient :: Move -> Vecorient m = Vec $ case m of

Up -> (-1, 0)Down -> (1 , 0)Left -> (0 ,-1)Right -> (0 , 1)

-- метка для пустой фишкиemptyLabel :: LabelemptyLabel = 15

Маленькие функции within, shift, orient, emptyLabel делают как раз то, что подписано в комментариях.Думаю, что их определение не сложно понять. Но есть одна тонкость, поскольку в функции orient мы поль-зуемся конструкторами Left и Right необходимо спрятать тип Either из Prelude. Мы ввели дополнительныйтип Vec для обозначения смещения, чтобы случайно не подставить вместо него индексы.Разберёмся с функцией move. Сначала мы вычисляем положение фишки, которая пойдёт на пустое место

id’. Мы делаем это, сместив (shift) положение пустышки (id) по направлению хода (orient a).Мы обновляем массив, который описывает доску с помощью специальной функции //. Посмотрим на её

тип:

Пятнашки | 169

Page 170: Ru Haskell Book

(//) :: Ix i => Array i a -> [(i, a)] -> Array i a

Она принимает массив и список обновлений в этом массиве. Обновления представлены в виде парыиндекс-значение. В охранном выражении мы проверяем, если индекс перемещаемой фишки в пределах дос-ки, то мы возвращаем новое положение, в котором пустышка уже находится в положении id’ и массив об-новлён. Мы составляем список обновлений updates bз двух элементов, это перемещения фишки и пустышки.Если же фишка за пределами доски, то мы возвращаем исходное положение.

Перемешиваем фишки

Игра начинается с такого положения, в котором все фишки перемешаны. Но перемешивать фишки про-извольным образом было бы не честно, поскольку известно, что в пятнашках половина расстановок не при-водит к выигрышу. Поэтому мы будем перемешивать так: мы стартуем из начального положения и делаемнесколько ходов произвольным образом. Количество ходов определяет сложность игры:

randomGame :: Int -> IO GamerandomGame n = (iterate (shuffle =<<) $ pure initGame) !! n

shuffle :: Game -> IO Gameshuffle g = flip move g <$> (randomElem $ nextMoves g)

randomElem :: [a] -> IO arandomElem = undefined

nextMoves :: Game -> [Move]nextMoves = und

Сначала разберёмся с функцией shuffle она перемешивает позицию один раз. Мы делаем ход в текущейпозиции, который мы выбрали случайным образом из списка доступных ходов. Выбором случайного эле-мента из списка, будет заниматься функция randomElem. Теперь вернёмся к функции составления случайнойпозиции. В ней мы пользуемся функцией iterate для того чтобы n раз перемешать позицию. Но мы не можемпросто написать:

iterate shuffle initGame

Так у нас не совпадут типы. Для функции iterate нужно чтобы вход и выход функции имели одинаковыетипы. Поэтому мы пользуемся в функции iterate функцией применения специальных значений.Нам осталось определить всего две функции, и всё готово для игры. Определим выбор случайного эле-

мента из списка:

import System.Random...

randomElem :: [a] -> IO arandomElem xs = (xs !! ) <$> randomRIO (0, length xs - 1)

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

nextMoves :: Game -> [Move]nextMoves g = filter (within . flip shift (emptyField g) . orient)

[Up, Down, Left, Right]

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

Отображение положения

Я немного поторопился, нам осталась ещё одна функция. Это отображение позиции. Я не буду подробноостанавливаться на теле функции, скажу лишь то, что она составляет строку так как это показано в коммен-тарии к функции.

170 | Глава 12: Поиграем

Page 171: Ru Haskell Book

-- +----+----+----+----+-- | 1 | 2 | 3 | 4 |-- +----+----+----+----+-- | 5 | 6 | 7 | 8 |-- +----+----+----+----+-- | 9 | 10 | 11 | 12 |-- +----+----+----+----+-- | 13 | 14 | 15 | |-- +----+----+----+----+--instance Show Game where

show (Game _ board) = ”\n” ++ space ++ line ++(foldr (\a b -> a ++ space ++ line ++ b) ”\n” $ map column [0 .. 3])where post id = showLabel $ board ! id

showLabel n = cell $ show $ case n of15 -> 0n -> n+1

cell ”0” = ” ”cell [x] = ’ ’:’ ’: x :’ ’:[]cell [a,b] = ’ ’: a : b :’ ’:[]line = ”+----+----+----+----+\n”nums = ((space ++ ”|”) ++ ) . foldr (\a b -> a ++ ”|” ++ b) ”\n”.

map postcolumn i = nums $ map (\x -> (i, x)) [0 .. 3]space = ”\t”

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

Prelude> :l Loop[1 of 2] Compiling Game ( Game.hs, interpreted )[2 of 2] Compiling Loop ( Loop.hs, interpreted )Ok, modules loaded: Loop, Game.*Loop> playПривет! Это игра пятнашки

+----+----+----+----+| 1 | 2 | 3 | 4 |+----+----+----+----+| 5 | 6 | 7 | 8 |+----+----+----+----+| 9 | 10 | 11 | 12 |+----+----+----+----+| 13 | 14 | 15 | |+----+----+----+----+

Возможные ходы пустой клетки:left или l -- налевоright или r -- направоup или u -- вверхdown или d -- вниз

Другие действия:new int или n int -- начать новую игру, int - целое число,

указывающее на сложностьquit или q -- выход из игры

Начнём новую игру?Укажите сложность (положительное целое число):5

+----+----+----+----+| 1 | 2 | 3 | 4 |+----+----+----+----+| 5 | 6 | 7 | 8 |+----+----+----+----+| 9 | | 10 | 11 |+----+----+----+----+| 13 | 14 | 15 | 12 |+----+----+----+----+

Ваш ход:

Пятнашки | 171

Page 172: Ru Haskell Book

r+----+----+----+----+| 1 | 2 | 3 | 4 |+----+----+----+----+| 5 | 6 | 7 | 8 |+----+----+----+----+| 9 | 10 | | 11 |+----+----+----+----+| 13 | 14 | 15 | 12 |+----+----+----+----+

Ваш ход:r

+----+----+----+----+| 1 | 2 | 3 | 4 |+----+----+----+----+| 5 | 6 | 7 | 8 |+----+----+----+----+| 9 | 10 | 11 | |+----+----+----+----+| 13 | 14 | 15 | 12 |+----+----+----+----+

Ваш ход:d

+----+----+----+----+| 1 | 2 | 3 | 4 |+----+----+----+----+| 5 | 6 | 7 | 8 |+----+----+----+----+| 9 | 10 | 11 | 12 |+----+----+----+----+| 13 | 14 | 15 | |+----+----+----+----+

Игра окончена.

Ураа, получилось. Мы так долго писали программу, проверяя лишь типы, и в самом конце, когда мызакончили определение, всё работает. Конечно не всё работает так гладко, я уже написал эту программу иобъясняю готовое решение, но когда общая схема программы утряслась, возможные ошибки определяютсяна раз. Мы могли вызвать отображение позиции не в том порядке или забыть проверку конца игры, всё этонесколько строчек изменений.Самые неприятные ошибки происходят, когда в середине выясняется, что мы ошиблись с типами. Типы,

которые мы выбрали не могут описать явление, возможно мы не можем делать какие-то операции, которыенам, как неожиданно выяснилось, очень нужны. Это значит, что нужно менять каркас. Менять каркас, этозначит сносить весь дом и строить новый. Возможно разрушения окажутся локальными, мы строим не дом,а город. И сносить придётся не всё, а несколько кварталов. Но это тоже большие перемены. Поэтому шагопределения типов очень важен.

12.3 Упражнения• Измените диалог с пользователем. Сделайте так чтобы у игры было главное меню, в котором игроквыбирает разные побочные функции, вроде выхода, начать новую игру, подсказка и игровое меню, вкотором игрок только передвигает фишки. Когда игрок собирает игру он попадает в главное меню.• Добавьте в игру подсчёт статистики. Если игрок дошёл до победной позиции он узнаёт за сколько ходовему удалось решить задачу. Также ведётся история предыдущих попыток, по которой пользовательможет следить как изменяются его результаты.• Подумайте можно ли выделить интерфейс игры в отдельный класс так, чтобы модуль Loop не зависелот конкретной реализации игры. Чтобы можно было, опираясь на абстрактные методы, вроде show дляGame, или реакции на ход, вести диалог с пользователем. Попробуйте переписать игру пятнашки спомощью такого класса.• Попробуйте написать другую игру, например игру раскладывания пасьянса, крестики-нолики илишашки, не меняя модуля Loop. Так чтобы вы сделали необходимые экземпляры для классов из преды-дущего упражнения, а всё остальное поведение следовало из них.

172 | Глава 12: Поиграем

Page 173: Ru Haskell Book

Глава 13

Лямбда-исчисление

В этой главе мы узнаем о лямбда-исчислении. Лямбда-исчисление описывает понятие алгоритма. Ещёдо появления компьютеров в 30-е годы двадцатого века математиков интересовал вопрос о возможности со-здания алгоритма, который мог бы на основе заданных аксиом дать ответ о том верно или нет некотороелогическое высказывание. Например у нас есть базовые утверждения и логические связки такие как ”и”,”или”, ”для любого из”, ”существует один из”, с помощью которых мы можем строить из базовых высказы-ваний составные. Некоторые из них окажутся ложными, а другие истинными. Нам интересно узнать какие.Но для решения этой задачи прежде всего необходимо было понять а что же такое алгоритм?Ответ на этот вопрос дали Алонсо Чёрч (Alonso Church) и Алан Тьюринг (Alan Turing). Чёрч разработал

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

стейших, а также подставлять в функции аргументы, которые могут быть как константами так и другимифункциями. Как только мы составили выражение мы можем передать его вычислителю. Он подставляет ар-гументы в функции и возвращает такое выражение, в котором невозможно далее проводить подстановкиаргументов. Этот процесс проведения подстановок считается вычислением алгоритма.В рамках теории машин Тьюринга алгоритм описывается по-другому. Машина Тьюринга имеет внут-

реннее состояние, Состояние содержит некоторое значение, которое изменяется по ходу работы машины.Машина живёт не сама по себе, она читает ленту символов. Лента символов – это большая цепочка букв.На каждую букву машина реагирует серией действий. Она может изменить значение состояния, обновитьбукву в ленте или перейти к следующему или предыдущему символу. Есть состояния, которые обозначаютконец работы, они называются терминальными. Как только машина дойдёт до терминального состояния мысчитаем, что вычисление алгоритма закончилось. После этого мы можем считать результат из состояниймашины.Функциональные языки программирования основаны на лямбда-исчислении. Поэтому мы будем гово-

рить именно об этом описании алгоритма.

13.1 Лямбда исчисление без типовСоставление термовМожно считать, что лямбда исчисление это такой маленький язык программирования. В нём есть множе-

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

• Переменные x, y, z …являются термами.• ЕслиM и N – термы, то (MN) – терм.• Если x – переменная, аM – терм, то (λx. M) – терм

В формальном описании добавляют ещё одно правило, оно говорит о том, что других термов нет. Первоеправило, говорит о том, что у нас есть алфавит символов, который что-то обозначает, эти символы явля-ются базовыми строительными блоками программы. Второе и третье правила говорят о том как из базовыхэлементов получаются составные. Второе правило – это правило применения функции к аргументу. В нёмM обозначает функцию, а N обозначает аргумент. Все функции являются функциями одного аргумента, ноони могут принимать и возвращать функции. Поэтому применение трёх аргументов к функции Fun будетвыглядеть так:

| 173

Page 174: Ru Haskell Book

(((Fun Arg1) Arg2) Arg3)

Третье правило говорит о том как создавать функции. Специальный символ лямбда (λ) в выражении(λx. M) говорит о том, что мы собираемся определить функцию с аргументом x и телом функцииM . С та-кими функциями мы уже сталкивались. Это безымянные функции. Приведём несколько примеров функций.Начнём с самого простого, определим тождественную функцию:

(λx. x)

Функция принимает аргумент x и тут же возвращает его в теле. Теперь посмотрим на константную функ-цию:

(λx. (λy. x))

Константная функция является функцией двух аргументов, поэтому наш терм принимает переменнуюx и возвращает другой терм функцию (λy. x). Эта функция принимает y, а возвращает x. В Haskell мы бынаписали это так:

\x -> (\y -> x)

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

(λf. (λg. (λx. (f(gx)))))

Переменные f и g – это функции, которые участвуют в композиции, а x это вход результирующей функ-ции. Уже в таком простом выражении у нас пять скобок на конце. Давайте введём несколько соглашений,которые облегчат написание термов:

Пишем ПодразумеваемОпустим внешние скобки: λx. x (λx. x)В применении группируем скобки влево: fghx ((fg)h)xФ функциях группируем скобки вправо: λx. λy. x (λx. (λy. x))Пишем функции нескольких аргументов λxy. x (λx. (λy. x))с одной лямбдой:

С этими соглашениями мы можем переписать терм для композиции так:

λfgx. f(gx)

Сравните с выражением на языке Haskell:

\f g x -> f (g x)

Выражения очень похожи. Haskell иногда называют засахаренной версией лямбда исчисления. В лямбда-исчислении мы не будем ставить пробелы для применения аргументов к функции. Мы будем считать, чтовсе имена однобуквенные. При этом переменные мы будем писать с маленькой буквы, а составные термы сбольшой.Определим ещё несколько функций. Например так выглядит функция flip:

λfxy. fyx

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

λf. λxy. fyx

Определим функцию on, она принимает функцию двух аргументов ∗ и функцию одного аргумента f , авозвращает функцию двух аргументов, в которой к аргументам сначала применяется функция f , а затем онипередаются в функцию ∗:

λ ∗ f. λx. ∗ (fx)(fx)

В лямбда-исчислении есть только префиксное применение поэтому мы написали ∗(fx)(fx) вместо при-вычного (fx) ∗ (fx). Здесь операция ∗ это не только умножение, а любая бинарная функция.

174 | Глава 13: Лямбда-исчисление

Page 175: Ru Haskell Book

АбстракцияФункции в лямбда-исчислении называют абстракциями. Мы берём терм M и параметризуем его по пе-

ременной x в выражении λx.M . При этом если в терме M встречается переменная x, то она становитсясвязанной. Например в терме λx.λy.x Переменная x является связанной, но в терме λy.x, она уже не связа-на. Такие переменные называют свободными. Множество связанных переменных термаM мы будем обозна-чать BV (M) от англ. bound variables, а множество свободных переменных мы будем обозначать FV (M) отангл. free variables.На интуитивном уровне процесс абстракции заключается в том, что мы смотрим на несколько частных

случаев и видим в них что-то общее. Это общее мы выделяем в функцию, которая параметризована частно-стями. Например мы видим выражения:

λx. + xx, λx. ∗ xx

И в том и в другом у нас есть функция двух аргументов + или ∗ и мы делаем из неё функцию одногоаргумента. Мы можем абстрагировать (параметризовать) это поведение в такую функцию:

λb. λx. bxx

На Haskell мы бы записали это так:

\b -> \x -> b x x

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

(λx. M) N

Заменяются на

M [x = N ]

Эта запись означает, что в терме M все вхождения x заменяются на терм N . Этот процесс называетсяредукцией терма. А выражения вида (λx. M) N называются редексами. Проведём к примеру редукцию терма:

(λb. λx. bxx)∗

Для этого нам нужно в терме (λx. bxx) заменить все вхождения переменной b на переменную ∗. Послеэтого мы получим терм:

λx. ∗ xx

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

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

Например рассмотрим такой редекс:

(λxy. x) y

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

λy. y

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

(λxz. x) y

После подстановки будет совсем другим. Но мы всего лишь изменили обозначение локальной перемен-ной y на z. И смысл изменился, для того чтобы исключить такие случаи пользуются переименованием пе-ременных или α-преобразованием. Для корректной работы функций необходимо следить за тем, чтобы всепеременные, которые были свободными в аргументе, остались свободными и после подстановки.

Лямбда исчисление без типов | 175

Page 176: Ru Haskell Book

β-редукцияПроцесс подстановки аргументов в функции называется β-редукцией. В редексе (λx. M)N вместо свобод-

ных вхождений x вM мы подставляем N . Посмотрим на правила подстановки:

x[x = N ] ⇒ Ny[x = N ] ⇒ y(PQ)[x = N ] ⇒ (P [x = N ] Q[x = N ])(λy. P )[x = N ] ⇒ (λy. P [x = N ]), y /∈ FV (N)(λx. P )[x = N ] ⇒ (λx. P )

Первые два правила определяют подстановку вместо переменных. Если переменная совпадает с той, наместо которой мы подставляем терм N , то мы возвращаем терм N , иначе мы возвращаем переменную:

x[x = N ] ⇒ Ny[x = N ] ⇒ y

Подстановка применения термов равна применению термов, в которых произведена подстановка:

(PQ)[x = N ] ⇒ (P [x = N ] Q[x = N ])

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

(λy. P )[x = N ] ⇒ (λy. P [x = N ]), y /∈ FV (N)

Условие y /∈ FV (N) означает, что необходимо следить за тем, чтобы в N не оказалось свободной пере-менной с именем y, иначе после подстановки она окажется связанной. Если такая переменная в N всё-такиокажется мы проведём α-преобразование в терме λy. M и заменим y на какую-нибудь другую переменную.В последнем правиле мы ничего не меняем, поскольку переменная x оказывается связанной. А мы про-

водим подстановку только вместо свободных переменных:

(λx. P )[x = N ] ⇒ (λx. P )

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

(λx. xx)(λx. xx)

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

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

по имени и вычисление по значению. Также там мы узнали о том, что ленивые вычисления это улучшеннаяверсия вычисления по имени, в которой аргументы функций вычисляются не более одного раза.Эти стратегии вычисления пришли из лямбда-исчисления. Если нам нужно избавиться от всех редексов

в выражении, то с какого редекса лучше начать? В вычислении по значению (аппликативная стратегия) мыначинаем с самого левого редекса, который не содержит других редексов, т.е. с самого маленького подвы-ражения. А в вычислении по имени (нормальная стратегия) мы начинаем с самого левого внешнего редекса.Левый редекс означает, что в записи выражения он находится ближе всех к началу выражения.

Теорема 1 (Карри) Если у терма есть нормальная форма, то последовательное сокращение самого левого внеш-него редекса приводит к ней.

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

(λxy. x) z ((λx. xx)(λx. xx))

Этот терм имеет нормальную форму z несмотря на то, что мы передаём вторым аргументом в констант-ную функцию терм, у которого нет нормальной формы. Алгоритм вычисления по значению зависнет привычислении второго аргумента. В то время как алгоритм вычисления по имени начнёт с самого внешнеготерма и там определит, что второй аргумент не нужен.Ещё один важный результат в лямбда-исчислении был сформулирован в следующей теореме:

176 | Глава 13: Лямбда-исчисление

Page 177: Ru Haskell Book

Теорема 2 (Чёрча-Россера) Если термX редуцируется к термам Y1 и Y2, то существует терм L, к которомуредуцируются и терм Y1 и терм Y2.

Эта теорема говорит о том, что терма может быть только одна нормальная форма. Поскольку если быих было две, то существовал третий терм, к которому можно было бы редуцировать эти нормальные формы.Но по определению нормальной формы, мы не можем её редуцировать. Из этого следует, что нормальныеформы должны совпадать.Теорема Чёрча-Россера указывает на способ сравнения термов. Для того чтобы понять равны термы или

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

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

ции вызвать саму функции, ведь мы не можем на неё сослаться, кажется, что у нас нет возможности строитьрекурсивные функции. Однако это не так. Нам на помощь придёт комбинатор неподвижной точки. По опре-делению комбинатор неподвижной точки решает задачу: для терма F найти терм X такой, что

FX = X

Существует много комбинаторов неподвижной точки. Рассмотрим Y -комбинатор:

Y = λf. (λx. f(xx))(λx. f(xx))

Убедимся в том, что для любого терма F , выполнено тождество: F (Y F ) = Y F :

Y F = (λx. F (xx))(λx. F (xx)) = F (λx. F (xx))(λx. F (xx)) = F (Y F )

Так с помощью Y -комбинатора можно составлять рекурсивные функции.

Кодирование структур данныхВы наверное заметили, что пока мы составляли лишь обобщённые функции. Эти функции комбинируют

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

ваны с помощью термов лямбда-исчисления. Тезис Чёрча утверждает, что с помощью лямбда-терма можнопредставить любую вычислимую числовую функцию. В 1936 году Чёрч с помощью лямбда-исчисления дока-зал существование неразрешимых проблем в теории чисел. Из этого следовала неразрешимость арифметикии неразрешимость исчисления логики предикатов первого порядка. Система аксиом называется разрешимойв том случае, если существует такой алгоритм, который позволяет по виду формулы определить следует лиона из заданных аксиом или нет.Посмотрим как с помощью термов кодируются структуры данных. Далее для сокращения записи мы бу-

дем считать, что в лямбда исчислении можно определять синонимы с помощью знака равно. ЗаписьN =Mговорит о том, что мы дали обозначениеN термуM . Этой операции нет в лямбда-исчислении, но мы будемпользоваться ею для удобства.

Логические значенияСуть логических значений заключается в операторе If , с помощью которого мы можем организовывать

ветвление алгоритма. Есть два терма True и False, которые для любых термов a и b, обладают свойствами:

If True a b = a

If False a b = b

Термы True, False и If , удовлетворяющие таким свойствам выглядят так:

True = λt f. t

False = λt f. f

If = λb x y. bxy

Лямбда исчисление без типов | 177

Page 178: Ru Haskell Book

Проверим выполнение свойств:

If True a b⇒ (λb x y. bxy)(λt f. t) a b⇒ (λt f. t) a b⇒ a

If False a b⇒ (λb x y. bxy)(λt f. f) a b⇒ (λt f. f) a b⇒ b

Свойства выполнены. Логические константы кодируются постоянными функциями двух аргументов.Функция True возвращает первый аргумент, игнорируя второй. А функция False делает то же самое, но на-оборот. В такой интерпретации логическое отрицание можно закодировать с помощью функции flip. Такжемы можем выразить и другие логические операции:

And = λa b. a b False

Or = λa b. a True b

Мы определили логические значения не конкретными значениями, а свойствами функций. Мы построилифункции, которые ведут себя как логические значения. Этот способ определения напоминает, определениекласса типов. Мы объявили три метода True, False и If и сказали, что экземпляр класса должен удовле-творять определённым свойствам, которые накладывают взаимные ограничения на методы класса. Ни одиниз методов не имеет смысла по отдельности, важно то как они взаимодействуют.

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

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

Zero = λsz. z

Succ = λnsz. s(nsz)

Как и в случае логических значений числа кодируются функциями двух аргументов. Число определяетсяпо терму, подсчётом цепочки первых аргументов s. Например так выглядит число два:

Succ (Succ Zero) ⇒ (λnsz. s(nsz))(Succ Zero) ⇒ λsz. s((Succ Zero)sz) ⇒λsz. s((λnsz. s(nsz)) Zero)sz ⇒ λsz. s(s(Zero s z)) ⇒ λsz. s(sz)

И мы получили два вхождения первого аргумента в теле функции. Определим сложение и умножение.Сложение принимает две функции двух аргументов и возвращает функцию двух аргументов.

Add = λ m n s z. m s (n s z)

В этой функции мы применяемm раз аргумент s к значению, в котором аргумент s применён n раз, такмы и получаемm+ n применений аргумента s. Сложим 3 и 2:

Add 3 2 ⇒ λs z. 3 s (2 s z) ⇒ λs z. 3 s (s (s z)) ⇒ λs z. s ( s (s (s (s z)))) ⇒ 5

В умножении чиселm и n мы будемm раз складывать число n:

Mul = λm n s z. m (Add n) Zero

Конструктивная математикаВ конструктивной математике существование объекта может быть доказано только описанием алгорит-

ма, с помощью которого можно построить объект. Например доказательство методом ”от противного” от-вергается.Лямбда исчисление строит конструктивное описание функции. По лямбда-терму мы можем не только

вычислять значения функции, но и понять как она была построена. В классической теории, функция этомножество пар (x, f(x)) аргумент-значение, которое обладает свойством:

x = y ⇒ f(x) = f(y)

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

178 | Глава 13: Лямбда-исчисление

Page 179: Ru Haskell Book

Расширение лямбда исчисленияПредположим, что мы решили написать язык программирования на основе лямбда-исчисления. Было бы

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

ные и константы. Для констант мы можем определять специальные правила редукции. Например мы можемдополнить исчисление константами:

+, ∗, 0, 1, 2, . . .

И ввести для них правила редукции, которые запрашивают ответ у процессора:

a+ b = AddWithCPU(a, b)

a ∗ b = MulWithCPU(a, b)

Так же мы можем определить и константы для логических значений:

True, False, If, Not, And, Or

И определить правила редукции:

If True a b = a

If False a b = b

Not True = False

Not False = True

Add False a = False

Add True b = b

. . .

Такие правила называют δ-редукцией (дельта-редукция).

13.2 Комбинаторная логикаОдновременно с лямбда-исчислением развивалась комбинаторная логика. Она отличается более ком-

пактным представлением. Есть всего лишь одно правило, это применение функции к аргументу. А функциистроятся не из произвольных термов, а из набора основных функций. Набор основных функций называютбазисом.Рассмотрим лямбда-термы:

λx. x, λy. y, λz. z

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

• Есть набор переменных x, y, z, …. Переменная – это терм.• Есть две константы K и S, они являются термами.• ЕслиM и N – термы, то (MN) – терм.• Других термов нет.

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

Kxy = x

Sxyz = xz(yz)

Комбинаторная логика | 179

Page 180: Ru Haskell Book

В этих правилах мы пользуемся соглашением о расстановки скобок. Также как и в лямбда исчислении вприменении скобки группируются влево. Когда мы пишем Kxy, мы подразумеваем ((Kx)y). Термы в ком-бинаторной логике принято называть комбинаторами. Редукция происходит до тех пор пока мы можем за-менять вхождения базисных комбинаторов. Так если мы видим связкуKXY или SXY Z, гдеX , Y , Z произ-вольные термы, то мы можем их заменить согласно правилам редукции. Такие связки называют редексами.Если в терме нет ни одного редекса, то он находится в нормальной форме. Замену редекса принято называтьсвёрткойИнтересно, что комбинаторы K и S совпадают с определением класса Applicative для функций:

instance Applicative (r->) wherepure a r = a(<*>) a b r = a r (b r)

В этом определении у функций есть общее окружение r, из которого они могут читать значения, так же каки в случае типа Reader. В методе pure (комбинаторK) мы игнорируем окружение (это константная функция),а в методе <*> (комбинаторS) передаём окружение в функцию и аргумент и составляем применение функциив контексте окружения r к значению, которое было получено в контексте того же окружения.Вернёмся к проблеме различного представления тождественной функции в лямбда-исчислении. В ком-

бинаторной логике тождественная функция выражается так:

I = SKK

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

Ix = SKKx = Kx(Kx) = x

Сначала мы заменили I на его определение, затем свернули по комбинатору S, затем по левому комби-натору K. В итоге получилось, что

Ix = x

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

ϕ, которая переводит термы комбинаторной логики в термы лямбда-исчисления:

ϕ(x) = x

ϕ(K) = λxy. x

ϕ(S) = λxyz. xz(yz)

ϕ(XY ) = ϕ(X) ϕ(Y )

В первом уравнении x – переменная. Также можно определить функцию ψ, которая переводит термылямбда-исчисления в термы комбинаторной логики.

ψ(x) = x

ψ(XY ) = ψ(X) ψ(Y )

ψ(λx. Y ) = [x]. ψ(Y )

Запись [x]. T , где x – переменная, T – терм, обозначает такой термD, из которого можно получить термT подстановкой переменной x, выполнено свойство:

([x]. T ) x = T

Эта запись означает параметризацию терма T по переменной x. Терм [x]. T можно получить с помощьюследующего алгоритма:

180 | Глава 13: Лямбда-исчисление

Page 181: Ru Haskell Book

[x]. x = SKK

[x] . X = KX, x /∈ V (X)

[x] . XY = S([x]. X)([x]. Y )

В первом уравнении мы заменяем переменную на тождественную функцию, поскольку переменные сов-падают. Запись V (X) во втором уравнении обозначает множество всех переменных в терме X . Посколькупеременная по которой мы хотим параметризовать терм (или абстрагировать) не участвует в самом терме,мы можем проигнорировать её с помощью постоянной функции K. В последнем уравнении мы параметри-зуем применение.С помощью этого алгоритма можно для любого терма T , все переменные которого содержатся в

{x1, . . . xn} составить такой комбинатор D, что Dx1 . . . xn = T . Для этого мы последовательно парметри-зуем терм T по всем переменным:

[x1, . . . , xn]. T = [x1]. ([x2, . . . , xn]. T )

Так постепенно мы придём к выражению, считаем что скобки группируются вправо:

[x1]. [x2]. . . . [xn]. T

Немного историиКомбинаторную логику открыл Моисей Шейнфинкель. В 1920 году на докладе в Гёттингене он рассказал

основные положения этой теории. Комбинаторная логика направлена на выделение простейших строитель-ных блоков математической логики. В этом докладе появилось понятие частичного применения. Шейнфин-кель показал как функции многих переменных могут быть сведены к функциям одного переменного. Далеев докладе описываются пять основных функций, называемых комбинаторами:

Ix = x – функция тождестваCxy = x – константная функцияTxyz = xzy – функция перестановкиZxyz = x(yz) – функция группировкиSxyz = xz(yz) – функция слияния

С помощью этих функций можно избавиться в формулах от переменных, так например свойство комму-тативности функции A можно представить так: TA = A. Эти комбинаторы зависят друг от друга. Можноубедиться в том, что:

I = SCC

Z = S(CS)S

T = S(ZZS)(CC)

Все комбинаторы выражаются через комбинаторыC и S. Ранее мы пользовались другими обозначениямидля этих комбинаторов. ОбозначенияK и S ввёл Хаскель Карри (Haskell Curry). Независимо отШейнфинкеляон переоткрыл комбинаторную логику и существенно развил её. В современной комбинаторной логике дляобозначения комбинаторов I , C, T , Z и S (по Шейнфинкелю) принято использовать имена I , K, C, B, S(по Карри).

13.3 Лямбда-исчисление с типамиМыможем добавить в лямбда-исчисление типы. Предположим, что у нас есть множество V базовых типов.

Тогда тип это:

T = V | T → T

Тип может быть либо одним элементом из множества базовых типов. Либо стрелочным (функциональ-ным) типом. Выражение ”терм M имеет тип α” принято писать так: Mα. Стрелочный тип α → β как и вHaskell говорит о том, что если у нас есть значение типа α, то с помощью операции применения мы можемиз терма с этим стрелочным типом получить терм типа β.Опишем правила построения термов в лямбда-исчислении с типами:

Лямбда-исчисление с типами | 181

Page 182: Ru Haskell Book

• Переменные xα, yβ , zγ , …являются термами.• ЕслиMα→β и Nα – термы, то (Mα→βNα)β – терм.• Если xα – переменная иMβ – терм, то (λxα. Mβ)α→β – терм• Других термов нет.

Типизация накладывает ограничение на то, какие выражения мы можем комбинировать. В этом естьплюсы и минусы. Теперь наша система является строго нормализуемой, это означает, что любой терм име-ет нормальную форму. Но теперь мы не можем выразить все функции на числах. Например мы не можемсоставить Y -комбинатор, поскольку теперь самоприменение (ee) невозможно.Мы ввели типы, но лишились рекурсии. Как нам быть? Эта проблема решается с помощью введения

специальной константы Y (τ→τ)→ττ , которая обозначает комбинатор неподвижной точки. Правило редукции

для Y :

(Yτfτ→τ )τ = (fτ→τ (Yτf

τ→τ ))τ

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

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

ными теориями функций. Конструктивными в том смысле, что определение функции содержит не наборзначений, а рецепт получения этих значений. В лямбда-исчислении мы видим как функция была построена,из каких простейших частей она состоит. Редукция термов позволяет вычислять функции.Мы узнали, что функциями можно кодировать логические значения и числа. Узнали, что все численные

функции могут быть закодированы лямбда-термами.

13.5 Упражнения• С помощью редукции убедитесь в том, что верны формулы (в терминах Карри) :

B = S(KS)S

C = S(BBS)(KK)

Bxyz = xzy

Cxyz = x(yz)

• Попробуйте закодировать пары с помощью лямбда термов. Вам необходимо построить три функции:Pair, Fst, Snd, которые обладают свойствами:

Fst (Pair a b) = a

Snd (Pair a b) = b

• в комбинаторной логике тоже есть комбинатор неподвижной точки, найдите его с помощью алгоритмаприведения термов лямбда исчисления к термам комбинаторной логики. Для краткости лучше вместоSKK писать просто I .• Напишите типы Lam и App, которые описывают лямбда-термы и термы комбинаторной логики в Haskell.Напишите функции перевода из значений Lam в App и обратно.

182 | Глава 13: Лямбда-исчисление

Page 183: Ru Haskell Book

Глава 14

Теория категорий

Многие понятия в Haskell позаимствованы из теории категорий, например это функторы, монады. Теориякатегорий – это скорее язык, математический жаргон, она настолько общая, что кажется ей нет никакогоприменения. Возможно это и так, но в этом языке многие сущности, которые лишь казались родственнымии было смутное интуитивное ощущение их близости, становятся тождественными.Теория категорий занимается описанием функций. В лямбда-исчислении основной операцией была под-

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

14.1 КатегорияМы будем говорить об объектах и связях между ними. Связи принято называть ”стрелками” или ”мор-

физмами”. Далее мы будем пользоваться термином стрелка. У стрелки есть начальный объект, его называютдоменом (domain) и конечный объект, его называют кодоменом (codomain).

Af - B

В этой записи стрелка f соединяет объекты A и B, в тексте мы будем писать это так f : A→ B, словнострелка это функция, а объекты это типы. Мы будем обозначать объекты большими буквами A, B, C, …, астрелки – маленькими буквами f , g, h, …Для того чтобы связи было интереснее изучать мы введём такоеправило:

Af - B

f ; g

C

g

?-

Если конец стрелки f указывает на начало стрелки g, то должна быть такая стрелка f ; g, которая обозна-чает составную стрелку. Вводится специальная операция ”точка с запятой”, которая называется композициейстрелок: Это правило говорит о том, что связи распространяются по объектам. Теперь у нас есть не простообъекты и стрелки, а целая сеть объектов, связанных между собой. Тот факт, что связи действительно рас-пространяются отражается свойством:

f ; (g ; h) = (f ; g) ; h

Это свойство называют ассоциативностью. Оно говорит о том, что стрелки, которые образуют составнуюстрелку являются цепочкой и нам не важен порядок их группировки, важно лишь кто за кем идёт. Подра-зумевается, что стрелки f , g и h имеют подходящие типы для композиции, что их можно соединять. Этосвойство похоже на интуитивное понятие пути, как цепочки отрезков.Связи между объектами можно трактовать как преобразования объектов. Стрелка f : A→ B – это способ,

с помощью которого мы можем перевести объект A в объект B. Композиция в этой аналогии приобретаетестественную интерпретацию. Если у нас есть способ f : A → B преобразования объекта A в объект B, и

| 183

Page 184: Ru Haskell Book

способ g : B → C преобразования объекта B в объект C, то мы конечно можем, применив сначала f , азатем g, получить из объекта A объект C.Когда мы думаем о стрелках как о преобразовании, то естественно предположить, что у нас есть преобра-

зование, которое ничего не делает, как тождественная функция. В будем говорить, что для каждого объектаA есть стрелка idA, которая начинается из этого объекта и заканчивается в нём же.

idA : A→ A

Тот факт, что стрелка idA ничего не делает отражается свойствами, которые должны выполняться длявсех стрелок:

idA ; f = f

f ; idA = f

Если мы добавим к любой стрелке тождественную стрелку, то от этого ничего не изменится.Всё готово для того чтобы дать формальное определение понятия категории (category). Категория это:

• Набор объектов (object).• Набор стрелок (arrow) или морфизмов (morphism).• Каждая стрелка соединяет два объекта, но объекты могут совпадать. Так обозначают, что стрелка fначинается в объекте A и заканчивается в объекте B:

f : A→ B

При этом стрелка соединяет только два объекта:

f : A→ B, f : A′ → B′ ⇒ A = A′, B = B′

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

f : A→ B, g : B → C ⇒ f ; g : A→ C

• Для каждого объекта есть стрелка, которая начинается и заканчивается в этом объекте. Эту стрелкуназывают тождественной (identity):

idA : A→ A

Должны выполняться аксиомы:

• Тождество id

id ; f = f

f ; id = f

• Ассоциативность ;f ; (g ; h) = (f ; g) ; h

Приведём примеры категорий.

• Одна точка с одной тождественной стрелкой образуют категорию.• В категории Set объектами являются все множества, а стрелками – функции. Стрелки соединяются спомощью композиции функций, тождественная стрелка, это тождественная функция.• В категории Hask объектами являются типы Haskell, а стрелками – функции, стрелки соединяются спомощью композиции функций, тождественная стрелка, это тождественная функция.

184 | Глава 14: Теория категорий

Page 185: Ru Haskell Book

• Ориентированный граф может определять категорию. Объекты – это вершины, а стрелки это связанныепути в графе. Соединение стрелок – это соединение путей, а тождественная стрелка, это путь в которомнет ни одного ребра.• Упорядоченное множество, в котором есть операция сравнения на больше либо равно задаёт катего-рию. Объекты – это объекты множества. А стрелки это пары объектов таких, что первый объект меньшевторого. Первый объект в паре считается начальным, а второй конечным.

(a, b) : a→ b если a ≤ b

Стрелки соединяются так:

(a, b) ; (b, c) = (a, c)

Тождественная стрелка состоит из двух одинаковых объектов:

ida = (a, a)

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

Отметим, что бывают такие области, в которых стрелки или преобразования с одинаковыми именамимогут соединять разные несколько разных объектов. Например в Haskell есть классы и функции с однимии теми же именами могут соединять разные объекты. Если все условия категории для объектов и стрелоквыполнены, кроме этого, то такую систему называют прекатегорией (pre-category). Из любой прекатегориине сложно сделать категорию, если включить имена объектов в имя стрелки. Тогда у каждой стрелки будуттолько одна пара объектов, которые она соединяет.

14.2 ФункторВспомним определение класса Functor:

class Functor f wherefmap :: (a -> b) -> (f a -> f b)

В этом определении участвуют тип f и метод fmap. Можно сказать, что тип f переводит произвольныетипы a в специальные типы f a. В этом смысле тип f является функцией, которая определена на типах. Методfmap переводит функции общего типа a -> b в специальные функции f a -> f b.При этом должны выполняться свойства:

fmap id = idfmap (f . g) = fmap f . fmap g

Теперь вспомним о категории Hask. В этой категории объектами являются типы, а стрелками функции.Функтор f отображает объекты и стрелки категории Hask в объекты и стрелки f Hask. При этом оказывается,что за счёт свойств функтора f Hask образует категорию.• Объекты – это типы f a.• Стрелки – это функции fmap f.• Композиция стрелок это просто композиция функций.• Тождественная стрелка это fmap id.Проверим аксиомы:

fmap f . fmap id = fmap f . id = fmap ffmap id . fmap f = id . fmap f = fmap f

fmap f . (fmap g . fmap h)= fmap f . fmap (g . h)= fmap (f . (g . h))= fmap ((f . g) . h)= fmap (f . g) . fmap h= (fmap f . fmap g) . fmap h

Функтор | 185

Page 186: Ru Haskell Book

Видно, что аксиомы выполнены, так функтор f порождает категорию f Hask. Интересно, что посколькуHask содержит все типы, то она содержит и типы f Hask. Получается, что мы построили категорию внутрикатегории. Это можно пояснить на примере списков. Тип [] погружает любой тип в список, а функцию длялюбого типа можно превратить в функцию, которая работает на списках с помощью метода fmap. При этом спомощью класса Functor мы проецируем все типы и все функции в мир списков [a]. Но сам этот мир списковсодержится в категории Hask.С помощью функторов мы строим внутри одной категории другую категорию, при этом внутренняя ка-

тегория обладает некоторой структурой. Так если раньше у нас были только произвольные типы a и произ-вольные функции a -> b, то теперь все объекты имеют тип [a] и все функции имеют тип [a] -> [b]. Также ифунктор Maybe переводит произвольное значение, в значение, которое обладает определённой структурой. Внём выделен дополнительный элемент Nothing, который обозначает отсутствие значения. Если по типу val:: a мы ничего не можем сказать о содержании значения val, то по типу val :: Maybe a, мы знаем одинуровень конструкторов. Например мы уже можем проводить сопоставление с образцом.Теперь давайте вернёмся к теории категорий и дадим формальное определение понятия. Пусть A и B –

категории, тогда функтором из A в B называют отображение F , которое переводит объекты A в объекты Bи стрелки A в стрелки B, так что выполнены следующие свойства:

Ff : FA→B FB если f : A→A BFidA = idFA для любого объекта A из AF (f ; g) = Ff ; Fg если (f ; g) подходят по типам

Здесь запись →A и →B означает, что эти стрелки в разных категориях. После отображения стрелки fиз категории A мы получаем стрелку в категории B, это и отражено в типе Ff : FA →B FB. Первоесвойство говорит о том, что после отображения стрелки соединяют те же объекты, что и до отображения.Второе свойства говорит о сохранении тождественных стрелок. А последнее свойство, говорит о том, что”пути” между объектами также сохраняются. Если мы находимся в категории A в объекте A и перед намиесть путь состоящий из нескольких стрелок в объект B, то неважно как мы пойдём в FB либо мы пройдёмэтот путь в категории A и в самом конце переместимся в FB или мы сначала переместимся в FA и затемпройдём по образу пути в категории FB. Так и так мы попадём в одно и то же место. Схематически этоможно изобразить так:

Af - B

g - C

FA

F

?

Ff- FB

F

?

Fg- FC

F

?

Стрелки сверху находятся в категорииA, а стрелки снизу находятся в категории B. Функтор F : A → A,который переводит категорию A в себя называют эндофунктором (endofunctor). Функторы отображают одникатегории в другие сохраняя структуру первой категории. Мы словно говорим, что внутри второй категорииесть структура подобная первой. Интересно, что последовательное применение функторов, также являетсяфунктором. Мы будем писать последовательное применение функторов F иG слитно, как FG. Также можноопределить и тождественный функтор, который ничего не делает с категорией, мы будем обозначать его какIA или просто I , если категория на которой он определён понятна из контекста. Это говорит о том, что мыможем построить категорию, в которой объектами будут другие категории, а стрелками будут функторы.

14.3 Естественное преобразованиеВ программировании часто приходится переводить данные из одной структуры в другую. Каждая из

структур хранит какие-то конкретные значения, но мы ничего с ними не делаем мы просто перекладываемсодержимое из одного ящика в другой. Например в нашем ящике только один отсек, но вдруг нам пришлобесконечно много подарков, что поделать нам приходится сохранить первый попавшийся, отбросив осталь-ные. Главное в этой аналогии это то, что мы ничего не меняем, а лишь перекладываем содержимое из однойструктуры в другую.В Haskell это можно описать так:

onlyOne :: [a] -> Maybe aonlyOne [] = NothingonlyOne (a:as) = Just a

186 | Глава 14: Теория категорий

Page 187: Ru Haskell Book

В этой функции мы перекладываем элементы из списка [a] в частично определённое значение Maybe.Тоже самое происходит и в функции concat:

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

Элементы перекладываются из списка списков в один список. В теории категорий этот процесс называ-ется естественным преобразованием. Структуры определяются функторами. Поэтому в определении будетучаствовать два функтора. В функции onlyOne это были функторы [] и Maybe. При перекладывании элемен-тов мы можем просто выбросить все элементы:

burnThemALl :: [a] -> ()burnThemAll = const ()

Можно сказать, что единичный тип также определяет функтор. Это константный функтор, он переводитлюбой тип в единственное значение (), а функцию в id:

data Empty a = Empty

instance Functor Empty wherefmap = const id

Тогда тип функции burnThemAll будет параметризован и слева и справа от стрелки:

burnThemAll :: [a] -> Empty aburnThemAll = const Empty

Пусть даны две категории A и B и два функтора F,G : A → B. Преобразованием (transformation) в B изF в G называют семейство стрелок ε:

εA : FA→B GA для любого A из AРассмотрим преобразование onlyOne :: [a] -> Maybe a. Категории A и B в данном случае совпадают –

это категория Hask. Функтор F – это список, а функтор G это Maybe. Преобразование onlyOne для каждогообъекта a из Hask определяет стрелку

onlyOne :: [a] -> Maybe a

Так мы получаем семейство стрелок, параметризованное объектом из Hask:

onlyOne :: [Int] -> Maybe IntonlyOne :: [Char] -> Maybe CharonlyOne :: [Int -> Int] -> Maybe (Int -> Int)......

Теперь давайте определим, что значит перекладывать из одной структуры в другую, не меняя содержа-ния. Представим, что функтор – это контейнер. Мы можем менять его содержание с помощью метода fmap.Например мы можем прибавить единицу ко всем элементам списка xs с помощью выражения fmap (+1) xs.Точно так же мы можем прибавить единицу к частично определённому значению. С точки зрения теории ка-тегорий суть понятия ”останется неизменным при перекладывании” заключается в том, что если мы возьмёмлюбую функцию к примеру прибавление единицы, то нам неважно когда её применять до функции onlyOneили после. И в том и в другом случае мы получим одинаковый ответ. Давайте убедимся в этом:

onlyOne $ fmap (+1) [1,2,3,4,5]=> onlyOne [2,3,4,5,6]=> Just 2

fmap (+1) $ onlyOne [1,2,3,4,5]=> fmap (+1) $ Just 1=> Just 2

Результаты сошлись, обратите внимание на то, что функции fmap (+1) в двух вариантах являются раз-ными функциями. Первая работает на списках, а вторая на частично определённых значениях. Суть в том,что если при перекладывании значение не изменилось, то нам не важно когда выполнять преобразованиевнутри функтора [] или внутри функтора Maybe. Теперь давайте выразим это на языке теории категорий.

Естественное преобразование | 187

Page 188: Ru Haskell Book

Преобразование ε в категории B из функтора F в функтор G называют естественным (natural), если

Ff ; εB = εA ;Gf для любого f : A→A B

Это свойство можно изобразить графически:

FAεA- GA

FB

Ff

?

εB- GB

Gf

?

По смыслу ясно, что если у нас есть три структуры данных (или три функтора), если мы просто пере-ложили данные из первой во вторую, а затем переложили данные из второй в третью, ничего не меняя. Тоитоговое преобразование, которое составлено из последовательного применения перекладывания данныхтакже не меняет данные. Это говорит о том, что композиция двух естественных преобразований также явля-ется естественным преобразованием. Также мы можем составить тождественное преобразование, для двуходинаковых функторов F : A → B, это будет семейство тождественных стрелок в категории B. Получает-ся, что для двух категорий A и B мы можем составить категорию Ftr(A,B), в которой объектами будутфункторы из A в B, а стрелками будут естественные преобразования. Поскольку естественные преобразова-ния являются стрелками, которые соединяют функторы, мы будем обозначать их как обычные стрелки. Такзапись η : F → G обозначает преобразование η, которое переводит функтор F в функтор G.Интересно, что изначально создатели теории категорий Саундедерс Маклейн и Сэмюэль Эйленберг при-

думали понятие естественного преобразования, а затем, чтобы дать ему обоснование было придумано поня-тие функтора, и наконец для того чтобы дать обоснование функторам были придуманы категории. Катего-рии содержат объекты и стрелки, для стрелок есть операция композиции. Также для каждого объекта естьтождественная стрелка. Функторы являются стрелками в категории, в которой объектами являются другиекатегории. А естественные преобразования являются стрелками в категории, в которой объектами являютсяфункторы. Получается такая иерархия структур.

14.4 МонадыМонадой называют эндофунктор T : A → A, для которого определены два естественных преобразования

η : I → T и µ : TT → T и выполнены два свойства:

• TηA ; µA = idTA

• TµA ; µTA = µTTA ; µA

Преобразование η – это функция return, а преобразование µ – это функция join. В теории категорий вклассе монад другие методы. Перепишем эти свойства в виде функций Haskell:join . fmap return = idjoin . fmap join = join . join

Порядок следования аргументов изменился, потому что мы пользуемся обычной композицией (черезточку). Выражение TηA означает применение функтора T к стрелке ηA. Ведь преобразование это семействострелок, которые параметризованы объектами категории. На языке Haskell это означает применить fmap кполиморфной функции (функции с параметром).Также эти свойства можно изобразить графически:

TATηA- TTA

µA- TA

TTTATµA - TTA

TTA

µTA

?

µA

- TA

µA

?

188 | Глава 14: Теория категорий

Page 189: Ru Haskell Book

Категория КлейслиЕсли у нас есть монада T , определённая в категории A, то мы можем построить в этой категории кате-

горию специальных стрелок вида A→ TB. Эту категорию называют категорией Клейсли.

• Объекты категории Клейсли AT – это объекты исходной категории A.• Стрелки в AT это стрелки из A вида A→ TB, мы будем обозначать их A→T B

• Композиция стрелок f : A→T B и g : B →T C определена с помощью естественных преобразованиймонады T :

f ;T g = f ; Tg ; µ

Значок ;T указывает на то, что слева от равно композиция в AT . Справа от знака равно используетсякомпозиция в исходной категории A.• Тождественная стрелка – это естественное преобразование η.

Графически композицию в категории Клейсли можно изобразить так:

Af - TB ;T B

g - TC

Af - TB

Tg- TTCµC- TC

Af ;T g- TC

Можно показать, что категория Клейсли действительно является категорией и свойства операций компо-зиции и тождества выполнены.

14.5 ДуальностьИнтересно, что если в категории A перевернуть все стрелки, то снова получится категория. Попробуйте

нарисовать граф со стрелками, и затем мысленно переверните направление всех стрелок. Все пути исход-ного графа перейдут в перевёрнутые пути нового графа. При этом пути будут проходить через те же точки.Сохранятся композиции стрелок, только все они будут перевёрнуты. Такую категорию обозначают Aop. Нооказывается, что переворачивать мы можем не только категории но и свойства категорий, или утвержденияо категориях, эту операцию называют дуализацией. Определим её:

dual A = A если A является объектомdual x = x если x обозначает стрелкуdual (f : A→ B) = dual f : B → A A и B поменялись местамиdual (f ; g) = dual g ; dual f f и g поменялись местамиdual (idA) = idA

Есть такое свойство, если и в исходной категории A выполняется какое-то утверждение, то в перевёр-нутой категории Aop выполняется перевёрнутое (дуальное) свойство. Часто в теории категорий из однихпонятий получают другие дуализацией. При этом мы можем не проверять свойства для нового понятия,они будут выполняться автоматически. К дуальным понятиям обычно добавляют приставку ”ко”. Приведёмпример, получим понятие комонады.Для начала вспомним определение монады. Монада – это эндофунктор (функтор, у которого совпадают

начало и конец или домен и кодомен) T : A → A и два естественных преобразования η : I → T иµ : TT → T , такие что выполняются свойства:

• Tη ; µ = id

• Tµ ; µ = µ ; µ

Дуализируем это определение. Комонада – это эндофунктор T : A → A и два естественных преобразо-вания η : T → I и µ : TT → T , такие что выполняются свойства

• µ ; Tη = id

Дуальность | 189

Page 190: Ru Haskell Book

• µ ; Tµ = µ ; µ

Мы просто переворачиваем домены и кодомены в стрелках и меняем порядок в композиции. Проверьтесошлись ли типы. Попробуйте нарисовать графическую схему свойств комонады и сравните со схемой длямонады.Можно также определить и категорию коКлейсли. В категории коКлейсли все стрелки имеют вид TA→

B. Теперь дуализируем композицию из категории Клейсли:

f ;T g = f ; Tg ; µ

Теперь получим композицию в категории коКлейсли:

g ;T f = µ ; Tg ; f

Мы перевернули цепочки композиций слева и справа от знака равно. Проверьте сошлись ли типы. Незабывайте что в этом определении η и µ естественные преобразования для комонады. Нам не нужно прове-рять является ли категория коКлейсли действительно категорией. Нам не нужно опять проверять свойствастрелки тождества и ассоциативности композиции, если мы уже проверили их для монады. Следовательноперевёрнутое утверждение будет выполняться в перевёрнутой категории коКлейсли. В этом основное пре-имущество определения через дуализацию.Этим приёмом мы можем воспользоваться и в Haskell, дуализируем класс Monad:

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

Перевернём все стрелки:

class Comonad c wherecoreturn :: c a -> acobind :: c b -> (c b -> a) -> c a

14.6 Начальный и конечный объектыНачальный объектПредставим, что в нашей категории есть такой объект 0, который соединён со всеми объектами. При-

чём стрелка начинается из этого объекта и для каждого объекта может быть только одна стрелка котораясоединят данный объект с 0. Графически эту ситуацию можно изобразить так:

. . . A1 A2

. . . � 0

6

-

-�

A3

. . .�

. . .?

A4

-

Такой объект называют начальным (initial object). Его принято обозначать нулём, словно это начало от-счёта. Для любого объекта A из категории A с начальным объектом 0 существует и только одна стрел-ка f : 0 → B. Можно сказать, что начальный объект определяет функцию, которая переводит объекты A встрелки f : 0 → A. Эту функцию обозначают специальными скобками L · M, она называется катаморфизмом(catamorphism).

LA M = f : 0 → A

У начального объекта есть несколько важных свойств. Они очень часто встречаются в разных вариациях,в понятиях, которые определяются через понятие начального объекта:

190 | Глава 14: Теория категорий

Page 191: Ru Haskell Book

L 0 M = id0 тождествоf, g : 0 → A ⇒ f = g уникальностьf : A→ B ⇒ LA M ; f = LB M слияние (fusion)

Эти свойства следуют из определения начального объекта. Свойство тождества говорит о том, что стрелкаведущая из начального объекта в начальный является тождественной стрелкой. В самом деле по определе-нию начального объекта для каждого объекта может быть только одна стрелка, которая начинается в 0 изаканчивается в этом объекте. Стрелка L 0 M начинается в 0 и заканчивается в 0, но у нас уже есть одна та-кая стрелка, по определению категории для каждого объекта определена тождественная стрелка, значит этастрелка является единственной.Второе свойство следует из единственности стрелки, ведущей из начального объекта в данный. Третье

свойство лучше изобразить графически:

Af - B

LA M LB M0

-�

Поскольку стрелки LA M и f можно соединить, то должна быть определена стрелка LA M ; f : 0 → B, нопоскольку в категории с начальным объектом из начального объекта 0 в объект B может вести лишь однастрелка, то стрелка LA M ; f должна совпадать с LB M.Конечный объектДуализируем понятие начального объекта. Пусть в категории A есть объект 1, такой что для любого

объектаA существует и только одна стрелка, которая начинается из этого объекта и заканчивается в объекте1. Такой объект называют конечным (terminal object):

. . . A1 A2

. . . - 1?��

-

A3

. . .

-

. . .

6

A4

Конечный объект определяет в категории функцию, которая ставит в соответствие объектам стрелки,которые начинаются из данного объекта и заканчиваются в конечном объекте. Такую функцию называютанаморфизмом (anamorphism), и обозначают специальными скобками [( · )], которые похожи на перевёрнутыескобки для катаморфизма:

[(A )] = f : A→ 1

Можно дуализировать и свойства:

[( 1 )] = id1 тождествоf, g : A→ 1 ⇒ f = g уникальностьf : A→ B ⇒ f ; [(B )] = [(A )] слияние (fusion)

Приведём иллюстрацию для свойства слияния:

Af - B

[(A )] [(B )]

1�

-

Начальный и конечный объекты | 191

Page 192: Ru Haskell Book

14.7 Сумма и произведениеДавным-давно, когда мы ещё говорили о типах, мы говорили, что типы конструируются с помощью двух

базовых операций: суммы и произведения. Сумма говорит о том, что значение может быть либо одним зна-чением либо другим. А произведение обозначает сразу несколько значений. В Haskell есть два типа, которыепредставляют собой сумму и произведение в общем случае. Тип для суммы это Either:

data Either a b = Left a | Right b

Произведение в самом общем виде представлено кортежами:

data (a, b) = (a, b)

В теории категорий сумма и произведение определяются как начальный и конечный объекты в специаль-ных категориях. Теория категорий изучает объекты по тому как они взаимодействуют с остальными объек-тами. Взаимодействие обозначается с помощью стрелок. Специальные свойства стрелок определяют объект.Например представим, что мы не можем заглядывать внутрь суммы типов, как бы мы могли взаимодей-

ствовать с объектом, который представляет собой сумму двух типовA+B? Нам необходимо уметь создаватьобъект типа A + B из объектов A и B извлекать их из суммы. Создание объектов происходит с помощьюдвух специальных конструкторов:

inl : A→ A+B

inr : B → A+B

Также нам хочется уметь как-то извлекать значения. По смыслу внутри суммыA+B хранится либо объектA либо объект B и мы не можем заранее знать какой из них, поскольку внутреннее содержание A + B отнас скрыто, но мы знаем, что это только A или B. Это говорит о том, что если у нас есть две стрелки A→ Cи B → C, то мы как-то можем построить A+B → C. У нас есть операция:

out(f, g) : A+B → C f : A→ C, g : B → C

При этом для того, чтобы стрелки inl, inr и out были согласованы необходимо, чтобы выполнялисьсвойства:

inl ; out(f, g) = f

inr ; out(f, g) = g

Для любых функций f и g. Графически это свойство можно изобразить так:

Ainl- A+B �inr B

f g

C

out.....?

.....

�-

Итак суммой двух объектов A и B называется объект A+B и две стрелки inl : A→ A+B и inr : B →A+ B такие, что для любых двух стрелок f : A → C и g : B → C определена одна и только одна стрелкаh : A+B → C такая, что выполнены свойства:

inl ; h = f

inr ; h = g

В этом определении объект A + B вместе со стрелками inl и inr, определяет функцию, которая понекоторому объекту C и двум стрелкам f и g строит стрелку h, которая ведёт из объекта A + B в объектC. Этот процесс определения стрелки по объекту напоминает определение начального элемента. Построимспециальную категорию, в которой объектA+B будет начальным. Тогда функция out будет катаморфизмом.Функция out принимает две стрелки и возвращает третью. Посмотрим на типы:

f : A→ C inl : A→ A+B

g : B → C inr : B → A+B

192 | Глава 14: Теория категорий

Page 193: Ru Haskell Book

Каждая из пар стрелок в столбцах указывают на один и тот же объект, а начинаются они из двух разныхобъектов A и B. Определим категорию, в которой объектами являются пары стрелок (a1, a2), которые на-чинаются из объектов A и B и заканчиваются в некотором общем объекте D. Эту категорию ещё называютклином. Стрелками в этой категории будут такие стрелки f : (d1, d2) → (e1, e2), что стрелки в следующейдиаграмме коммутируют (не важно по какому пути идти из двух разных точек).

A B

e1 d2

D

d1

?

f-

�E

e2

?-

Композиция стрелок – это обычная композиция в исходной категории, в которой определены объекты Aи B, а тождественная стрелка для каждого объекта, это тождественная стрелка для того объекта, в которомсходятся обе стрелки. Можно проверить, что это действительно категория.Если в этой категории есть начальный объект, то мы будем называть его суммой объектов A и B. Две

стрелки, которые содержит этот объект мы будем называть inl и inr, а общий объект в котором эти стрелкисходятся будем называть A + B. Теперь если мы выпишем определение для начального объекта, но вме-сто произвольных стрелок и объектов подставим наш конкретный случай, то мы получим как раз исходноеопределение суммы.Начальный объект (inl : A → A + B, inr : B → A + B) ставит в соответствие любому объекту

(f : A→ C, g : B → C) стрелку h : A+B → C такую, что выполняются свойства:

Ainl- A+B �inr B

f g

C

h.....?

.....

�-

А как на счёт произведения? Оказывается, что произведение является дуальным понятием по отношениюк сумме. Его иногда называют косуммой, или сумму называют копроизведением. Дуализируем категорию,которую мы строили для суммы.У нас есть категория A и в ней выделено два объекта A и B. Объектами новой категории будут пары

стрелок (a1, a2), которые начинаются в общем объекте C а заканчиваются в объектах A и B. Стрелками вэтой категории будут стрелки исходной категории h : (e1, e2) → (d1, d2) такие что следующая диаграммакоммутирует:

A B

e1 d2

D

d1

6

�f

-

E

e2

6�

Композиция и тождественные стрелки позаимствованы из исходной категории A. Если в этой категориисуществует конечный объект. То мы будем называть его произведением объектов A и B. Две стрелки этогообъекта обозначаются как (exl, exr), а общий объект из которого они начинаются мы назовём A×B. Теперьраспишем определение конечного объекта для нашей категории пар стрелок с общим началом.Конечный объект (exl : A×B → A, exr : A×B → B) ставит в соответствие любому объекту категории

(f : C → A, g : C → B) стрелку h : C → A×B. При этом выполняются свойства:

A � exlA×B

exr- B

f g

C

h.....

6..... -

Итак мы определили сумму, а затем на автомате, перевернув все утверждения, получили определениепроизведения. Но что это такое? Соответствует ли оно интуитивному понятию произведения?

Сумма и произведение | 193

Page 194: Ru Haskell Book

Так же как и в случае суммы в теории категорий мы определяем понятие, через то как мы можем с нимвзаимодействовать. Посмотрим, что нам досталось от абстрактного определения. У нас есть обозначениепроизведения типов A × B. Две стрелки exl и exr. Также у нас есть способ получить по двум функциямf : C → A и g : C → B стрелку h : C → A×B. Для начала посмотрим на типы стрелок конечного объекта:

exl : A×B → A

exr : A×B → B

По типам видно, что эти стрелки разбивают пару на составляющие. По смыслу произведения мы точнознаем, что у нас есть в A × B и объект A и объект B. Эти стрелки позволяют нам извлекать компонентыпары. Теперь посмотрим на анаморфизм:

[( f, g )] : C → A×B f : C → A, g : C → B

Эта функция позволяет строить пару по двум функциям и начальному значению. Но, поскольку здесь мыничего не вычисляем, а лишь связываем объекты, мы можем по паре стрелок, которые начинаются из общегоисточника связать источник с парой конечных точек A×B.При этом выполняются свойства:

[( f, g )] ; exl = f

[( f, g )] ; exr = g

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

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

циями одного аргумента. Ведь у стрелки есть только один источник. Как быть если мы хотим определитьфункцию нескольких аргументов, что она связывает? Если в нашей категории определено произведение объ-ектов, то мы можем представить функцию двух аргументов, как стрелку, которая начинается из произведе-ния:

(+) : Num×Num→ Num

Но в лямбда-исчислении нам были доступны более гибкие функции, функции могли принимать на входфункции и возвращать функции. Как с этим обстоят дела в теории категорий? Если перевести определениефункций высшего порядка на язык теории категорий, то мы получим стрелки, которые могут связывать дру-гие стрелки. Категория с функциями высшего порядка может содержать свои стрелки в качестве объектов.Стрелки как объекты обозначаются с помощью степени, так запись BA означает стрелку A → B. При этомнам необходимо уметь интерпретировать стрелку, мы хотим уметь подставлять значения. Если у нас естьобъект BA, то должна быть стрелка

eval : BA ×A→ B

На языке функций можно сказать, что стрелка eval принимает функцию высшего порядка A→ B и зна-чение типа A, а возвращает значение типа B. Объект BA называют экспонентой. Теперь дадим формальноеопределение.Пусть в категории A определено произведение. Экспонента – это объект BA вместе со стрелкой eval :

BA × A → B такой, что для любой стрелки f : C × A → B определена стрелка curry(f) : C → BA приэтом следующая диаграмма коммутирует:

C C ×A

f

BA

curry(f)

?BA ×A

(curry(f), id)

?- B

-

Давайте разберёмся, что это всё означает. По смыслу стрелка curry(f) это каррированная функция двухаргументов. Вспомните о функции curry из Haskell. Диаграмма говорит о том, что если мы каррированием

194 | Глава 14: Теория категорий

Page 195: Ru Haskell Book

функции двух аргументов получим функцию высшего порядка C → BA, а затем с помощью функции evalполучим значение, то это всё равно, что подставить два значения в исходную функцию. Запись (curry(f), id)означает параллельное применение двух стрелок внутри пары:

(f, g) : A×A′ → B ×B′, f : A→ B, g : A′ → B′

Так применив стрелки curry(f) : C → BA и id : A → A к паре C × A, мы получим пару BA × A.Применение здесь условное мы подразумеваем применение в функциональной аналогии, в теории категорийпроисходит связывание пар объектов с помощью стрелки (f, g).Интересно, что и экспоненту можно получить как конечный объект в специальной категории. Пусть есть

категория A и в ней определено произведение объектов A и B. Построим категорию, в которой объектамиявляются стрелки вида:

C ×A→ B

где C – это произвольный объект исходной категории. Стрелкой между объектами c : C × A → B и d :D × A → B в этой категории будет стрелка f : C → D из исходной категории, такая, что следующаядиаграмма коммутирует:

C C ×A

c

D

f

?D ×A

(f, id)

?

d- B

-

Если в этой категории существует конечный объект, то он является экспонентой. А функция curry явля-ется анаморфизмом для экспоненты.

14.9 Краткое содержаниеТеория категорий изучает понятия через то как эти понятия взаимодействуют друг с другом. Мы забываем

о том, как эти понятия реализованы, а смотрим лишь на свойства связей.Мы узнали что такое категория. Категория это структура с объектами и стрелками. Стрелки связывают

объекты. Причём связи могут соединятся. Также считается, что объект всегда связан сам с собой. Мы узнали,что есть такие категории, в которых сами категории являются объектами, а стрелки в таких категориях мыназвали функторами. Также мы узнали, что сами функторы могут стать объектами в некоторой категории,тогда стрелки в этой категории мы будем называть естественными преобразованиями.Мы узнали что такое начальный и конечный объект и как с помощью этих понятий можно определить

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

14.10 Упражнения• Проверьте аксиомы категории (ассоциативность и тождество) для категории функторов и категорииестественных преобразований.• Изоморфизмом называют такие стрелки f : A→ B и g : B → A, для которых выполнено свойство:

f ; g = idA

g ; f = idB

ОбъектыA иB называют изоморфными, если они связаны изоморфизмом, это обозначают так:A ∼= B.Докажите, что все начальные и конечные элементы изоморфны.• Поскольку сумма и произведение типов являются начальным и конечным объектами в специальныхкатегориях для них также выполняются свойства тождества, уникальности и слияния. Выпишите этисвойства для суммы и произведения.• Подумайте как можно определить экземпляр класса Comonad для потоков:

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

Page 196: Ru Haskell Book

data Stream a = a :& Stream a

Можно ли придумать экземпляр для класса Monad?• Дуальную категорию для категории A обозначают Aop. Если F является функтором в категории Aop,то в исходной категории его называют контравариантным функтором. Выпишите определение функто-ра в Aop, а затем с помощью дуализации получите свойства контравариантного функтора в исходнойкатегории A.

196 | Глава 14: Теория категорий

Page 197: Ru Haskell Book

Глава 15

Категориальные типы

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

15.1 Программирование в стиле оригамиОригами – состоит из двух слов ”свёртка” и ”бумага”. При программировании в стиле оригами все функ-

ции строятся через функции свёртки и развёртки. Есть даже такие языки программрования, в которых этоединственный способ определения рекурсии. Этот стиль очень хорошо подходит для ленивых языков про-граммирования, поскольку в связке:

fold f . unfold g

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

fix. Например так выглядит рекурсивная функция сложения всех чисел от одного до n:

sumInt :: Int -> IntsumInt 0 = 0sumInt n = n + sumInt (n-1)

Эту функцию мы можем переписать с помощью функции fix. При вычислении fix f будет составленозначение

f (f (f (f ...)))

Теперь перепишем функцию sumInt через fix:

sumInt = fix $ \f n ->case n of

0 -> 0n -> n + f (n - 1)

Смотрите лямбда функция в аргументе fix принимает функцию и число, а возвращает число. Тип этойфункции (Int -> Int) -> (Int -> Int). После применения функции fix мы как раз и получим функциютипа Int -> Int. В лямбда функции рекурсивный вызов был заменён на вызов функции-параметра f.Оказывается, что этот приём может быть применён и для рекурсивных типов данных. Мы можем создать

обобщённый тип, который обозначает рекурсивный тип:

newtype Fix f = Fix { unFix :: f (Fix f) }

В этой записи мы получаем уравнение неподвижной точки Fix f = f (Fix f), где f это некоторый типс параметром. Определим тип целых чисел:

data N a = Zero | Succ a

type Nat = Fix N

| 197

Page 198: Ru Haskell Book

Теперь создадим несколько конструкторов:zero :: Natzero = Fix Zero

succ :: Nat -> Natsucc = Fix . Succ

Сохраним эти определения в модуле Fix.hs и посмотрим в интерпретаторе на значения и их типы, ghc несможет вывести экземпляр Show для типа Fix, потому что он зависит от типа с параметром, а не от конкретно-го типа. Для решения этой проблемы нам придётся определить экземпляры вручную и подключить несколькорасширений языка. Помните в главе о ленивых вычислениях мы подключали расширение BangPatterns? Нампонадобятся:{-# Language FlexibleContexts, UndecidableInstances #-}

Теперь определим экземпляры для Show и Eq:instance Show (f (Fix f)) => Show (Fix f) where

show x = ”(” ++ show (unFix x) ++ ”)”

instance Eq (f (Fix f)) => Eq (Fix f) wherea == b = unFix a == unFix b

Определим списки-оригами:data L a b = Nil | Cons a b

deriving (Show)

type List a = Fix (L a)

nil :: List anil = Fix Nil

infixr 5 ‘cons‘

cons :: a -> List a -> List acons a = Fix . Cons a

В типе L мы заменили рекурсивный тип на параметр. Затем в записи List a = Fix (L a) мы произ-водим замыкание по параметру. Мы бесконечно вкладываем тип L a во второй параметр. Так получаетсярекурсивный тип для списков. Составим какой-нибудь список:*Fix> :r[1 of 1] Compiling Fix ( Fix.hs, interpreted )Ok, modules loaded: Fix.*Fix> 1 ‘cons‘ 2 ‘cons‘ 3 ‘cons‘ nil(Cons 1 (Cons 2 (Cons 3 (Nil))))

Спрашивается, зачем нам это нужно? Зачем нам записывать рекурсивные типы через тип Fix? Оказыва-ется при такой записи мы можем построить универсальные функции fold и unfold, они будут работать длялюбого рекурсивного типа.Помните как мы составляли функции свёртки? Мы строили воображаемый класс, в котором сворачивае-

мый тип заменялся на параметр. Например для списка мы строили свёртку так:class [a] b where

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

После этого мы легко получали тип для функции свёртки:foldr :: (a -> b -> b) -> b -> ([a] -> b)

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

мы строим типы с параметром, а затем получаем из них рекурсивные типы с помощью конструкции Fix.Теперь методы класса с параметром это наши конструкторы исходных классов, а рекурсивный тип записанчерез Fix. Если мы сопоставим два способа, то мы сможем получить такой тип для функции свёртки:

198 | Глава 15: Категориальные типы

Page 199: Ru Haskell Book

fold :: (f b -> b) -> (Fix f -> b)

Смотрите функция свёртки по-прежнему принимает методы воображаемого класса с параметром, но те-перь класс перестал быть воображаемым, он стал типом с параметром. Результатом функции свёртки будетфункция из рекурсивного типа Fix f в тип параметр.Аналогично строится и функция unfold:

unfold :: (b -> f b) -> (b -> Fix f)

В первой функции мы указываем один шаг разворачивания рекурсивного типа, а функция развёрткирекурсивно распространяет этот один шаг на потенциально бесконечную последовательность примененийэтого одного шага.Теперь давайте определим эти функции. Но для этого нам понадобится от типа f одно свойство. Он

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

fold :: Functor f => (f a -> a) -> (Fix f -> a)fold f = f . fmap (fold f) . unFix

Проверим эту функцию по типам. Для этого нарисуем схему композиции:

Fix funFix - f (Fix f)

fmap (fold f) - f af - a

Сначала мы разворачиваем обёртку Fix и получаем значение типа f (Fix f), затем с помощью fmap мывнутри типа f рекурсивно вызываем функцию свёртки и в итоге получаем значение f a, на последнем шагемы выполняем свёртку на текущем уровне вызовом функции f.Аналогично определяется и функция unfold. Только теперь мы сначала развернём первый уровень, затем

рекурсивно вызовем развёртку внутри типа f и только в самом конце завернём всё в тип Fix:

unfold :: Functor f => (a -> f a) -> (a -> Fix f)unfold f = Fix . fmap (unfold f) . f

Схема композиции:

Fix f � Fixf (Fix f) �fmap (unfold f)

f a � fa

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

L и N экземпляром класса Functor:

instance Functor N wherefmap f x = case x of

Zero -> ZeroSucc a -> Succ (f a)

instance Functor (L a) wherefmap f x = case x of

Nil -> NilCons a b -> Cons a (f b)

Это всё что нам нужно для того чтобы начать пользоваться функциями свёртки и развёртки! Определимэкземпляр Num для натуральных чисел:

instance Num Nat where(+) a = fold $ \x -> case x of

Zero -> aSucc x -> succ x

(*) a = fold $ \x -> case x ofZero -> zeroSucc x -> a + x

Программирование в стиле оригами | 199

Page 200: Ru Haskell Book

fromInteger = unfold $ \n -> case n of0 -> Zeron -> Succ (n-1)

abs = undefinedsignum = undefined

Сложение и умножение определены через свёртку, а функция построения натурального числа из чис-ла типа Integer определена через развёртку. Сравните с теми функциями, которые мы писали в главе проструктурную рекурсию. Теперь мы не передаём отдельно две функции, на которые мы будем заменять кон-структоры. Эти функции закодированы в типе с параметром. Для того чтобы этот код заработал нам придётсядобавить ещё одно расширение TypeSynonymInstances наши рекурсивные типы являются синонимами, а неновыми типами. В рамках стандарта Haskell мы можем определять экземпляры только для новых типов, длятого чтобы обойти это ограничение мы добавим ещё одно расширение.

*Fix> succ $ 1+2(Succ (Succ (Succ (Succ (Zero)))))*Fix> ((2 * 3) + 1) :: Nat(Succ (Succ (Succ (Succ (Succ (Succ (Succ (Zero))))))))*Fix> 2+2 == 2*(2::Nat)True

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

headL :: List a -> aheadL x = case unFix x of

Nil -> error ”empty list”Cons a _ -> a

tailL :: List a -> List atailL x = case unFix x of

Nil -> error ”empty list”Cons a b -> b

Теперь определим несколько новых функций:

mapL :: (a -> b) -> List a -> List bmapL f = fold $ \x -> case x of

Nil -> nilCons a b -> f a ‘cons‘ b

takeL :: Int -> List a -> List atakeL = curry $ unfold $ \(n, xs) ->

if n == 0 then Nilelse Cons (headL xs) (n-1, tailL xs)

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

*Fix> :r[1 of 1] Compiling Fix ( Fix.hs, interpreted )Ok, modules loaded: Fix.*Fix> takeL 3 $ iterateL (+1) zero(Cons (Zero) (Cons (Succ (Zero)) (Cons (Succ (Succ (Zero))) (Nil))))*Fix> let x = 1 ‘cons‘ 2 ‘cons‘ 3 ‘cons‘ nil*Fix> mapL (+10) $ x ‘concatL‘ x(Cons 11 (Cons 12 (Cons 13 (Cons 11 (Cons 12 (Cons 13 (Nil)))))))

Обратите внимание, на то что с большими буквами мы пишем Cons и Nil когда хотим закодироватьфункции для свёртки-развёртки, а с маленькой буквы пишем значения рекурсивного типа. Надеюсь, что выразобрались на примерах как устроены функции fold и unfold, потому что теперь мы перейдём к теории,которая за этим стоит.

200 | Глава 15: Категориальные типы

Page 201: Ru Haskell Book

15.2 Индуктивные и коиндуктивные типыС точки зрения теории категорий функция свёртки является катаморфизмом, а функция развёртки – ана-

морфизмом. Напомню, что катаморфизм – это функция которая ставит в соответствие объектам категориис начальным объектом стрелки, которые начинаются из начального объекта, а заканчиваются в данном объ-екте. Анаморфизм – это перевёрнутый наизнанку катаморфизм.Начальным и конечным объектом будет рекурсивный тип. Вспомним тип свёртки:

fold :: Functor f => (f a -> a) -> (Fix f -> a)

Функция свёртки строит функции, которые ведут из рекурсивного типа в произвольный тип, поэтому вданном случае рекурсивный тип будет начальным объектом. Функция развёртки строит из произвольноготипа данный рекурсивный тип, на языке теории категорий она строит стрелку из произвольного объекта врекурсивный, это означает что рекурсивный тип будет конечным объектом.unfold :: Functor f => (a -> f a) -> (a -> Fix f)

Категории, которые определяют рекурсивные типы таким образом называются (ко)алгебрами функторов.Видите в типе и той и другой функции стоит требование о том, что f является функтором. Катаморфизм ианаморфизм отображают объекты в стрелки. По типу функций fold и unfold мы можем сделать вывод, чтообъектами в нашей категории будут стрелки видаf a -> a

или для свёрток:a -> f a

А стрелками будут обычные функции одного аргумента. Теперь дадим более формальное определение.Эндофунктор F : A → A определяет стрелки α : FA → A, которые называется F -алгебрами. Стрелку

h : A→ B называют F -гомоморфизмом, если следующая диаграмма коммутирует:

FAα - A

FB

Fh

?

β- B

h

?

Или можно сказать по другому, для F -алгебр α : FA→ A и β : FB → B выполняется:

Fh ; β = α ; hЭто свойство совпадает со свойством естественного преобразования только вместо одного из функторов

мы подставили тождественный функтор I . Определим категорию Alg(F ), для категории A и эндофунктораF : A → A

• Объектами являются F -алгебры FA→ A, где A – объект категории A• Два объекта α : FA → A и β : FB → B соединяет F -гомоморфизм h : A→ B. Это такая стрелка изA, для которой выполняется:

Fh ; β = α ; h

• Композиция и тождественная стрелка взяты из категории A.Если в этой категории есть начальный объект inF : FT → T , то определён катаморфизм, который

переводит объекты FA→ A в стрелки T → A. Причём следующая диаграмма коммутирует:

FTinF - T

FA

F Lα M?

α- A

Lα M?

Этот катаморфизм и будет функцией свёртки для рекурсивного типа . ПонятиеAlg(F )можно перевернутьи получить категорию CoAlg(F ).

Индуктивные и коиндуктивные типы | 201

Page 202: Ru Haskell Book

• Объектами являются F -коалгебры A→ FA, где A – объект категории A• Два объекта α : FA → A и β : FB → B соединяет F -когомоморфизм h : A→ B. Это такая стрелкаиз A, для которой выполняется:

h ; α = β ; Fh

• Композиция и тождественная стрелка взяты из категории A.

Если в этой категории есть конечный объект, его называют outF : T → FT , то определён анаморфизм,который переводит объекты A→ FA в стрелки A→ T . Причём следующая диаграмма коммутирует:

TinF- FT

A

[(α )]

?

α- FA

F [(α )]

?

Если для категорииA и функтораF определены стрелки inF и outF , то они являются взаимнообратнымии определяют изоморфизм T ∼= FT . Часто объект T в случае Alg(F ) обозначают µF , поскольку начальныйобъект определяется функтором F , а в случае CoAlg(F ) обозначают νF .Типы, которые являются начальными объектами, принято называть индуктивными, а типы, которые яв-

ляются конечными объектами – коиндуктивными.

Существование начальных и конечных объектовМы говорили, что если начальный(конечный) объект существует, а когда он существует? Рассмотрим

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

Полные частично упорядоченные множества

Оказывается на значениях можно ввести частичный порядок. Порядок называется частичным, если отно-шение ≤ определено не для всех элементов, а лишь для некоторых из них. Частичный порядок на значенияхотражает степень неопределённости значения. Самый маленький объект это полностью неопределённое зна-чение ⊥. Любое значение типа содержит больше определённости чем ⊥.Для того чтобы не путать упорядочивание значений по степени определённости с обычным числовым

порядком, пользуются специальным символом ⊑. Запись

a ⊑ b

означает, что b более определено (или информативнее) чем a.Так для логических значений определены два нетривиальных сравнения:

data Bool = True | False

⊥ ⊑ True

⊥ ⊑ False

Мы будем называть нетривиальными сравнения в которых, компоненты слева и справа от⊑ не равны. На-пример ясно, что True ⊑ True или⊥ ≤ ⊥. Это тривиальные сравнения и мы их будем лишь подразумевать.Считается, что если два значения определены полностью, то мы не можем сказать какое из них информатив-нее. Так к примеру для логических значений мы не можем сказать какое значение более определено Trueили False.Рассмотрим пример по-сложнее. Частично определённые значения:

data Maybe a = Nothing | Just a

202 | Глава 15: Категориальные типы

Page 203: Ru Haskell Book

⊥ ⊑ Nothing⊥ ⊑ Just ⊥⊥ ⊑ Just aJust a ⊑ Just b, если a ⊑ b

Если вспомнить как происходит вычисление значения, то значение a менее определено чем b, если взрыв-ное значение⊥ в a находится ближе к корню значения, чем в b. Итак получается, что в категории Hask объек-ты это множества с частичным порядком. Что означает требование монотонности функции? Монотонность вконтексте операции⊑ говорит о том, что чем больше определён вход функции тем больше определён выход:

a ⊑ b ⇒ f a ⊑ f b

Это требование накладывает запрет на возможность проведения сопоставления с образцом по значению⊥. Иначе мы можем определять немонотонные функции вроде:

isBot :: Bool -> BoolisBot undefined = TrueisBot _ = undefined

Полнота частично упорядоченного множества означает, что у любой последовательности xn

x0 ⊑ x1 ⊑ x2 ⊑ . . .

есть значение x, к которому она сходится. Это значение называют супремумом множества. Что такое полныечастично упорядоченные множества мы разобрались. А что такое полиномиальный функтор?

Полиномиальный функтор

Полиномиальный функтор – это функтор который построен лишь с помощью операций суммы, произве-дения, постоянных функторов, тождественного фуктора и композиции функторов. Определим эти операции:

• Сумма функторов F и G определяется через операцию суммы объектов:

(F +G)X = FX +GX

• Произведение функторов F и G определяется через операцию произведения объектов:

(F ×G)X = FX ×GX

• Постоянный функтор отображает все объекты категории в один объект, а стрелки в тождественнубюстрелку этого объекта, мы будем обозначать постоянный функтор подчёркиванием:

AX = A

Af = idA

• Тождественный функтор оставляет объекты и стрелки неизменными:

IX = X

If = f

• Композиция функторов F и G это последовательное применение функторов

FGX = F (GX)

Индуктивные и коиндуктивные типы | 203

Page 204: Ru Haskell Book

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

Bool = µ(1 + 1)

Объект 1 обозначает любую константу, это конечный объект исходной категории. Нам не важны именаконструкторов, но важна структура типа. µ обозначает начальный объект в F -алгебре.Определим натуральные числа:

Nat = µ(1 + I)

Эта запись обозначает начальный объект для F -алгебры с функтором F = 1 + I . Посмотрим на опреде-ление списка:

ListA = µ(1 +A× I)

Список это начальный объект F -алгебры 1 +A× I . Также можно определить бинарные деревья:

BTreeA = µ(A+ I × I)

Определим потоки:

StreamA = ν(A× I)

Потоки являются конечным объектом F -коалгебры, где F = A× I .

15.3 ХиломорфизмОказывается, что с помощью катаморфизма и анаморфизма мы можем определить функцию fix, т.е. мы

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

f (f (f ...)))

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

repeat f = f : f : f : ...

А затем заменим конструктор : на применение. В итоге мы получим такую функцию:

fix :: (a -> a) -> afix = foldr ($) undefined . repeat

Убедимся, что эта функция работает:

Prelude> let fix = foldr ($) undefined . repeatPrelude> take 3 $ y (1:)[1,1,1]Prelude> fix (\f n -> if n==0 then 0 else n + f (n-1)) 1055

Теперь давайте определим функцию fix через функции cata и ana:

fix :: (a -> a) -> afix = cata (\(Cons f a) -> f a) . ana (\a -> Cons a a)

Эта связка анаморфизм с последующим катаморфизмом встречается так часто, что ей дали специальноеимя. Хиломорфизмом называют функцию:

hylo :: Functor f => (f b -> b) -> (a -> f a) -> (a -> b)hylo phi psi = cata phi . ana psi

Отметим, что эту функцию можно выразить и по-другому:

204 | Глава 15: Категориальные типы

Page 205: Ru Haskell Book

hylo :: Functor f => (f b -> b) -> (a -> f a) -> (a -> b)hylo phi psi = phi . (fmap $ hylo phi psi) . psi

Этот вариант более эффективен по расходу памяти, мы не строим промежуточное значение Fix f, а сразуобрабатываем значения в функции phi по ходу их построения в функции psi. Давайте введём инфикснуюоперацию хиломорфизм для этого определения:(>>) :: Functor f => (a -> f a) -> (f b -> b) -> (a -> b)psi >> phi = phi . (fmap $ hylo phi psi) . psi

Теперь давайте скроем одноимённую функцию из Prelude и определим несколько рекурсивных функцийс помощью хиломорфизма. Начнём с функции вычисления суммы чисел от нуля до данного числа:sumInt :: Int -> IntsumInt = range >> sum

sum x = case x ofNil -> 0Cons a b -> a + b

range n| n == 0 = Nil| otherwise = Cons n (n-1)

Сначала мы создаём в функции range список всех чисел от данного числа до нуля. А затем в функцииsum складываем значения. Теперь мы можем легко определить функцию вычисления факториала:fact :: Int -> Intfact = range >> prod

prod x = case x ofNil -> 1Cons a b -> a * b

Напишем функцию, которая извлекает из потока n-тый элемент. Сначала определим тип для потока:type Stream a = Fix (S a)

data S a b = a :& bderiving (Show, Eq)

instance Functor (S a) wherefmap f (a :& b) = a :& f b

headS :: Stream a -> aheadS x = case unFix x of

(a :& _) -> a

tailS :: Stream a -> Stream atailS x = case unFix x of

(_ :& b) -> b

Теперь функцию извлечения элемента:getElem :: Int -> Stream a -> agetElem = curry (enum >> elem)

where elem ((n, a) :& next)| n == 0 = a| otherwise = next

enum (a, st) = (a, headS st) :& (a-1, tailS st)

В функции enum мы добавляем к элементам потока убывающую последовательность чисел, она стартуетиз данного числа. Элемент, который нам нужен, будет содержать в этой последовательности число ноль. Вфункции elem мы как раз и извлекаем тот элемент рядом с которым хранится число ноль. Обратите внима-ние на то, что рекурсия встроена в этот алгоритм, если данное число не равно нулю, мы просто извлекаемследующий элемент.С помощью этой функции мы можем вычислить n-тое число из ряда чисел Фибоначчи. Сначала создадим

поток чисел Фибоначчи:

Хиломорфизм | 205

Page 206: Ru Haskell Book

fibs :: Stream Intfibs = ana (\(a, b) -> a :& (b, a+b)) (0, 1)

Теперь просто извлечём n-тый элемент из потока чисел Фибоначчи:

fib :: Int -> Intfib = flip getElem fibs

Вычислим поток всех простых чисел. Мы будем вычислять его по алгоритму ”решето Эратосфена”. Вначале алгоритма у нас есть поток целых чисел и известно, что первое число является простым.

2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 . . .

В процессе этого алгоритма мы вычёркиваем все не простые числа. Сначала мы ищем первое не зачёркну-тое число и помещаем его в результирующий поток, а на следующий шаг алгоритма мы передаём исходный,поток в котором зачёркнуты все числа кратные тому, что мы положили последним:

2

3, //4, 5, //6, 7, //8, 9, ///10, 11, ///12, 13, ///14, 15 . . .

Теперь мы ищем первое незачёркнутое число и помещаем его в результат. А на следующий шаг рекусиипередаём поток, в котором зачёркнуты все числа кратные новому простому числу:

2, 3

//4, 5, //6, 7, //8, //9, ///10 ///12, 13, ///14, ///15 . . .

И так далее, на каждом шаге мы будем получать одно простое число. Зачёркивание мы будем имитиро-вать с помощью типа Maybe. Всё начинается с потока целых чисел, в котором не зачёркнуто ни одно число:

nums :: Stream (Maybe Int)nums = mapS Just $ iterateS (+1) 2

mapS :: (a -> b) -> Stream a -> Stream bmapS f = ana $ \xs -> (f $ headS xs) :& tailS xs

iterateS :: (a -> a) -> a -> Stream aiterateS f = ana $ \x -> x :& f x

В силу ограничений системы типов Haskell мы не можем определить экземпляр Functor для типа Stream,поскольку Stream является не самостоятельным типом а типом-синонимом. Поэтому нам приходится опре-делить функцию mapS. Определим шаг рекурсии:

primes :: Stream Intprimes = ana erato nums

erato xs = n :& erase n yswhere n = fromJust $ headS xs

ys = dropWhileS isNothing xs

Переменная n содержит первое не зачёркнутое число на данном шаге. Переменная ys указывает на спи-сок чисел, из начала которого удалены все зачёркнутые числа. Функции isNothing и fromJust взяты из стан-дартного модуля Data.Maybe. Нам осталось определить лишь две функции. Это аналог функции dropWhileна списках. Эта функция удаляет из начала списка все элементы, которые удовлетворяют некоторому пре-дикату. Вторая функция erase вычёркивает все числа в потоке кратные данному.

dropWhileS :: (a -> Bool) -> Stream a -> Stream adropWhileS p = psi >> phi

where phi ((b, xs) :& next) = if b then next else xspsi xs = (p $ headS xs, xs) :& tailS xs

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

206 | Глава 15: Категориальные типы

Page 207: Ru Haskell Book

erase :: Int -> Stream (Maybe a) -> Stream (Maybe a)erase n xs = ana phi (0, xs)

where phi (a, xs)| a == 0 = Nothing :& (a’, tailS xs)| otherwise = headS xs :& (a’, tailS xs)where a’ = if a == n-1 then 0 else (a+1)

В функции erase мы заменяем на Nothing каждый элемент, порядок следования которого кратен аргу-менту n. Проверим, что у нас получилось:

*Fix> primes(2 :& (3 :& (5 :& (7 :& (11 :& (13 :& (17 :& (19 :& (23 :&(29 :& (31 :& (37 :& (41 :& (43 :& (47 :& (53 :& (59 :&(61 :& (67 :& (71 :& (73 :& (79 :& (83 :& (89 :& (97 :&(101 :& (103 :& (107 :& (109 :& (113 :& (127 :& (131 :&...

15.4 Краткое содержаниеВ этой главе мы узнали, что любая рекурсивная функция может быть выражена через структурную ре-

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

Управляющие структуры определяются структурой типов.

Определив тип, мы получаем вместе с ним две функции структурной рекурсии, это катаморфизм (дляначальных объектов) и анаморфизм (для конечных объектов). С помощью катаморфизма мы можем свора-чивать значение данного типа в значения любого другого типа, а с помощью анаморфизма мы можем раз-ворачивать значения данного типа из значений любого другого типа. Также мы узнали, что категория Haskявляется категорией CPO, категорией полных частично упорядоченных множеств.

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

можно больше определений из главы о структурной рекурсии в терминах типа Fix и функций cata, ana иhylo. Также потренируйтесь на стандартных функциях из модуля Prelude. Определите новые типы через Fixнапример деревья из модуля Data.Tree. Попробуйте свои силы на функциях по-сложнее например алгоритмеэвристического поиска.

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

Page 208: Ru Haskell Book

Глава 16

Дополнительные возможности

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

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

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

Сахар для списковПеречисленияДля класса Enum определён специальный синтаксис составления последовательностей перечисляемых

значений. Так например мы можем составить список целых чисел от нуля до десяти:Prelude> [0 .. 10][0,1,2,3,4,5,6,7,8,9,10]

А так мы можем составить бесконечную последовательность положительных чисел:Prelude> take 20 $ [0 .. ][0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]

Мы можем составлять последовательности с определённым шагом. Так можно выделить все чётные по-ложительные числа:Prelude> take 20 $ [0, 2 .. ][0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38]

А так мы можем составить убывающую последовательность чисел:Prelude> [10, 9 .. 0][10,9,8,7,6,5,4,3,2,1,0]

Что интересно в списке могут находиться не только числа, а любые значения из класса Enum. Напримеропределим тип:data Day = Monday | Tuesday | Wednesday | Thursday

| Friday | Saturday | Sundayderiving (Show, Enum)

Теперь мы можем написать:*Week> [Friday .. Sunday][Friday,Saturday,Sunday]*Week> [ Monday .. ][Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday]

Также шаг последовательности может быть и дробным:*Week> [0, 0.5 .. 4][0.0,0.5,1.0,1.5,2.0,2.5,3.0,3.5,4.0]

208 | Глава 16: Дополнительные возможности

Page 209: Ru Haskell Book

Генераторы списков

Генераторы списков (list comprehensions) объединяют в себе функции преобразования и фильтрации спис-ков. Они записываются так:

[ f x | x <- list, p x]

В этой записи мы фильтруем список list предикатом p и преобразуем результат функцией x. Напримервозведём в квадрат все чётные элементы списка:

Prelude> [x*x | x <- [1 .. 10], even x][4,16,36,64,100]

Предикатов может быть несколько, так например мы можем оставить лишь положительные чётные числа:

Prelude> [x | x <- [-10 .. 10], even x, x >= 0][0,2,4,6,8,10]

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

Prelude> [ [x,y] | x <- ”Hello”, y <- ”World”][”HW”,”Ho”,”Hr”,”Hl”,”Hd”,”eW”,”eo”,”er”,”el”,”ed”,”lW”,”lo”,”lr”,”ll”,”ld”,”lW”,”lo”,”lr”,”ll”,”ld”,”oW”,”oo”,”or”,”ol”,”od”]

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

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

a -> m b

Если бы эти функции выглядели как обычные функции:

a -> b

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

a1 -> a2 -> a3 -> ... -> an -> m b

А теперь представьте, что вам нужно подставить специальное значение третьим аргументом такой функ-ции и затем передать ещё в одну такую же функцию. Для облегчения участи программистов было придуманоспециальное окружение do, в котором специальные функции комбинируются так словно они являются обыч-ными. Для этого используется обратная стрелка. Посмотрим как определяется функция sequence в окруже-нии do:

sequence :: [m a] -> m [a]sequence [] = return []sequence (mx:mxs) = do

x <- mxxs <- sequence mxsreturn (x:xs)

Во втором уравнении сначала мы говорим вычислителю словом do о том, что выражения записаны в миремонады m. Запись с перевёрнутой стрелкой x <- mx означает, что мы далее в do-блоке можем пользоватьсязначением x так словно оно имеет тип просто a, но не m a. Смотрите в этом определении мы сначала извле-каем первый элемент списка, затем извлекаем хвост списка, приведённый к типу m [a], и в самом конце мысоединяем голову и хвост и в самом конце оборачиваем результат в специальное значение.Например мы можем построить функцию, которая дважды читает строку со стандартного ввода и затем

возвращает объединение двух строк:

Пуд сахара | 209

Page 210: Ru Haskell Book

getLine2 :: IO StringgetLine2 = do

a <- getLineb <- getLinereturn (a ++ b)

В do-нотации можно вводить локальные переменные с помощью слова let:

t = dob <- f ac <- g blet x = c + b

y = x + creturn y

Посмотрим как do-нотация переводится в выражение, составленное с помощью методов класса Monad:

doa <- ma => ma >>= (\a -> exp)exp

doexp1 => exp1 >> exp2exp2

dolet x = fx => let x = fx

y = fy y = fyexp in exp

Переведём с помощью этих правил определение для второго уравнения из функции sequence

sequence (mx:mxs) = dox <- mx mx >>= (\x -> doxs <- sequence mxs => xs <- sequence mxs =>return (x:xs) return (x:xs))

=> mx >>= (\x -> sequence mxs >>= (\xs -> return (x:xs)))

do или Applicative?С появлением класса Applicative во многих случаях do-нотация теряет свою ценность. Так например

любой do-блок вида:

f mx my = dox <- mxy <- myreturn (op x y)

Можно записать гораздо короче:

f = liftA2 op

Например напишем функцию, которая объединяет два файла в один:

appendFiles :: FilePath -> FilePath -> FilePath -> IO ()

С помощью do-нотации:

appendFiles file1 file2 resFile = doa <- readFile file1b <- readFile file2writeFile resFile (a ++ b)

А теперь с помощью класса Applicative:

appendFiles file1 file2 resFile = writeFile resFile =<<liftA2 (++) (readFile file1) (readFile file2)

210 | Глава 16: Дополнительные возможности

Page 211: Ru Haskell Book

16.2 РасширенияРасширение появляется в ответ на проблему, с которой трудно или невозможно справится в рамках стан-

дарта Haskell. Мы рассмотрим несколько наиболее часто используемых расширений. Расширения подключа-ются с помощью специального комментария. Он помещается в начале модуля. Расширение действует тольков текущем модуле.

{-# LANGUAGE ExtentionName1, ExtentionName2, ExtentionName3 #-}

Обратите внимание на символ решётка, обрамляющий комментарии. Слово LANGUAGE говорит компи-лятору о том, что мы хотим воспользоваться расширениями с именами ExtentionName1, ExtentionName2,ExtentionName3. Такой комментарий называется прагмой (pragma). Часто компилятор ghc в случае ошибкипредлагает нам подключить расширение, в котором ошибка уже не будет ошибкой, а возможностью языка.Он говорит возможно вы имели в виду расширение XXX. Например попробуйте загрузить в интерпретатормодуль:

module Test where

class Multi a b where

В этом случае мы увидим ошибку:

Prelude> :l Test[1 of 1] Compiling Test ( Test.hs, interpreted )

Test.hs:3:0:Too many parameters for class ‘Multi’(Use -XMultiParamTypeClasses to allow multi-parameter classes)In the class declaration for ‘Multi’

Failed, modules loaded: none.

Компилятор сообщает нам о том, что у нас слишком много параметров в классе Multi. В рамках стандар-та Haskell можно создавать лишь классы с одним параметром. Но за сообщением мы видим подсказку, еслимы воспользуемся расширением -XMultiParamTypeClasses, то всё будет хорошо. В этом сообщении имя рас-ширения закодировано в виде флага. Мы можем запустить ghc или ghci с этим флагом и тогда расширениебудет активировано, и модуль загрузится. Попробуем:

Prelude> :qLeaving GHCi.$ ghci -XMultiParamTypeClassesPrelude> :l Test[1 of 1] Compiling Test ( Test.hs, interpreted )Ok, modules loaded: Test.*Test>

Модуль загрузился! У нас есть и другая возможность подключить модуль с помощью прагмы LANGUAGE.Имя расширения записано во флаге после символов -X. Добавим в модуль Test расширение с именемMultiParamTypeClasses:

{-# LANGUAGE MultiParamTypeClasses #-}module Test where

class Multi a b where

Теперь загрузим ghci в обычном режиме:

*Test> :qLeaving GHCi.$ ghciPrelude> :l Test[1 of 1] Compiling Test ( Test.hs, interpreted )Ok, modules loaded: Test.

Расширения | 211

Page 212: Ru Haskell Book

Обобщённые алгебраические типы данныхПредположим, что мы хотим написать компилятор небольшого языка. Наш язык содержит числа и логиче-

ские значения. Мы можем складывать числа и умножать. Для логических значений определена конструкцияif-then-else. Определим тип синтаксического дерева для этого языка:

data Exp = ValTrue| ValFalse| If Exp Exp Exp| Val Int| Add Exp Exp| Mul Exp Expderiving (Show)

В этом определении кроется одна проблема. Наш тип позволяет нам строить бессмысленные выражениявроде Add ValTrue (Val 2) или If (Val 1) ValTrue (Val 22). Наш тип Val включает в себя все хорошие вы-ражения и много плохих. Эта проблема проявится особенно ярко, если мы попытаемся определить функциюeval, которая вычисляет значение для нашего языка. Получается, что тип этой функции:

eval :: Exp -> Either Int Bool

Для решения этой проблемы были придуманы обобщённые алгебраические типы данных (generalisedalgebraic data types, GADTs). Они подключаются расширением GADTs. Помните когда-то мы говорили, чтотипы можно представить в виде классов. Например определение для списка

data List a = Nil | Cons a (List a)

можно мысленно переписать так:

data List a whereNil :: List aCons :: a -> List a -> List a

Так вот в GADT определения записываются именно в таком виде. Обобщение заключается в том, чтотеперь на месте произвольного параметра a мы можем писать конкретные типы. Определим тип GExp

{-# LANGUAGE GADTs #-}

data Exp a whereValTrue :: Exp BoolValFalse :: Exp BoolIf :: Exp Bool -> Exp a -> Exp a -> Exp aVal :: Int -> Exp IntAdd :: Exp Int -> Exp Int -> Exp IntMul :: Exp Int -> Exp Int -> Exp Int

Теперь у нашего типа Exp появился параметр, через который мы кодируем дополнительные ограниченияна типы операций. Теперь мы не сможем составить выражение Add ValTrue ValFalse, потому что оно непройдёт проверку типов.Определим функцию eval:

eval :: Exp a -> aeval x = case x of

ValTrue -> TrueValFalse -> FalseIf p t e -> if eval p then eval t else eval eVal n -> nAdd a b -> eval a + eval bMul a b -> eval a * eval b

Если eval получит логическое значение, то будет возвращено значение типа Bool, а на значение типа ExpInt будет возвращено целое число. Давайте убедимся в этом:

212 | Глава 16: Дополнительные возможности

Page 213: Ru Haskell Book

*Prelude> :l Exp[1 of 1] Compiling Exp ( Exp.hs, interpreted )Ok, modules loaded: Exp.*Exp> let notE x = If x ValFalse ValTrue*Exp> let squareE x = Mul x x*Exp>*Exp> eval $ squareE $ If (notE ValTrue) (Val 1) (Val 2)4*Exp> eval $ notE ValTrueFalse*Exp> eval $ notE $ Add (Val 1) (Val 2)

<interactive>:1:14:Couldn’t match expected type ‘Bool’ against inferred type ‘Int’Expected type: Exp Bool

Actual type: Exp IntIn the return type of a call of ‘Add’In the second argument of ‘($)’, namely ‘Add (Val 1) (Val 2)’

Сначала мы определили две вспомогательные функции. Затем вычислили несколько значений. Haskellочень часто применяется для построения компиляторов. Мы рассмотрели очень простой язык, но в болеесложном случае суть останется прежней. Дополнительный параметр позволяет нам закодировать в парамет-ре тип функций нашего языка. Спрашивается: зачем нам дублировать вычисления в функции eval? Зачем намсначала кодировать выражение конструкторами, чтобы только потом получить то, что мы могли вычислитьи напрямую.При таком подходе у нас есть полный контроль за деревом выражения, мы можем проводить дополни-

тельную оптимизацию выражений, если нам известны некоторые закономерности. Ещё функция eval можетвычислять совсем другие значения. Например она может по виду выражения составлять код на другом языке.Возможно этот язык гораздо мощнее Haskell по вычислительным способностям, но беднее в плане вырази-тельности, гибкости синтаксиса. Тогда мы будем в функции eval проецировать разные конструкции Haskellв конструкции другого языка. Такие программы называются предметно-ориентированными языками програм-мирования (domain specific languages). Мы кодируем в типе Exp некоторую область и затем надстраиваемнад типом Exp разные полезные функции. На самом последнем этапе функция eval переводит всё деревовыражения в значение или код другого языка.Отметим, что не так давно было предложено другое решение этой задачи. Мы можем закодировать типы

функций в классе:

class E exp wheretrue :: exp Boolfalse :: exp Booliff :: exp Bool -> exp a -> exp a -> exp aval :: Int -> exp Intadd :: exp Int -> exp Int -> exp Intmul :: exp Int -> exp Int -> exp Int

Преимуществом такого подхода является модульность. Мы можем спокойно разделить выражение на двесоставляющие части:

class (Log exp, Arith exp) => E exp

class Log exp wheretrue :: exp Boolfalse :: exp Booliff :: exp Bool -> exp a -> exp a -> exp a

class Arith exp whereval :: Int -> exp Intadd :: exp Int -> exp Int -> exp Intmul :: exp Int -> exp Int -> exp Int

Интерпретация дерева выражения в этом подходе заключается в создании экземпляра класса. Напримерсоздадим класс-вычислитель Eval:

newtype Eval a = Eval { runEval :: a }

instance Log Eval where

Расширения | 213

Page 214: Ru Haskell Book

true = Eval Truefalse = Eval Falseiff p t e = if runEval p then t else e

instance Arith Eval whereval = Evaladd a b = Eval $ runEval a + runEval bmul a b = Eval $ runEval a * runEval b

instance E Eval

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

notE :: Log exp => exp Bool -> exp BoolnotE x = iff x true false

squareE :: Arith exp => exp Int -> exp IntsquareE x = mul x x

e1 :: E exp => exp Inte1 = squareE $ iff (notE true) (val 1) (val 2)

e2 :: E exp => exp Boole2 = notE true

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

*Exp> :r[1 of 1] Compiling Exp ( Exp.hs, interpreted )Ok, modules loaded: Exp.*Exp> runEval e14*Exp> runEval e2False

Получились такие же результаты и в этом случае нам не нужно подключать никаких расширений. Теперьсоздадим тип-принтер, он будет распечатывать выражение:

newtype Print a = Print { runPrint :: String }

instance Log Print wheretrue = Print ”True”false = Print ”False”iff p t e = Print $ ”if (” ++ runPrint p ++ ”) {”

++ runPrint t ++ ”}”++ ”{” ++ runPrint e ++ ”}”

instance Arith Print whereval n = Print $ show nadd a b = Print $ ”(” ++ runPrint a ++ ”)+(” ++ runPrint b ++ ”)”mul a b = Print $ ”(” ++ runPrint a ++ ”)*(” ++ runPrint b ++ ”)”

Теперь распечатаем предыдущие выражения:

*Exp> :r[1 of 1] Compiling Exp ( Exp.hs, interpreted )Ok, modules loaded: Exp.*Exp> runPrint e1”(if (if (True) {False}{True}) {1}{2})*(if (if (True) {False}{True}) {1}{2})”*Exp> runPrint e2”if (True) {False}{True}”

При таком подходе нам не пришлось ничего менять в выражениях, мы просто заменили тип выраженияи оно автоматически подстроилось под нужный результат. Подробнее об этом подходе можно почитать насайте http://okmij.org/ftp/tagless-final/course/course.html или в статье Жака Каре (Jacques Carette),Олега Киселёва (Oleg Kiselyov) и Чунг-Че Шена (Chung-chieh Shan) Finally Tagless, Partially Evaluated.

214 | Глава 16: Дополнительные возможности

Page 215: Ru Haskell Book

Семейства типовСемейства типов позволяют выражать зависимости типов. Например представим, что класс определяет

не только методы, но и типы. Причём новые типы зависят от конкретного экземпляра класса. Посмотрим,например, на определение линейного пространства из библиотеки vector-space:

class AdditiveGroup v wherezeroV :: v(^+^) :: v -> v -> vnegateV :: v -> v

class AdditiveGroup v => VectorSpace v wheretype Scalar v :: *(*^) :: Scalar v -> v -> v

Линейное пространство это математическая структура, объектами которой являются вектора и скаля-ры. Для векторов определена операция сложения, а для скаляров операции сложения и умножения. Крометого определена операция умножения вектора на скаляр. При этом должны выполнятся определённые свой-ства. Мы не будем подробно на них останавливаться, вкратце заметим, что эти свойства говорят о том, чтомы действительно пользуемся операциями сложения и умножения. В классе VectorSpace мы видим новуюконструкцию, объявление типа. Мы говорим, что есть производный тип, который следует из v. Далее черездвойное двоеточие мы указываем его вид. В данном случае это простой тип без параметров.Вид (kind) это тип типа. Простой тип без параметра обозначается звёздочкой. Тип с параметром обозна-

чается как функция * -> *. Если бы тип принимал два параметра, то он обозначался бы * -> * -> *. Такжепараметры могут быть не простыми типами а типами с параметрами, например тип, который обозначаеткомпозицию типов:

newtype O f g a = O { unO :: f (g a) }

имеет вид (* -> *) -> (* -> *) -> * -> *.Определим класс векторов на двумерной сетке и сделаем его экземпляром класса VectorSpace. Для нача-

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

{-# Language TypeFamilies #-}module Point2D where

class AdditiveGroup v where...

Теперь определим новый тип:

data V2 = V2 Int Intderiving (Show, Eq)

Сделаем его экземпляром класса AdditiveGroup:

instance AdditiveGroup V2 wherezeroV = V2 0 0(V2 x y) ^+^ (V2 x’ y’) = V2 (x+x’) (y+y’)negateV (V2 x y) = V2 (-x) (-y)

Мы складываем и вычитаем значения в каждом из элементов кортежа. Нейтральным элементом от-носительно сложения будет кортеж, состоящий из двух нулей. Теперь определим экземпляр для классаVectorSpace. Поскольку кортеж состоит из двух целых чисел, скаляр также будет целым числом:

instance VectorSpace V2 wheretype Scalar V2 = Ints *^ (V2 x y) = V2 (s*x) (s*y)

Попробуем вычислить что-нибудь в интерпретаторе:

Расширения | 215

Page 216: Ru Haskell Book

*Prelude> :l Point2D[1 of 1] Compiling Point2D ( Point2D.hs, interpreted )Ok, modules loaded: Point2D.*Point2D> let v = V2 1 2*Point2D> v ^+^ vV2 2 4*Point2D> 3 *^ v ^+^ vV2 4 8*Point2D> negateV $ 3 *^ v ^+^ vV2 (-4) (-8)

Семейства функций дают возможность организовывать вычисления на типах. Посмотрим на такой клас-сический пример. Реализуем в типах числа Пеано. Нам понадобятся два типа. Один для обозначения нуля,а другой для обозначения следующего элемента:{-# Language TypeFamilies, EmptyDataDecls #-}module Nat where

data Zerodata Succ a

Значения этих типов нам не понадобятся, поэтому мы воспользуемся расширением EmptyDataDecls, ко-торое позволяет определять типы без значенеий. Значениями будут комбинации типов. Мы определим опе-рации сложения и умножения для чисел. Для начала определим сложение:type family Add a b :: *

type instance Add a Zero = atype instance Add a (Succ b) = Succ (Add a b)

Первой строчкой мы определили семейство функций Add, у которого два параметра. Определение семей-ства типов начинается с ключевой фразы type family. За двоеточием мы указали тип семейства. В данномслучае это простой тип без параметра. Далее следуют зависимости типов для семейства Add. Зависимоститипов начинаются с ключевой фразы type instance. В аргументах мы словно пользуемся сопоставлением собразцом, но на этот раз на типах. Первое уравнение:type instance Add a Zero = a

Говорит о том, что если второй аргумент имеет тип ноль, то мы вернём первый аргумент. Совсем как вобычном функциональном определении сложения для натуральных чисел Пеано. а во втором уравнении мысоставляем рекурсивное уравнение:type instance Add a (Succ b) = Succ (Add a b)

Точно также мы можем определить и умножение:type family Mul a b :: *

type instance Mul a Zero = Zerotype instance Mul a (Succ b) = Add a (Mul a b)

При этом нам придётся подключить ещё одно расширение UndecidableInstances, поскольку во второмуравнении мы подставили одно семейство типов в другое. Этот флаг часто используется в сочетании с рас-ширением TypeFamilies. Семейства типов фактически позволяют нам определять функции на типах. Этоведёт к тому, что алгоритм вывода типов становится неопределённым. Если типы правильные, то компиля-тор сможет это установить, но если они окажутся неправильными, может возникнуть такая ситуация, чтокомпилятор зациклится и будет бесконечно долго искать соответствие одного типа другому. Теперь про-верим результаты. Для этого мы создадим специальный класс, который будет переводить значения-типы вобычные целочисленные значения:class Nat a where

toInt :: a -> Int

instance Nat Zero wheretoInt = const 0

instance Nat a => Nat (Succ a) wheretoInt x = 1 + toInt (proxy x)

proxy :: f a -> aproxy = undefined

216 | Глава 16: Дополнительные возможности

Page 217: Ru Haskell Book

Мы определили для каждого значения-типа экземпляр класса Nat, в котором мы можем переводить типыв числа. Функция proxy позволяет нам извлечь значение из типа-конструктора Succ. При этом мы нигде непользуемся значениями типов Zero и Succ, ведь у этих типов нет значений. Поэтому в экземпляре для Zeroмы пользуемся постоянной функцией const.Теперь посмотрим, что у нас получилось:

Prelude> :l Nat*Nat> let x = undefined :: (Mul (Succ (Succ (Succ Zero))) (Succ (Succ Zero)))*Nat> toInt x6

Видно, что с помощью класса Nat мы можем извлечь значение, закодированное в типе. Зачем нам этистранные типы-значения? Мы можем использовать их в двух случаях. Мы можем кодировать значения в типеили проводить более тонкую проверку типов.Помните когда-то мы определяли функции для численного интегрирования. Там точность метода была

жёстко задана в тексте программы:

dt :: Fractional a => adt = 1e-3

-- метод Эйлераint :: Fractional a => a -> [a] -> [a]int x0 ~(f:fs) = x0 : int (x0 + dt * f) fs

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

data Stream n a = a :& Stream n a

Параметр n кодирует точность. Теперь мы можем извлекать точность из типа:

dt :: (Nat n, Fractional a) => Stream n a -> adt x = 1 / (fromIntegral $ toInt $ proxy fs)

where proxy :: Stream n a -> nproxy = undefined

int :: (Nat n, Fractional a) => a -> Stream n a -> Stream n aint x0 ~(f:&fs) = x0 :& int (x0 + dt fs * f) fs

Теперь посмотрим как мы можем сделать проверку типов более тщательной. Представим, что у нас естьтип матриц. Известно, что сложение определено только для матриц одинаковой длины, а для умноженияматриц число столбцов одной матрицы должно совпадать с числом колонок другой матрицы. Мы можемотразить все эти зависимости в целочисленных типах:

data Mat n m a = ...

instance Num a => AdditiveGroup (Mat n m a) wherea ^+^ b = ...zeroV = ...negateV a = ...

mul :: Num a => Mat n m a -> Mat m k a -> Mat n k a

При таких определениях мы не сможем сложить матрицы разных размеров. Причём ошибка будет вычис-лена до выполнения программы. Это освобождает от проверки границ внутри алгоритма умножения матриц.Если алгоритм запустился, то мы знаем, что размеры аргументов соответствуют.Скоро в ghc появится поддержка чисел на уровне типов. Это будет специальное расширение

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

Расширения для классовРассмотрим несколько полезных расширений, относящихся к определению классов и экземпляров клас-

сов. Расширение MultiParamTypeClasses позволяет объявлять классы с несколькими аргументами. Напримервзгляните на такой класс:

Расширения | 217

Page 218: Ru Haskell Book

class Iso a b whereto :: a -> bfrom :: b -> a

Так мы можем определить изоморфизм между типами a и bРасширение TypeSynonymInstances позволяет определять экземпляры для синонимов типов. Мы уже

пользовались этим расширением, когда определяли рекурсивные типы через тип Fix, там нам нужно бы-ло определить экземпляр Num для синонима Nat:type Nat = Fix N

instance Num Nat where

В рамках стандарта все суперклассы должны быть простыми. Все они имеют вид T a. Если мы хотим хотимиспользовать суперклассы с составными типами, нам придётся подключить расширение FlexibleContexts.Этим расширением мы пользовались, когда определяли экземпляр Show для Fix:instance Show (f (Fix f)) => Show (Fix f) where

show x = ”(” ++ show (unFix x) ++ ”)”

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

написание типов функций считается признаком хорошего стиля. Поскольку по типам можно догадаться чемфункция занимается. Но есть в правиле вывода типов одно исключение. Если мы напишем:f = show

То компилятор сообщит нам об ошибке:Test.hs:5:5:

Ambiguous type variable ‘a0’ in the constraint:(Show a0) arising from a use of ‘show’

Possible cause: the monomorphism restriction applied to the following:f :: a0 -> String (bound at Test.hs:5:1)

Probable fix: give these definition(s) an explicit type signatureor use -XNoMonomorphismRestriction

In the expression: showIn an equation for ‘f’: f = show

и предложит воспользоваться расширением NoMonomorphismRestriction. Это расширение отменяет ограни-чение мономорфизма. Считается, что если у значения нет объявления типа, то тип значения должен бытьмономорфным, т.е. это должен быть конкретный тип, без параметров.По соображениям, которые остались для меня не ясными, такой код сработает:

f x = show x

Зачем нужно ограничение мономорфизма? Есть примеры, в которых из-за отсутствия мономорфизмалокальные переменные приходится вычислять несколько раз. Часто приводится такой пример:genericLength :: Num a => [b] -> agenericLength = foldr (const (+1)) 0

f xs = (len,len)where len = genericLength xs

Без ограничения мономорфизма, для функции f будет выведен тип:f :: (Num a, Num b) => [x] -> (a, b)

Это приведёт к тому, что переменная len будет вычислена дважды. Часто в сильно обобщённых биб-лиотеках, с большими зависимостями в типах выписывать типы крайне неудобно. Например в библиотекесоздания парсеров Parsec. С этим ограничением приходится писать огромные объявления типов для крохот-ных выражений. Что-то вроде:fun :: (Stream s m t, Show t) => ParsecT s u m a -> ParsecT s u m [a]

И так для любого выражения. В этом случае лучше просто выключить ограничение, добавив в началофайла:{-# Language NoMonomorphismRestriction #-}

218 | Глава 16: Дополнительные возможности

Page 219: Ru Haskell Book

16.3 Краткое содержаниеВ этой главе мы затронули малую часть возможностей, которые предоставляются системой ghc. Haskell

является полигоном для испытания самых разнообразных идей. Это экспериментальный язык. Но в практиче-ских целях в 1998 году был зафиксирован стандарт языка, его обычно называют Haskell98. Любое расшире-ние подключается с помощью специальной прагмы Language. Новый стандарт Haskell Prime включит в себянаиболее устоявшиеся расширения. Также мы рассмотрели несколько полезных классов и синтаксическихконструкций, которые, возможно, облегчают написание программ.

16.4 УпражненияЭто была справочная глава, присмотритесь к рассмотренным возможностям и подумайте какие нужны

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

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

Page 220: Ru Haskell Book

Глава 17

Средства разработки

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

17.1 ПакетыВ Haskell есть ещё один уровень организации данных, мы можем объединять модули в пакеты (package).

Также как и модули пакеты могут зависеть от других пакетов, если они пользуются модулями их этих па-кетов. Одним пакетом мы уже пользовались и довольно часто, это пакет base, который содержит все стан-дартные модули, например такие как Prelude, Control.Applicative или Data.Function. Для создания иустановки пакетов существует приложение cabal. Оно определяет протокол организации и распростране-ния модулей Haskell.

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

хранятся в директории с именем src. Для того чтобы превратить набор модулей в пакет, нам необходимопоместить в одну директорию с src два файла:

• имяПакета.cabal – файл с описанием пакета.• Setup.hs – файл с инструкциями по установке пакета

.cabalПосмотрим на простейший файл с описанием библиотеки, этот файл находится в одной директории с

той директорией, в которой содержатся все модули приложения и имеет расширение .cabal:

Name : FooVersion : 1.0

Librarybuild-depends : baseexposed-modules : Foo

Сначала идут свойства пакета. Общий формат определения свойства:

ИмяСвойства : Значение

В примере мы указали имя пакета Foo, и версию 1.0. После того, как мы указали все свойства, мы опре-деляем будет наш пакет библиотекой или исполняемой программой или возможно он будет и тем и другим.Если пакет будет библиотекой, то мы помещаем за набором атрибутов слово Library, а если это исполняе-мая программа, то мы помещаем слово Executable, после мы пишем описание модулей пакета, зависимостиот других пакетов, какие модули будут видны пользователю. Формат составления описаний в этой части та-кой же как и в самом начале файла. Сначала идёт зарезервированное слово-атрибут, затем через двоеточиеследует значение. Обратите внимание на отступы за словом Library, они обязательны и сделаны с помощьюпробелов, cabal не воспринимает табуляцию.Файл .cabal может содержать комментарии, они делаются также как и в Haskell, закомментированная

строка начинается с двойного тире.

220 | Глава 17: Средства разработки

Page 221: Ru Haskell Book

Setup.hsФайл Setup.hs содержит информацию о том как устанавливается библиотека. При установке могут ис-

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

import Distribution.Simplemain = defaultMain

Этот файл позволяет нам создавать библиотеки и приложения, которые созданы только с помощьюHaskell. Это не так уж и мало!

Создаём библиотекиТипичный файл .cabal для библиотеки выглядит так:

Name: pinocchioVersion: 1.1.1Cabal-Version: >= 1.2License: BSD3License-File: LICENSEAuthor: Mister GeppettoHomepage: http://pinocchio.sourceforge.net/Category: AISynopsis: Tools for creation of woodcrafted robotsBuild-Type: Simple

LibraryBuild-Depends: baseHs-Source-Dirs: src/Exposed-modules:

Wood.Robot.Act, Wood.Robot.Percept, Wood.Robot.ThinkOther-Modules:

Wood.Robot.Internals

Этим файлом мы описали библиотеку с именем pinocchio, версия 1.1.1, она использует версию cabalне ниже 1.2. Библиотека выпущена под лицензией BSD3. Файл с лицензией находится в текущей директо-рии под именем LICENSE. Автор библиотеки Mister Geppetto. Подробнее узнать о библиотеке можно на еёдомашней странице http://pinocchio.sourceforge.net/. Атрибут Category указывает на широкую отрасльзнаний, к которой принадлежит наша библиотека. В данном случае мы описываем библиотеку для построе-ния роботов из дерева, об этом мы пишем в атрибуте Synopsis (краткое описание), поэтому наша библиоте-ка принадлежит к категории искусственный интеллект или сокращённо AI. Последний атрибут Build-Typeуказывает на тип сборки пакета. Мы будем пользоваться значением Simple, который соответствует сборке спомощью простейшего файла Setup.hs, который мы рассмотрели в предыдущем разделе.После описания пакета, идёт слово Library, ведь мы создаём библиотеку. Далее в атрибуте Build-

Depends мы указываем зависимости для нашего пакета. Здесь мы перечисляем все пакеты, которые мы ис-пользуем в своей библиотеке. В данном случае мы пользовались лишь стандартной библиотекой base. Ватрибуте hs-source-dirs мы указываем, где искать директорию с исходным кодом библиотеки. Затем мыуказываем три внешних модуля, они будут доступны пользователю после установки библиотеки (атрибутExposed-Modules), и внутренние скрытые модули (атрибут Other-Modules).

Создаём исполняемые программыТипичный файл .cabal для исполняемой программы:

Name: microVersion: 0.0Cabal-Version: >= 1.2License: BSD3Author: Tony ReedsSynopsis: Small programming languageBuild-Type: Simple

Executable microBuild-Depends: base, parsec

Пакеты | 221

Page 222: Ru Haskell Book

Main-Is: Main.hsHs-Source-Dirs: micro

Executable micro-replMain-Is: Main.hsBuild-Depends: base, parsecHs-Source-Dirs: replOther-Modules: Utils

В этом файле мы описываем две программы. Компилятор языка и интерпретатор языка micro. Если срав-нить этот файл с файлом для библиотеки, то мы заметим лишь один новый атрибут. Это Main-Is. Он указыва-ет в каком модуле содержится функция main. После установки этого пакета будут созданы два исполняемыхфайла. С именами micro и micro-repl.

Установка пакетаПакеты устанавливаются с помощью команды install. Необходимо перейти в директорию пакета, ту,

в которой находятся два служебных файла (.cabal и Setup.hs) и директория с исходниками, и запуститькоманду:

cabal install

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

устанавливают другие пакеты. Проблема возникает из-за того, что cabal может положить бинарный файл вдиректорию, которая не видна следующим программам, которые хотят продолжить установку. В этом слу-чае необходимо либо переложить созданные бинарные файлы в директорию, которая будет им видна, илидобавить директорию с новыми бинарными файлами в PATH (под UNIX, Linux). Переменная операционнойсистемы PATH содержит список всех путей, в которых система ищет исполняемые программы, если путь неуказан явно. Посмотреть содержание PATH можно, вызвав:

$ echo $PATH

Появится строка директорий, которые записаны через двоеточие. Для того чтобы добавить директорию/data/dir в PATH необходимо написать:

$ PATH=$PATH:/data/dir

Эта команда добавит директорию в PATH для текущей сессии в терминале, если мы хотим записать еёнасовсем, мы добавим эту команду в специальный скрытый файл .bashrc, он находится в домашней дирек-тории пользователя. Под Windows добавить директорию в PATH можно с помощью графического интерфейса.Кликните правой кнопкой мыши на иконку My Computer (Мой Компьютер), в появившемся меню выбери-те вкладку Properties (Свойства). Появится окно System Properties (Свойства системы), в нём выберитевкладку Advanced и там нажмите на кнопку Environment variables (Переменные среды). И в этом окне будетстрока Path, её мы и хотим отредактировать, добавив необходимые нам пути.Давайте потренируемся и создадим библиотеку и исполняемую программу. Создадим библиотеку, кото-

рая выводит на экран Hello World. Создадим директорию hello, и в ней создадим директорию src. Эта ди-ректория будет содержать исходный код. Главный модуль библиотеки экспортирует функцию приветствия:

module Hello where

import Utility.Hello(hello)import Utility.World(world)

helloWorld = hello ++ ”, ” ++ world ++ ”!”

Главный модуль программы Main.hs определяет функцию main, которая выводит текст приветствия наэкран:

222 | Глава 17: Средства разработки

Page 223: Ru Haskell Book

module Main where

import Hello

main = print helloWorld

У нас будет два внутренних модуля, каждый из которых определяет синоним для одного слова. Мы по-местим их в папку Utility. Это модуль Utility.Hellomodule Utility.Hello wherehello = ”Hello”

И модуль Utility.World:module Utility.World whereworld = ”World”

Исходники готовы, теперь приступим к описанию пакета. Создадим в корневой директории пакета файлhello.cabal.Name: helloVersion: 1.0Cabal-Version: >= 1.2License: BSD3Author: AntonSynopsis: Little example of cabal usageCategory: ExampleBuild-Type: Simple

LibraryBuild-Depends: base == 4.*Hs-Source-Dirs: src/Exposed-modules:

HelloOther-Modules:

Utility.HelloUtility.World

Executable helloBuild-Depends: base == 4.*Main-Is: Main.hsHs-Source-Dirs: src/

В этом файле мы описали библиотеку и программу. В строке base == 4.* мы указали версию пакета base.Запись 4.* означает любая версия, которая начинается с четвёрки. Осталось только поместить в корневуюдиректорию пакета файл Setup.hs.import Distribution.Simplemain = defaultMain

Теперь мы можем переключиться на корневую директорию пакета и установить пакет:anton@anton-desktop:~/haskell-notes/code/ch-17/hello$ cabal installResolving dependencies...Configuring hello-1.0...Preprocessing library hello-1.0...Preprocessing executables for hello-1.0...Building hello-1.0...[1 of 3] Compiling Utility.World ( src/Utility/World.hs, dist/build/Utility/World.o )[2 of 3] Compiling Utility.Hello ( src/Utility/Hello.hs, dist/build/Utility/Hello.o )[3 of 3] Compiling Hello ( src/Hello.hs, dist/build/Hello.o )Registering hello-1.0...[1 of 4] Compiling Utility.World ( src/Utility/World.hs, dist/build/hello/hello-tmp/Utility/World.o )[2 of 4] Compiling Utility.Hello ( src/Utility/Hello.hs, dist/build/hello/hello-tmp/Utility/Hello.o )[3 of 4] Compiling Hello ( src/Hello.hs, dist/build/hello/hello-tmp/Hello.o )[4 of 4] Compiling Main ( src/Main.hs, dist/build/hello/hello-tmp/Main.o )Linking dist/build/hello/hello ...Installing library in /home/anton/.cabal/lib/hello-1.0/ghc-7.4.1Installing executable(s) in /home/anton/.cabal/binRegistering hello-1.0...

Пакеты | 223

Page 224: Ru Haskell Book

Мы видим сообщения о процессе установки. После установки в текущей директории пакета появиласьдиректория dist, в которую были помещены скомпилированные файлы библиотеки. В последних строкахcabal сообщил нам о том, что он установил библиотеку в директорию:Installing library in /home/anton/.cabal/lib/hello-1.0/ghc-7.4.1

и исполняемый файл в директорию:Installing executable(s) in /home/anton/.cabal/bin

С помощью различных флагов мы можем контролировать процесс установки пакета. Назначать дополни-тельные директории, указывать куда поместить скомпилированные файлы. Подробно об этом можно почи-тать в справке, выполнив в командной строке одну из команд:cabal --helpcabal install --help

Если у вас не получилось сразу установить пакет не отчаивайтесь и почитайте сообщения об ошибкахиз cabal, он информативно жалуется о забытых зависимостях и неспособности правильно прочитать файл сописанием пакета.

Удаление библиотекиУстановленные с помощью cabal файлы видны из любого модуля. Имена модулей регистрируются гло-

бально. Если нам захочется установить библиотеку с уже зарегистрированным именем, произойдёт хаос.Возможно прежняя библиотека нам уже не нужна. Как нам удалить её? Посмотрим на решение для компи-лятора ghc. Мы можем посмотреть список всех зарегистрированных в ghc библиотек с помощью команды:$ ghc-pkg list

Cabal-1.8.0.6array-0.3.0.1base-4.2.0.2......

Появится длинный список с именами библиотек. Для удаления одной из них мы можем выполнить ко-манду:ghc-pkg unregister имя-библиотеки

Например так мы можем удалить только что установленную библиотеку hello:$ ghc-pkg unregister hello

Репозиторий пакетов HackageЕсли у нас подключен интернет, то мы можем воспользоваться наследием сообщества Haskell и уста-

новить пакет с Hackage. Там расположено много-много-много пакетов. Любой разработчик Haskell можетдобавить свой пакет на Hackage. Посмотреть на пакеты можно на сайте этого репозитория:

http://hackage.haskell.org

Если для вашей задачи необходимо выполнить какую-нибудь довольно общую задачу, например написатьтип красно-чёрных деревьев или построить парсер или возможно вам нужен веб-сервер, поищите этот пакетна Hackage, он там наверняка окажется, ещё и в нескольких вариантах.Для установки пакета с Hackage нужно просто написать

cabal install имя-пакета

Возможно нам нужен очень новый пакет, который был только что залит автором на Hackage. Тогда вы-полняем:cabal update

Происходит обновление данных о загруженных на Hackage. Что хорошо, вы можете загрузить исходникииз Hackage, например у вас никак не получается написать пакет, который устанавливался бы без ошибок.Просто загрузим исходники какого-нибудь пакета из Hackage и посмотрим на пример рабочего пакета.

224 | Глава 17: Средства разработки

Page 225: Ru Haskell Book

Дополнительные атрибуты пакетаВ файле .cabal также часто указывают такие атрибуты как:

MaintainerПоле содержит адрес электронной почты тех. поддержки StabilityСтатус версии библиотеки (стабильная, экспериментальная, нестабильная). DescriptionПодробное описание назначения пакета. Оно помещается на главную страницу пакета в документации.

Extra-Source-FilesВ этом поле можно через пробел указать дополнительные файлы, включаемые в пакет. Это могут бытьпримеры использования, описание в формате PDF или хроника изменений и другие служебные файлы.

License-fileПуть к файлу с лицензией.

17.2 Создание документации с помощью HaddockЕсли мы зайдём на Hackage, то там мы увидим длинный список пакетов, отсортированных по категориям.

К какой категории какой пакет относится мы указываем в .cabal-файле в атрибуте Category. Далее рядом сименем пакета мы видим краткое описание, оно берётся из атрибута Synopsis. Если мы зайдём на страницуодного из пакетов, то там мы увидим страницу в таком же формате, что и документация к стандартнымбиблиотекам. Мы видим описание пакета и ниже иерархию модулей. Мы можем зайти в заинтересовавшийнас модуль и посмотреть на объявленные функции, типы и классы. В самом низу страницы находится ссылкак исходникам пакета.”Домашняя страница” пакета была создана с помощью приложения Haddock. Оно генерирует документа-

цию в формате html по специальным комментариям. Haddock встроен в cabal, например мы можем сделатьдокументацию к нашему пакету hello. Для этого нужно переключиться на корневую директорию пакета ивызвать:

cabal haddock

После этого в директории dist появится директория doc, в которой внутри директории html находитсясозданная документация. Мы можем открыть файл index.html и там мы увидим ”иерархию нашего” модуля.В модуле пока нет ни одной функции, так получилось потому, что Haddock помещает в документацию лишьте функции, у которых есть объявление типа. Если мы добавим в модуле Hello.hs: к единственной функцииобъявление типа:

helloWorld :: StringhelloWorld = hello ++ ”, ” ++ world ++ ”!”

И теперь перезапустим haddock. То мы увидим, что в модуле Hello появилась одна запись.

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

-- | Here is the commenthelloWorld :: StringhelloWorld = hello ++ ”, ” ++ world ++ ”!”

Обратите внимание на значок ”или”, сразу после комментариев. Этот комментарий будет включен вдокументацию. Также можно писать комментарии после определения для этого к комментарию добавляетсязначок степени:

helloWorld :: StringhelloWorld = hello ++ ”, ” ++ world ++ ”!”-- ^ Here is the comment

К сожалению на момент написания этих строк Haddock может включать в документацию лишь латинскиесимволы. Комментарии могут простираться несколько строк:

-- | Here is the type.-- It contains three elements.-- That’s it.data T = A | B | C

Создание документации с помощью Haddock | 225

Page 226: Ru Haskell Book

Также они могут быть блочными:

{-|Here is the type.It contains three elements.That’s it.

-}data T = A | B | C

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

add :: Num a => a -- ^ The first argument-> a -- ^ The second argument-> a -- ^ The return value

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

data T-- | constructor A= A-- | constructor B| B-- | constructor C| C

Или так:

data T = A -- ^ constructor A| B -- ^ constructor B| C -- ^ and so on

Комментарии к классу:

-- | С-classclass С a where

-- | f-functionf :: a -> a-- | g-functiong :: a -> a

Комментарии к модулюКомментарии к модулю помещаются перед объявлением имени модуля. Эта информация попадёт в самое

начало страницы документации:

-- | Little examplemodule Hello where

Структура страницы документацииЕсли модуль большой, то его бывает удобно разделить на части, словно разделы в главе книги. Определе-

ния группируются по функциональности и помещаются в разные разделы или даже подразделы. Структурадокументации определяется с помощью специальных комментариев в экспорте модуля. Посмотрим на при-мер:

-- | Little examplemodule Hello(

-- * Introduction-- | Here is the little example to show you-- how to make docs with Haddock

-- * Types-- | The types.T(..),-- * Classes

226 | Глава 17: Средства разработки

Page 227: Ru Haskell Book

-- | The classes.C(..),-- * FunctionshelloWorld-- ** Subfunctions1-- ** Subfunctions2

) where

...

Комментарии со звёздочкой создают раздел, а с двумя звёздочками – подраздел. Те определения, ко-торые экспортируются за комментариями со звёздочкой попадут в один раздел или подраздел. Если сразуза комментарием со звёздочкой идёт комментарий со знаком ”или”, то он будет помещён в самое началораздела. В нём мы можем пояснить по какому принципу группируются определения в данном разделе.

РазметкаС помощью специальных символов можно выделять различные элементы текста, например, ссылки, куски

кода, названия определений или модулей. Haddock установит необходимые ссылки и выделит элемент вдокументации.При этом символы /, ’, ‘, ”, @, < являются специальными, если вы хотите воспользоваться одним из специ-

альных символов в тексте необходимо написать перед ним обратный слэш \. Также символы для обозначениякомментариев *, |, и > являются специальными, если они расположены в самом начале строки.

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

параграфа:

-- | The first paragraph goes here.---- The second paragraph goes here.fun :: a -> b

Блоки кодаСуществует два способа обозначения блоков кода:

-- | This documentation includes two blocks of code:---- @-- f x = x + x-- g x = x-- @---- > g x = x * 42

В первом варианте мы заключаем блок кода в окружение @...@. Так мы можем выделить целый кусоккода. Для выделения одной строки мы можем воспользоваться знаком >.

Примеры вычисления в интерпретатореВ Haddock мы можем привести пример вычисления выражения в интерпретаторе. Это делается с помощью

тройного символа >:

-- | Two examples are given bellow:---- >>> 2+3-- 5---- >>> print 1 >> print 2-- 1-- 2

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

Создание документации с помощью Haddock | 227

Page 228: Ru Haskell Book

Имена определений

Для того чтобы выделить имя любого определения, будь то функция, тип или класс, необходимо заклю-чить его в ординарные кавычки, как в ’T’. При этом Haddock установит ссылку к определению и подсветитимя в тексте. Для того чтобы сослаться на определение из другого модуля необходимо написать его полноеимя, т.е. с приставкой имени модуля, например функция fun, определённая в модуле M, имеет полное имяM.fun, тогда в комментариях мы обозначаем её ’M.fun’.Ординарные кавычки часто используются в английском языке как апострофы, в таких сочетаниях как

don’t, isn’t. Перед такими вхождениями ординарных кавычек можно не писать обратный слэш. Haddock сумеетотличить их от идентификатора.

Курсив и моноширинный шрифт

Для выделения текста курсивом, он заключается в окружение .... Для написания текста моношириннымшрифтом, он заключается в окружение @...@.

Модули

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

-- | This is a reference to the ”Foo” module.

Списки

Список без нумерации обозначается с помощью звёздочек:

-- | This is a bulleted list:---- * first item---- * second item

Пронумерованный список, обозначается символами (n) или n. (n с точкой), где n – некоторое целоечисло:

-- | This is an enumerated list:---- (1) first item---- 2. second item

Список определений

Определения обозначаются квадратными скобками, например комментарий:

-- | This is a definition list:---- [@foo@] The description of @[email protected] [@bar@] The description of @bar@.

в документации будет выглядеть так:foo

The description of foo. barThe description of bar.Для выделения текста моноширинным шрифтом мы воспользовались окружением @...@.

URL

Ссылки на сайты включаются с помощью окружения <...>.

228 | Глава 17: Средства разработки

Page 229: Ru Haskell Book

Ссылки внутри модуля

Для того чтобы сослаться на какой-нибудь текст внутри модуля, его необходимо отметить ссылкой. Дляэтого мы помещаем в том месте, на которое мы хотим сослаться, запись #label#, где label – это идентифика-тор ссылки. Теперь мы можем сослаться на это место из другого модуля с помощью записи ”module#label”,где module – имя модуля, в котором находится ссылка label.

17.3 Краткое содержаниеВ этой главе мы познакомились с основными элементами арсенала разработчика программ. Мы научи-

лись создавать библиотеки и документировать их.

17.4 УпражненияВспомните один из примеров и превратите его в библиотеку. Например, напишите библиотеку для нату-

ральных чисел Пеано.

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

Page 230: Ru Haskell Book

Глава 18

Ориентируемся по карте

Рассмотрим задачу поиска маршрута на карте. У нас есть карта метро и нам нужно проложить маршрутот одной станции к другой. Карта метро – это граф, узлы обозначают станции, а рёбра соединяют соседниестанции. Предположим, что мы знаем расстояния между всеми станциями и нам надо найти кратчайшийпуть от станции площадь Баха до станции Таинственный лес (рис. 18.1).

Тилль

Инева

Лао

пл.Баха

Сириус Звезда

ул.Булычёва

Дно болота

Троллев мост

Призрак

лес

Таинственный

Де

Крест

Родник

пл.Шекспира

Север

Юг

Восток

ЗападКосмодром

Рис. 18.1: Схема метрополитена

Давайте переведём этот рисунок на Haskell. Сначала опишем имена линий и станций:

module Metro where

data Station = St Way Namederiving (Show, Eq)

data Way = Blue | Black | Green | Red | Orangederiving (Show, Eq)

data Name = Kosmodrom | UlBylichova | Zvezda| Zapad | Ineva | De | Krest | Rodnik | Vostok| Yug | Sirius | Til | TrollevMost | Prizrak | TainstvenniyLes| DnoBolota | PlBakha | Lao | Sever| PlShekspira

deriving (Show, Eq)

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

230 | Глава 18: Ориентируемся по карте

Page 231: Ru Haskell Book

data Point = Point{ px :: Double, py :: Double} deriving (Show, Eq)

place :: Name -> Pointplace x = uncurry Point $ case x of

Kosmodrom -> (-3,7)UlBylichova -> (-2,4)Zvezda -> (0,1)Zapad -> (1,7)Ineva -> (0.5, 4)De -> (0,-1)Krest -> (0,-3)Rodnik -> (0,-5)Vostok -> (-1,-7)Yug -> (-7,-1)Sirius -> (-3,0)Til -> (3,2)TrollevMost -> (5,4)Prizrak -> (8,6)TainstvenniyLes -> (11,7)DnoBolota -> (-7,-4)PlBakha -> (-3,-3)Lao -> (3.5,0)Sever -> (6,1)PlShekspira -> (3,-3)

dist :: Point -> Point -> Doubledist a b = sqrt $ (px a - px b)^2 + (py a - py b)^2

stationDist :: Station -> Station -> DoublestationDist (St n a) (St m b)

| n /= m && a == b = penalty| otherwise = dist (place a) (place b)where penalty = 1

Расстояние между точками вычисляется по формуле Евклида (dist). Если у станций одинаковые имена,но они расположены на разных линиях мы будем считать, что расстояние между ними равно единице. Теперьнам необходимо описать связность станций. Мы опишем связность в виде функции, которая для даннойстанции возвращает список всех соседних с ней станций:

metroMap :: Station -> [Station]metroMap x = case x of

St Black Kosmodrom -> [St Black UlBylichova]St Black UlBylichova ->

[St Black Kosmodrom, St Black Zvezda, St Red UlBylichova]St Black Zvezda ->

[St Black UlBylichova, St Blue Zvezda, St Green Zvezda]...

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

A*.

18.1 Алгоритм эвристического поиска А*Наша задача относится к задачам поиска путей на графе. Путём на графе называют такую последова-

тельность узлов, в которой для любых двух соседних узлов существует ребро, которое их соединяет. В на-шем случае графом является карта метро, узлами – станции, рёбрами – линии между станциями, а путями –маршруты.Представим, что мы находимся в узле A и нам необходимо попасть в узел B и единственное, что нам

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

Алгоритм эвристического поиска А* | 231

Page 232: Ru Haskell Book

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

проще. Такой компас принято называть эвристикой. Это функция, которая принимает узел и возвращаетчисло. Чем меньше число, тем ближе узел к цели. Обычно эвристика указывает не точное расстояние доцели, поскольку мы не знаем где цель, а приблизительную оценку. Мы не знаем расстояние до цели, нодогадываемся, нам кажется, что она где-то там, ещё чуть-чуть и мы найдём её. Примером эвристики дляпоиска по карте может быть функция, которая вычисляет расстояние по прямой до цели. Предположим, чтомы не знаем где находится цель (какая дорога к ней ведёт), но мы знаем её координаты. Также мы знаемкоординаты каждой вершины, в которой мы находимся. Тогда мы можем легко вычислить расстояние попрямой до цели и наш поиск станет гораздо более осмысленным.Так находясь в точке A мы можем сразу пойти в тот соседний узел, который ближе всех к цели. Такой

поиск называют поиском по первому лучшему приближению. В поиске A* учитывается не только расстояниедо цели, но и то расстояние, которое мы уже прошли. Мы выбираем не ту вершину, которая ближе к цели, ату для которой полный путь до цели будет минимальным. Ведь пока мы идём мы можем запоминать какоерасстояние мы уже прошли. Прибавив к этому значению, то которое мы получим с помощью эвристики мыполучим полный (предполагаемый) путь до цели.Поиск А* гораздо лучше поиска по первому лучшему приближению. Его часто применяют в компьютерных

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

вторятся (они образуют циклы). В случае поиска на дереве мы считаем, что все вершины уникальны. Припоиске на графе очень важно запоминать те вершины, в которых мы уже побывали. Иначе мы будем оченьчасто ходить кругами.В Haskell очень удобно работать с данными, которые имеют иерархическую структуру. Их можно пред-

ставить в виде дерева, обычно в таких типах у нас есть конструкторы-константы и конструкторы, которыесобирают составные значения. Граф выходит за рамки этого класса данных, потому что рёбра графов могутобразовывать циклы. Но мы схитрим и представим граф поиска в виде дерева. Корнем нашего дерева будетначальная точка поиска, а поддеревьями для данной вершины узла будут все вершины-соседи. В таком де-реве будет очень много повторяющихся узлов, так например мы можем пойти в соседнюю вершину, потомвернуться обратно, опять пойти в туже соседнюю вершину, и так до бесконечности. Для того, чтобы избежатьподобных ситуаций мы будем запоминать те вершины, в которых мы уже побывали и не рассматривать их,если они встретятся нам ещё раз.Сформулируем задачу поиска в типах. У нас есть дерево поиска, которое содержит все возможные раз-

ветвления, также каждая вершина содержит значение эвристики, по нему мы знаем насколько близка даннаявершина к цели. Также у нас есть специальный предикат, который определён на вершинах, по нему мы мо-жем узнать является ли данная вершина целью. Нам нужно получить путь, или цепочку вершин, котораябудет начинаться в корне дерева поиска и заканчиваться в целевой вершине.search :: Ord h => (a -> Bool) -> Tree (a, h) -> Maybe [a]

Здесь a – это значение вершины и h – значение эвристики. Обратите внимание на зависимость Ord h вконтексте, ведь мы собираемся сравнивать эти значения по близости к цели. При обходе дерева мы будемзапоминать повторяющиеся вершины, для этого мы воспользуемся типом множество из стандартного мо-дуля Data.Set. Внутри Set могут хранится только значения, для которых определены операции сравнения,поэтому нам придётся добавить ещё одну зависимость в контекст функции:import Data.Treeimport qualified Data.Set as S

search :: (Ord h, Ord a) => (a -> Bool) -> Tree (a, h) -> Maybe [a]

Поиск будет заключаться в том, что мы будем обходить дерево от корня к узлам. При этом среди всехузлов-альтернатив мы будем просматривать узлы с наименьшим значением эвристики. В этом нам помо-жет специальная структура данных, которая называется очередью с приоритетом (priority queue). Эта очередьхранит элементы с учётом их старшинства (приоритета). Мы можем добавлять в неё элементы и извлекатьэлементы. При этом мы всегда будем извлекать элемент с наименьшим приоритетом. Мы воспользуемсяочередями из библиотеки fingertree. Для начала установим библиотеку:cabal install fingertree

Теперь посмотрим в документацию и узнаем какие функции нам доступны. Документацию к пакету мож-но найти на сайте http://hackage.haskell.org/package/fingertree. Пока отложим детальное изучение ин-терфейса, отметим лишь то, что мы можем добавлять элементы к очереди и извлекать элементы с учётомприоритета:

232 | Глава 18: Ориентируемся по карте

Page 233: Ru Haskell Book

insert :: Ord k => k -> v -> PQueue k v -> PQueue k vminView :: Ord k => PQueue k v -> Maybe (v, PQueue k v)

Вернёмся к функции search. Я бы хотел обратить ваше внимание на то, как мы будем разрабатывать этуфункцию. Вспомним, что Haskell – ленивый язык. Это означает, что при обработке рекурсивных типов данных,функция ”углубляется” в значение лишь тогда, когда функция, которая вызвала эту функцию попросит её обэтом. Это даёт нам возможность работать с потенциально бесконечными структурами данных и, что болееважно, разделять сложный алгоритм на независимые составляющие.В функции search нам необходимо обойти все элементы в порядке значения эвристики и остановиться

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

search :: (Ord h, Ord a) => (a -> Bool) -> Tree (a, h) -> Maybe [a]search isGoal = findPath isGoal . flattenTree . addPath

выпишем типы составляющих функций и проверим код в интерпретаторе.

un = undefined

findPath :: (a -> Bool) -> [Path a] -> Maybe [a]findPath = un

flattenTree :: (Ord h, Ord a) => Tree (Path a, h) -> [Path a]flattenTree = un

addPath :: Tree (a, h) -> Tree (Path a, h)addPath = un

data Path a = Path{ pathEnd :: a, path :: [a]}

Обратите внимание на то как поступающие на вход данные разделились между функциями. Информа-ция о приоритете вершин не идёт дальше функции flattenTree, а предикат isGoal используется только вфункции findPath. Модуль прошёл проверку типов и мы можем детализировать функции дальше:

addPath :: Tree (a, h) -> Tree (Path a, h)addPath = iter []

where iter ps t = Node (Path val (reverse ps’), h) $iter ps’ <$> subForest twhere (val, h) = rootLabel t

ps’ = val : ps

В этой функции мы просто присоединяем к данной вершине все родительские вершины, так мы составля-ем маршрут от данной вершины до начальной, поскольку мы всё время добавляем новые вершины в началосписка, в итоге у нас получаются перевёрнутые маршруты, поэтому перед тем как обернуть значение в кон-структор Path мы переворачиваем список. На самом деле нам нужно перевернуть только один путь. Путь,который ведёт к цели, но за счёт того, что язык у нас ленивый, функция reverse будет применена не сразу, алишь тогда, когда нам действительно понадобится значение пути. Это как раз и произойдёт лишь один раз,в самом конце программы, лишь для одного значения!Давайте пока пропустим функцию flattenTree и сначала определим функцию findPath. Эта функция

принимает все вершины, которые мы обошли если бы шли без цели (функции isGoal) и ищет среди нихпервую, которая удовлетворяет предикату. Для этого мы воспользуемся стандартной функцией find из мо-дуля Data.List:

findPath :: (a -> Bool) -> [Path a] -> Maybe [a]findPath isGoal = fmap path . find (isGoal . pathEnd)

Напомню тип функции find, она принимает предикат и список, а возвращает первое значение списка, накотором предикат вернёт True:

find :: (a -> Bool) -> [a] -> Maybe a

Алгоритм эвристического поиска А* | 233

Page 234: Ru Haskell Book

Функция fmap применяется из-за того, что результат функции find завёрнут в Maybe, это частично опре-делённая функция. В самом деле ведь в списке может и не оказаться подходящего значения.Осталось определить функцию flattenTree. Было бы хорошо определить её так, чтобы она была развёрт-

кой для списка. Поскольку функция find является свёрткой (может быть определена через fold), вместе этифункции работали бы очень эффективно. Мы определим функцию flattenTree через взаимную рекурсию.Две функции будут по очереди вызывать друг друга. Одна из них будет извлекать следующее значение изочереди, а другая – проверять не встречалось ли нам уже такое значение, и добавлять новые элементы вочередь.

flattenTree :: (Ord h, Ord a) => Tree (Path a, h) -> [Path a]flattenTree a = ping none (singleton a)

ping :: (Ord h, Ord a) => Visited a -> ToVisit a h -> [Path a]ping visited toVisit

| isEmpty toVisit = []| otherwise = pong visited toVisit’ awhere (a, toVisit’) = next toVisit

pong :: (Ord h, Ord a)=> Visited a -> ToVisit a h -> Tree (Path a, h) -> [Path a]

pong visited toVisit a| inside a visited = ping visited toVisit| otherwise = getPath a :

ping (insert a visited) (schedule (subForest a) toVisit)

Типы Visited и ToVisit обозначают наборы вершин, которые мы уже посетили и которые только собира-емся посетить. Не вдаваясь в подробности интерфейса этих типов, давайте присмотримся к функциям ping иpong с точки зрения функции, которая их будет вызывать, а именно функции findPath. Эта функция ожидаетна входе список. Внутри она обходит список в поисках нужного элемента, поэтому она будет применять со-поставление с образцом, разбирая список на части. Сначала она запросит сопоставление с пустым списком,запустится функция ping с пустым множеством посещённых вершин (none) и одним элементом в очередивершин (singleton a), которые предстоит посетить. Функция ping проверит не является ли очередь пустой,очередь содержит один элемент, поэтому она перейдёт к следующему случаю и извлечёт из очереди одинэлемент (next), который будет передан в функцию pong. Функция pong проверит нет ли в списке уже посе-щённых элементов того, который был только что извлечён (inside a visited). Если это окажется так, тоона запросит следующий элемент у функции ping. Если же исходный элемент окажется новым, она добавитего в список (getPath a : ...) и запланирует обход всех дочерних деревьев данного элемента (schedule(subForest a) toVisit). При первом заходе исходный элемент окажется новым и функция findPath поймёт,что список не пустой и остановит вычисление. Она немного передохнёт и примется за следующий случай.Там она будет извлекать первый элемент списка и сопоставлять его с предикатом. При этом первый элементуже вычислен. Мы воспользуемся этим, убедимся в том, что он не является целью и рекурсивно вызовемфункцию find на хвосте списка. Функция findPath запросит следующее значение и так далее.Наша функция flattenPath не является развёрткой, но очень похожа на неё тем, что позволяет вычислять

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

getPath :: Tree (Path a, h) -> Path agetPath = fst . rootLabel

Функции для множества вершин, которые мы уже посетили:

import qualified Data.Set as S...

type Visited a = S.Set a

none :: Ord a => Visited anone = S.empty

insert :: Ord a => Tree (Path a, h) -> Visited a -> Visited ainsert = S.insert . pathEnd . getPath

inside :: Ord a => Tree (Path a, h) -> Visited a -> Boolinside = S.member . pathEnd . getPath

234 | Глава 18: Ориентируемся по карте

Page 235: Ru Haskell Book

Функции для очереди тех вершин, что мы только собираемся посетить:

import Data.Maybeimport qualified Data.PriorityQueue.FingerTree as Q...

type ToVisit a h = Q.PQueue h (Tree (Path a, h))

priority t = (snd $ rootLabel t, t)

singleton :: Ord h => Tree (Path a, h) -> ToVisit a hsingleton = uncurry Q.singleton . priority

next :: Ord h => ToVisit a h -> (Tree (Path a, h), ToVisit a h)next = fromJust . Q.minView

isEmpty :: Ord h => ToVisit a h -> BoolisEmpty = Q.null

schedule :: Ord h => [Tree (Path a, h)] -> ToVisit a h -> ToVisit a hschedule = Q.union . Q.fromList . fmap priority

Эти функции очень простые, они специализируют более общие функции для типов Set иPQueue, вы наверняка легко разберётесь с ними, заглянув в документацию к модулям Data.Set иData.PriorityQueue.FingerTree.Осталось только написать функцию, которая будет составлять дерево поиска для алгоритма A*. Она при-

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

astarTree :: (Num h, Ord h)=> (a -> [(a, h)]) -> (a -> h) -> a -> Tree (a, h)

astarTree alts distToGoal s0 = unfoldTree f (s0, 0)where f (s, h) = ((s, heur h s), next h <$> alts s)

heur h s = h + distToGoal snext h (a, d) = (a, d + h)

Поиск маршрутов в метроТеперь давайте посмотрим как наша функция справится с задачей поиска маршрутов в метро:

metroTree :: Station -> Station -> Tree (Station, Double)metroTree init goal = astarTree distMetroMap (stationDist goal) init

connect :: Station -> Station -> Maybe [Station]connect a b = search (== b) $ metroTree a b

main = print $ connect (St Red Sirius) (St Green Prizrak)

К примеру найдём маршрут от станции ”Дно Болота” до станции ”Призрак”:

*Metro> connect (St Orange DnoBolota) (St Green Prizrak)Just [St Orange DnoBolota,St Orange PlBakha,

St Red PlBakha,St Red Sirius,St Green Sirius,St Green Zvezda,St Green Til,St Green TrollevMost,St Green Prizrak]

*Metro> connect (St Red PlShekspira) (St Blue De)Just [St Red PlShekspira,St Red Rodnik,St Blue Rodnik,

St Blue Krest,St Blue De]*Metro> connect (St Red PlShekspira) (St Orange De)Nothing

В третьем случае маршрут не был найден, поскольку у нас нет станции De на оранжевой ветке.

18.2 Тестирование с помощью QuickCheckМы проверили три случая, ещё три случая, ещё три случая, ожидаемый результат сходится с тем, что

возвращает нам интерпретатор, но можем ли мы быть уверены в том, что алгоритм действительно работает?

Тестирование с помощью QuickCheck | 235

Page 236: Ru Haskell Book

Для Haskell была разработана специальная библиотека тестирования QuickCheck, которая упрощает про-цесс проверки программ. Мы можем сформулировать свойства, которые обязательно должны выполняться,а QuickCheck сгенерирует случайный набор данных и проверит наши свойства на них.Например в нашей задаче путь из A в B должен совпадать с перевёрнутым путём из B в A. Также все станции

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

module Test where

import Control.Applicative

import Metro

prop1 :: Station -> Station -> Boolprop1 a b = connect a b == (fmap reverse $ connect b a)

prop2 :: Station -> Station -> Boolprop2 a b = maybe True (all (uncurry near) . pairs) $ connect a b

pairs :: [a] -> [(a, a)]pairs xs = zip xs (drop 1 xs)

near :: Station -> Station -> Boolnear a b = a ‘elem‘ (fst <$> distMetroMap b)

Установим QuickCheck:

cabal install QuickCheck

Теперь нам нужно подсказать QuickCheck как генерировать случайные значения типа Station. QuickCheckтестирует функции, которые принимают значения из класса Arbitrary и возвращают Bool. Класс Arbitraryотвечает за генерацию случайных значений.Основной метод arbitrary возвращает генератор случайных значений:

class Arbitrary a wherearbitrary :: Gen a

Мы воспользуемся тем, что этот класс уже определён для многих стандартных типов. Кроме того классGen явялется монадой. Мы сгенерируем случайное целое число и отобразим его в одну из станций. Сделатьэто можно разными способами, мы начнём из одной станции и будем случайно блуждать по карте:

import Test.QuickCheck...

instance Arbitrary Station wherearbitrary = ($ s0) . foldr (.) id . fmap select <$> ints

where ints = vector =<< choose (0, 100)s0 = St Blue De

select :: Int -> Station -> Stationselect i s = as !! mod i (length as)

where as = fst <$> distMetroMap s

Мы воспользовались двумя функциями из бибилотеки QuickCheck. Это vector и choose. Первая строитсписок случайных чисел заданной длины, а вторая выбирает случайное число из заданного диапазона. Теперьмы можем протетстировать наши предикаты с помощью функции quickCheck:

*Test Prelude> quickCheck prop1+++ OK, passed 100 tests.*Test Prelude> quickCheck prop2+++ OK, passed 100 tests.*Test Prelude>

Свойства прошли тестирование на выборке из 100 комбинаций аргументов. Если нам интересно, мыможем с помощью функции verboseCheck посмотреть на каких именно значениях проводилось тестирование:

236 | Глава 18: Ориентируемся по карте

Page 237: Ru Haskell Book

*Test Prelude> verboseCheck prop2Passed:St Black KosmodromSt Red UlBylichovaPassed:St Black UlBylichovaSt Orange SeverPassed:St Red SiriusSt Blue Krest...

Если бы свойство не выполнилось, QuickCheck сообщил бы нам об этом и показал бы те элементы, длякоторых свойство не выполнилось. Давайте составим такое свойство искусственно. Например, проверим,находятся ли все станции на одной линии:

fakeProp :: Station -> Station -> BoolfakeProp (St a _) (St b _) = a == b

Посмотрим, что на это скажет QuickCheck:

*Test Prelude> quickCheck fakeProp*** Failed! Falsifiable (after 1 test):St Green SiriusSt Blue Rodnik

По умолчанию QuickCheck проверит свойство сто раз. Для изменения этих настроек, мы можем восполь-зоваться функцией quickCheckWith, дополнительным параметром она принимает значение типа Arg, котороесодержит параметры тестирования. Например протестируем первое свойство 500 раз:

*Test> quickCheckWith (stdArgs{ maxSuccess = 500 }) prop1+++ OK, passed 500 tests.

Мы воспользовались стандартными настройками (stdArgs) и изменили один параметр.

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

сомневаемся в остальных. Как раз для этого случая в QuickCheck предусмотрена функция a==>b. Это функ-ция обозначает условную проверку, свойство b будет протестировано только в том случае, если свойство aокажется верным. Иначе тестовые данные будут отброшены.

notBlueAndBlack a b = cond a && cond b ==> prop1 a bwhere cond (St a _) = a /= Blue && a /= Black

Далее тестируем как обычно:

*Test> quickCheck notBlueAndBlack+++ OK, passed 100 tests.

Также с помощью функции forAll мы можем подсказать QuickCheck на каких данных тестировать свой-ство.

forAll :: (Show a, Testable prop) => Gen a -> (a -> prop) -> Property

Эта функция принимает генератор случайных значений и свойство, которое зависит от тех значений,которые создаются этим генератором. К примеру, пусть нас интересуют только все возможные пути междучетырьмя станциями: (St Blue De), (St Red Lao), (St Green Til) и (St Orange Sever). Воспользуемсяфункцией elements :: [a] -> Gen a, она как раз принимает список значений, и возвращает генератор,который случайным образом выбирает любое значение из этого списка.

testFor = forAll (liftA2 (,) gen gen) $ uncurry prop1where gen = elements [St Blue De, St Red Lao,

St Green Til, St Orange Sever]

Проверим, те ли значения попали в выборку:

Тестирование с помощью QuickCheck | 237

Page 238: Ru Haskell Book

*Test> verboseCheckWith (stdArgs{ maxSuccess = 3 }) testForPassed:(St Blue De,St Orange Sever)Passed:(St Orange Sever,St Red Lao)Passed:(St Red Lao,St Red Lao)+++ OK, passed 3 tests.

Мы можем настроить формирование выборки ещё одним способом. Для этого мы сделаем специальныйтип обёртку над Station и определим для ненго свой экземпляр класса Arbitrary:

newtype OnlyOrange = OnlyOrange Stationnewtype Only4 = Only4 Station

instance Arbitrary OnlyOrange wherearbitrary = OnlyOrange . St Orange <$>

elements [DnoBolota, PlBakha, Krest, Lao, Sever]

instance Arbitrary Only4 wherearbitrary = Only4 <$> elements [St Blue De, St Red Lao,

St Green Til, St Orange Sever]

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

*Test> quickCheck $ \(Only4 a) (Only4 b) -> prop1 a b+++ OK, passed 100 tests.*Test> quickCheck $ \(Only4 a) (OnlyOrange b) -> prop1 a b+++ OK, passed 100 tests.*Test> quickCheck $ \a (OnlyOrange b) -> prop2 a b+++ OK, passed 100 tests.

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

сообщил бы нам сколько элементов в какой класс попали. Это делается с помощью функции classify:

classify :: Testable prop => Bool -> String -> prop -> Property

Она принимает условие классификации, метку класса и свойство. Например так мы можем разбить вы-борку по типам линий:

prop3 :: Station -> Station -> Propertyprop3 a@(St wa _) b@(St wb _) =

classify (wa == Orange || wb == Orange) ”Orange” $classify (wa == Black || wb == Black) ”Black” $classify (wa == Red || wb == Red) ”Red” $ prop1 a b

Протестируем:

*Test> quickCheck prop3+++ OK, passed 100 tests:34% Red15% Orange9% Black8% Orange, Red6% Black, Red5% Orange, Black

18.3 Оценка шустродействия с помощью criterionНедавно появилась библиотека unordered-containers. Она предлагает более эффективную реализацию

нескольких структур из стандартной библиотеки containers. Например там мы можем найти тип HashSet.Почему бы нам не заменить на него стандартный тип Set?

238 | Глава 18: Ориентируемся по карте

Page 239: Ru Haskell Book

cabal install unordered-containers

Изменения отразятся лишь на контекстах функций. Элементы принадлжежащие множеству HashSetдолжны быть экземплярами классов Eq и Hashable. Новый класс Hashable нужен для ускорения работы сданными. Давайте посмотрим на этот класс:

Prelude> :m Data.HashablePrelude Data.Hashable> :i Hashableclass Hashable a wherehash :: a -> InthashWithSalt :: Int -> a -> Int

-- Defined in ‘Data.Hashable’...... много экземпляров

Обязательный метод класса hash даёт нам возможность преобразовать элемент в целое число. Это числоназывают хеш-ключом. Хеш-ключи используеются для хранения элементов в хеш-таблицах. Мы не будемподробно на них останавливаться, отметим лишь то, что они позволяют очень быстро извлекать данные изконтейнеров и обновлять данные.Теперь просто скопируйте модуль Astar.hs измените одну строчку, и добавьте ещё одну (в шапке моду-

ля):

import qualified Data.HashSet as Simport Data.Hashable

Попробуйте загрузить модуль в интерпретатор. ghci выдаст длинный список ошибок, это – хорошо. Поним вы сможете легко догадать в каких местах необходимо заменить Ord a на (Hashable a, Eq a).Теперь для поиска маршрутов нам необходимо определить экземпляр класса Hashable для типа Station.

В модуле Data.Hashable уже определены экземпляры для многих стандартных типов. Мы воспользуемсяэкземпляром для целых чисел.Добавим в driving подчинённых типов класс Enum и воспользуемся им в экземпляре для Hashable:

instance Hashable Station wherehash (St a b) = hash (fromEnum a, fromEnum b)

Теперь определим две функции определения маршрута:

import qualified AstarSet as Simport qualified AstarHashSet as H...

connectSet :: Station -> Station -> Maybe [Station]connectSet a b = S.search (== b) $ metroTree a b

connectHashSet :: Station -> Station -> Maybe [Station]connectHashSet a b = H.search (== b) $ metroTree a b

Как нам сравнить быстродействие двух алгоримтов? Оценка быстродействия программ, написанных наHaskell, может таить в себе подвохи. Например если мы запустим оба алгоритма в одной программе, возмож-но случится такая ситуация, что часть данных, одинаковая для каждого из методов будет вычислена одинраз, а во втором алгоритме переиспользована, и нам может показаться, что второй алгоритм гораздо быстреепервого. Также необходимо учитывать внешние факторы. Тестовая программа вычисляется на одном ком-пьютере, и если алгоритмы тестируются в разное время, может статься так, что мы сидели-сидели и ждалипока тест завершится, в это время работал первый алгоритм, потом нам надоело ждать, мы решили включитьмузыку, проверить почту, и второму алгоритмку досталось меньше вычислительных ресурсов. Все эти фак-торы необходимо учитывать при тестировании. Как раз для этого и существует замечательная бибилиотекаcriterion.Она проводит серию тестов и по ним оценивает показатели быстродействия. При этом учитывается до-

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

Оценка шустродействия с помощью criterion | 239

Page 240: Ru Haskell Book

Основные типы criterionЦентральным элементом бибилиотеки является класс Benchmarkable. Он объединяет данные, которые

можно тестировать. Среди них чистые функции (тип Pure) и значения с побочными эффектами (тип IO a).Мы можем превращать данные в тесты (тип Benchmark) с помощью функции bench:

benchSource :: Benchmarkable b => String -> b -> Benchmark

Она добавляет к данным комментарий и превращает их в тесты. Как было отмечено, существует однатонкость при тестировании чистых функций: чистые функции в Haskell могут разделять данные между со-бой, поэтому для независимого тестирования мы оборачиваем функции в специальный тип Pure. У нас естьдва варианта тестирования:Мы можем протестировать приведение результата к заголовочной нормальной форме (вспомните главу

о ленивых вычислениях):

nf :: NFData b => (a -> b) -> a -> Pure

или к слабой заголовочной нормальной форме:

whnf :: (a -> b) -> a -> Pure

Аналогичные функции (nfIO, whnfIO) есть и для данных с побочными эффектами. Класс NFData обозна-чает все значения, для которых заголовочная нормальная форма определена. Этот класс пришёл в бибилио-теку criterion из библиотеки deepseq. Стоит отметить эту бибилотеку. В ней определён аналог функцииseq. Функция seq приводит значения к слабой заголовочной нормальной форме (мы заглядываем вглюбьзначения лишь на один конструктор), а функция deepseq проводит полное вычисление значения. Значениеприводится к заголовочной нормальной форме.Также нам пригодится функция группировки тестов:

bgroup :: String -> [Benchmark] -> Benchmark

С её помощью мы объединяем список тестов в один, под некоторым именем. Тестирование проводится спомощью функции defaultMain:

defaultMain :: [Benchmark] -> IO ()

Она принимает список тестов и выполняет их. Выполнение тестов заключается в компиляции програм-мы. После компиляции мы получим исполняемый файл который проводит тестирование в зависимости отпараметров, указываемых фланами. До них мы ещё доберёмся, а пока опишем наши тесты:

-- | Module: Speed.hsmodule Main where

import Criterion.Mainimport Control.DeepSeq

import Metro

instance NFData Station wherernf (St a b) = rnf (rnf a, rnf b)

instance NFData Way whereinstance NFData Name where

pair1 = (St Orange DnoBolota, St Green Prizrak)pair2 = (St Red Lao, St Blue De)

test name search = bgroup name $ [bench ”1” $ nf (uncurry search) pair1,bench ”2” $ nf (uncurry search) pair2]

main = defaultMain [test ”Set” connectSet,test ”Hash” connectHashSet]

240 | Глава 18: Ориентируемся по карте

Page 241: Ru Haskell Book

Экземпляр для класса NFData похож на экземпляр для Hashable. Мы также определили метод значениячерез методы для типов, из которых он состоит. Класс NFData устроен так, что для типов из класса Enum мыможем воспользоваться определением по умолчанию (как в случае для Way и Name).Теперь перейдём в командную строку, переключимся на директорию с нашим модулем и скомпилируем

его:$ ghc -O --make Speed.hs

Флаг -O говорит ghc, что не обходимо провести оптимизацию кода. Появится исполняемый файл Speed.Что мы можем делать с этим файлом? Узнать это можно, запустив его с флагом –help:Мы можем узнать какие функции нам доступны, набрав:

$ ./Speed --helpI don’t know what version I am.Usage: Speed [OPTIONS] [BENCHMARKS]-h, -? --help print help, then exit-G --no-gc do not collect garbage between iterations-g --gc collect garbage between iterations-I CI --ci=CI bootstrap confidence interval-l --list print only a list of benchmark names-o FILENAME --output=FILENAME report file to write to-q --quiet print less output

--resamples=N number of bootstrap resamples to perform-s N --samples=N number of samples to collect-t FILENAME --template=FILENAME template file to use-u FILENAME --summary=FILENAME produce a summary CSV file of all results-V --version display version, then exit-v --verbose print more output

If no benchmark names are given, all are runOtherwise, benchmarks are run by prefix match

Из этих настроек самые интресные, это -s и -o. -s указывает число сэмплов выборке (столько раз будетзапущен каждый тест). а -o говорит, о том в какой файл поместить результаты. Результаты представлены ввиде графиков, формируется файл, который можно открыть в любом браузере. Записать данные в таблицу(например для отчёта) можно с помощью флага -u.Проверим результаты:

./Speed -o res.html -s 100

Откроем файл res.html и посмотрим на графики. Оказалось, что для данных двух случаев первый алго-ритм работал немного лучше. Но выборку из двух вариантов вряд ли можно считать убедительной. Давайтерасширим выборку с помощью QuickCheck. Мы запустим проверку какого-нибудь свойства тем и другимметодом. В итоге QuickCheck сам сгенерирует достаточное число случайных данных, а criterion оценитбыстродействие. Мы проверим самое первое свойство (о перевёрнутых маршрутах) на том и другом алгорит-ме.module Main where

import Control.Applicative

import Test.QuickCheckimport Metro

instance Arbitrary Station wherearbitrary = ($ s0) . foldr (.) id . fmap select <$> ints

where ints = vector =<< choose (0, 100)s0 = St Blue De

select :: Int -> Station -> Stationselect i s = as !! mod i (length as)

where as = fst <$> distMetroMap s

prop :: (Station -> Station -> Maybe [Station])-> Station -> Station -> Bool

prop search a b = search a b == (reverse <$> search b a)

main = defaultMain [bench ”Set” $ quickCheck (prop connectSet),bench ”Hash” $ quickCheck (prop connectHashSet)]

Оценка шустродействия с помощью criterion | 241

Page 242: Ru Haskell Book

В этом тесте метод Set также оказался совсем немного быстрее.Как интерпретировать результаты? С левой стороны мы видим оценку плотности вероятности распреде-

ления быстродействия. Под графиком мы видим среднее (mean) и дисперсию значения (std dev). Показанытри числа. Это нижняя грань доверительного интервала, оценка величины и верхняя грань доверительногоинтервала (ci, confidence interval). Среднее значение показывает оценку величины, мы говорим, что алго-ритм работает примерно 100 миллисекунд. Дисперсия – это разброс результатов вокруг среднего значения.С правой стороны мы видим графики с точками. Каждая точка обозначает отдельный запуск алгоритма.Количество запусков соответствует флагу -s. В последнеё строке под графиком criterion сообщает степеньнедоверия к результатам. В последнем опыте этот показатель достаточно высок. Возможно это связано с тем,что наш алгоритм выбора случайных станций имеет сильный разброс по времени. Ведь сначала мы генери-руем слуайное число n от 0 до 100, и затем начинаем блуждать по карте от начальной точке n раз. Такжеможет влиять то, что время работы алгоритма зависит от положения станций.

18.4 Краткое содержаниеВ этой главе мы реализовали алгоритм эвристического поиска А*. Также мы узнали несколько стандарт-

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

(criterion).

18.5 Упражнения• Я говорил о том, что два варианта алгоритмов дают одинаковые результаты, но так ли это на самомделе? Проверьте это в QuickCheck.• Алгоритм эвристического поиска может применятся не только для поиска маршрутов на карте. Частоалгоритм А* применяется в играх. Встройте этот алгоритм в игру пятнашки (глава 12). Если игрокзапутался и не знает как ходить, он может попросить у компьютера совет. В этой задаче альтернативы –это вершины графа, соседние вершины – это те вершины, в которые мы можем попасть за один ход.Подсказка: воспользуйтесь манхэттенским расстоянием.• Оцените эффективность двух алгоритмов поиска в игре пятнашки. Рассмотрите зависимость быстро-действия от степени сложности игры.

242 | Глава 18: Ориентируемся по карте

Page 243: Ru Haskell Book

Глава 19

Императивное программирование

В этой главе мы потренируемся в укрощении императивного кода. В Haskell все побочные эффекты ого-рожены от чистых функций бетонной стеной IO. Однажды оступившись, мы не можем свернуть с пути по-бочных эффектов, мы вынуждены тащить на себе груз IO до самого конца программы. Но, как мы видели вглавах 11 и 12, тип IO, хоть и обволакивает программу, всё же позволяет пользоваться благами чистых вы-числений. От программиста зависит насколько сильна будет хватка IO. Необходимо уметь выделять точки,в которых применение побочных вычислений действительно необходимо, подключая в них чистые функциичерез методы классов Functor, Applicative и Monad. Тип IO похож на дорогу с контрольными пунктами, вкоторых необходимо отчитаться перед компилятором за ”грязный код”. При неумелом проектировании на-писание программ, насыщенных побочными эффектами, может превратится в пытку. Контрольные пунктыбудут встречаться в каждой функции.Естественный источник побочных эффектов – это пользователь программы. Но, к сожалению, это не един-

ственный источник. Haskell – открытый язык программирования. В нём можно пользоваться программамииз низкоуровневого языка C. Основное преимущество С заключается в непревзойдённой скорости программ.Этот язык позволяет программисту работать с памятью компьютера напрямую. Но за эту силу приходитсяплатить. Возможны очень неприятные и трудноуловимые ошибки. Утечки памяти, обращение по неверномуадресу в памяти, неожиданное обновление переменных. Ещё один плюс С в том, что это язык с историей,на нём написано много хороших библиотек. Некоторые из них встроены в Haskell с помощью специальногомеханизма FFI (foreign function interface). Обсуждение того, как устроен FFI выходит за рамки этой книги. Ин-тересующийся читатель может обратиться к книге Real World Haskell. Мы же потренируемся в использованиитаких библиотек. Язык C является императивным, поэтому, применяя его функций в Haskell, мы неизбежносталкиваемся с типом IO, поскольку большинство интересных функций в С изменяют состояние своих аргу-ментов. В С пишут и чистые функции, такие функции переносятся в Haskell без потери чистоты, но это невсегда возможно.В этой главе мы напишем небольшую 2D-игру, подключив две FFI-библиотеки, это графическая библио-

тека OpenGL и физический движок Chipmunk.

Описание игры

Игра происходит на бильярдной доске. Игрок управляет красным шаром, кликнув в любую точку экрана,он может изменить направление вектора скорости красного шара. Шар покатится туда, куда кликнул пользо-ватель в последний раз. Из луз будут вылетать шары трёх типов: синие, зелёные и оранжевые. Столкновениекрасного шара с синим означает минус одну жизнь, с зелёным – плюс одну жизнь, оранжевый шар означаетбонус. Если шар игрока сталкивается с оранжевым шаром все шары в определённом радиусе от места столк-новения исчезают и записываются в бонусные очки, за каждый шар по одному очку, при этом шар с которымпроизошло столкновение не считается. Все столкновения – абсолютно упругие, поэтому при столкновенииэнергия сохраняется и шары никогда не остановятся. Если шар попадает в лузу, то он исчезает. Если в лузупопал шар игрока – это означает, что игра окончена. Игрок стартует с несколькими жизнями, когда их чис-ло подходит к нулю игра останавливается. После столкновения с зелёным шаром, шар пропадает, а послестолкновения с синим – нет. В итоге все против игрока, кроме зелёных и оранжевых шаров.

19.1 Основные библиотекиКонтролировать физику игрового мира будет библиотека Chipmunk, а библиотека OpenGL будет рисовать

(конечно если мы её этому научим). Пришло время с ними познакомится.

| 243

Page 244: Ru Haskell Book

Изменяемые значенияПеред тем как мы перейдём к библиотекам нам нужно узнать ещё кое-что. В Haskell мы не можем изменять

значения. Но в С это делается постоянно, а соответственно и в библиотеках написанных на С тоже. Для тогочтобы имитировать в Haskell механизм обновления значений были придуманы специальные типы. Мы можемобъявить изменяемое значение и обновлять его, но только в пределах типа IO.

IORefТип IORef из модуля Data.IORef описывает изменяемые значения:

newIORef :: a -> IO IORef

readIORef :: IORef a -> IO awriteIORef :: IORef a -> a -> IO ()modifyIORef :: IORef a -> (a -> a) -> IO ()

Функция newIORef создаёт изменяемое значение и инициализирует его некоторым значением, кото-рые мы можем считать с помощью функции readIORef или обновить с помощью функций writeIORef илиmodifyIORef. Посмотрим как это работает:

module Main where

import Data.IORef

main = var >>= (\v ->readIORef v >>= print

>> writeIORef v 4>> readIORef v >>= print)where var = newIORef 2

Теперь посмотрим на ответ ghci:

*Main> :l HelloIORef[1 of 1] Compiling Main ( HelloIORef.hs, interpreted )Ok, modules loaded: Main.*Main> main24

Самое время вернуться к главе 16 и вспомнить о do-нотации. Такой императивный код гораздо нагляднееписать так:

main = dovar <- newIORef 2x <- readIORef varprint xwriteIORef var 4x <- readIORef varprint x

Эта запись выглядит как последовательность действий. Не правда ли очень похоже на обычный импера-тивный язык. Такие переменные встречаются очень часто в библиотеках, заимствованных из С.

StateVarВ модуле Data.StateVar определены типы, которые накладывают ограничение на права по чтению и

записи. Мы можем определять переменные доступные только для чтения (GettableStateVar a), только длязаписи (SettableStateVar a) или обычные изменяемые переменные (SetVar a).Операции чтения и записи описываются с помощью классов:

class HasGetter s whereget :: s a -> IO a

class HasSetter s where($=) :: s a -> a -> IO ()

244 | Глава 19: Императивное программирование

Page 245: Ru Haskell Book

Тип IORef принадлежит и тому, и другому классу:

main = dovar <- newIORef 2x <- get varprint xvar $= 4x <- get varprint x

OpenGLOpenGL является ярким примером библиотеки построенной на изменяемых переменных. OpenGL можно

представить как большой конечный автомат. Каждая строчка кода – это запрос на изменение состояния. При-чём этот автомат является глобальной переменной. Его текущее состояние зависит от всей цепочки преды-дущих команд. Параметры рисования задаются глобальными переменными (тип StateVar).

OpenGL не зависит от конкретной оконной системы, она отвечает лишь за рисование. Для того чтобысоздать окно и перехватывать в нём действия пользователя нам понадобится отдельная библиотека. Дляэтого мы воспользуемся GLFW, это библиотека также пришла в Haskell из С. Интерфейсы GLFW и OpenGL оченьпохожи. Мы будем обновлять различные параметры библиотеки с помощью типа StateVar. Давайте создадимокно и закрасим фон белым цветом:

module Main where

import Graphics.UI.GLFWimport Graphics.Rendering.OpenGLimport System.Exit

title = ”Hello OpenGL”

width = 700height = 600

main = doinitializeopenWindow (Size width height) [] WindowwindowTitle $= title

clearColor $= Color4 1 1 1 1

windowCloseCallback $= exitWith ExitSuccessloop

loop = dodisplayloop

display = doclear [ColorBuffer]swapBuffers

Мы инициализируем GLFW, задаём параметры окна. Устанавливаем цвет фона. Цвет имеет четыре пара-метра это RGB-цвета и параметр прозрачности. Затем мы говорим, что программе делать при закрытии окна.Мы устанавливаем функцию обратного вызова (callback) windowCloseCallback. В самом конце мы входим вцикл, который только и делает, что стирает окно цветом фона и делает рабочий буфер видимым. Что такоебуфер? Буфер – это место в котором мы рисуем. У нас есть два буфера. Один мы показываем пользователю,а в другом в это в время рисуем, когда приходит время обновлять картинку мы просто меняем их местамикомандой swapBuffers.Посмотрим, что у нас получилось:

$ ghc --make HelloOpenGL.hs$ ./HelloOpenGL

Нарисуем упрощённое начальное положение нашей игры: прямоугольную рамку и в ней – красный шар:

Основные библиотеки | 245

Page 246: Ru Haskell Book

module Main where

import Graphics.UI.GLFWimport Graphics.Rendering.OpenGL

import System.Exit

title = ”Hello OpenGL”

width, height :: GLsizei

width = 700height = 600

w2, h2 :: GLfloat

w2 = (fromIntegral $ width) / 2h2 = (fromIntegral $ height) / 2

dw2, dh2 :: GLdouble

dw2 = fromRational $ toRational w2dh2 = fromRational $ toRational h2

main = doinitializeopenWindow (Size width height) [] WindowwindowTitle $= title

clearColor $= Color4 1 1 1 1ortho (-dw2-50) (dw2+50) (-dh2-50) (dh2+50) (-1) 1

windowCloseCallback $= exitWith ExitSuccesswindowSizeCallback $= (\size -> viewport $= (Position 0 0, size))

loop

loop = dodisplayloop

display = doclear [ColorBuffer]

color blackline (-w2) (-h2) (-w2) h2line (-w2) h2 w2 h2line w2 h2 w2 (-h2)line w2 (-h2) (-w2) (-h2)

color redcircle 0 0 10

swapBuffers

vertex2f :: GLfloat -> GLfloat -> IO ()vertex2f a b = vertex (Vertex3 a b 0)

-- colors

white = Color4 (0::GLfloat)black = Color4 (0::GLfloat) 0 0 1red = Color4 (1::GLfloat) 0 0 1

-- primitives

line :: GLfloat -> GLfloat -> GLfloat -> GLfloat -> IO ()

246 | Глава 19: Императивное программирование

Page 247: Ru Haskell Book

line ax ay bx by = renderPrimitive Lines $ dovertex2f ax ayvertex2f bx by

circle :: GLfloat -> GLfloat -> GLfloat -> IO ()circle cx cy rad =

renderPrimitive Polygon $ mapM_ (uncurry vertex2f) pointswhere n = 50

points = zip xs ysxs = fmap (\x -> cx + rad * sin (2*pi*x/n)) [0 .. n]ys = fmap (\x -> cy + rad * cos (2*pi*x/n)) [0 .. n]

Рис. 19.1: Начальное положение

Мы рисуем с помощью функции renderPrimitive. Она принимает метку элемента, который мы собира-емся рисовать и набор вершин. Так метка Lines обозначает линии, а метка Polygon – закрашенные много-угольники. В OpenGL нет специальной операции для рисования окружностей, поэтому нам придётся предста-вить окружность в виде многоугольника (circle). Функция ortho устанавливает область видимости рисунка,шесть аргументов функции обозначают пары диапазонов по каждой из трёх координат. При этом вершиныпередаются не списком а в специальном do-блоке. За счёт этого мы можем изменить какие-нибудь парамет-ры OpenGL во время рисования. Обратите внимание на то, как мы изменяем цвет примитива. Перед тем какрисовать примитив мы устанавливаем значение цвета (color).

Анимация

Оживим нашу картинку. При клике мышкой шарик игрока последует в направлении курсора. Для тогочтобы картинка задвигалась нам необходимо обновлять рисунок с определённой частотой. Мы будем регу-лировать частоту обновления с помощью функции sleep, с её помощью мы можем задержать выполнениепрограммы (время измеряется в секундах):

sleep :: Double -> IO ()

За перехват действий пользователя отвечает функции:

getMouseButton :: MouseButton -> IO KeyButtonStatemousePos :: StateVar Position

Функция getMouseButton сообщает текущее состояние кнопок мыши, мы будем перехватывать положениемыши во время нажатия левой кнопки:

Основные библиотеки | 247

Page 248: Ru Haskell Book

onMouse ball = domb <- getMouseButton ButtonLeftwhen (mb == Press) (get mousePos >>= updateVel ball)

Стандартная функция when из модуля Control.Monad выполняет действие только в том случае, если пер-вый аргумент равен True. Для обновления положения и направления скорости шарика нам придётся вос-пользоваться глобальной переменной типа IORef Ball:

data Ball = Ball{ ballPos :: Vec2d, ballVel :: Vec2d}

Код программы:

module Main where

import Control.Applicativeimport Data.IORefimport Graphics.UI.GLFWimport Graphics.Rendering.OpenGLimport System.Exitimport Control.Monad

type Time = Double

title = ”Hello OpenGL”

width, height :: GLsizei

fps :: Intfps = 60

frameTime :: TimeframeTime = 1000 * ((1::Double) / fromIntegral fps)

width = 700height = 600

w2, h2 :: GLfloat

w2 = (fromIntegral $ width) / 2h2 = (fromIntegral $ height) / 2

dw2, dh2 :: GLdouble

dw2 = fromRational $ toRational w2dh2 = fromRational $ toRational h2

type Vec2d = (GLfloat, GLfloat)

data Ball = Ball{ ballPos :: Vec2d, ballVel :: Vec2d}

initBall = Ball (0, 0) (0, 0)

dt :: GLfloatdt = 0.3

minVel = 10

main = doinitializeopenWindow (Size width height) [] WindowwindowTitle $= title

248 | Глава 19: Императивное программирование

Page 249: Ru Haskell Book

clearColor $= Color4 1 1 1 1ortho (-dw2) (dw2) (-dh2) (dh2) (-1) 1

ball <- newIORef initBall

windowCloseCallback $= exitWith ExitSuccesswindowSizeCallback $= (\size -> viewport $= (Position 0 0, size))

loop ball

loop :: IORef Ball -> IO ()loop ball = do

display ballonMouse ballsleep frameTimeloop ball

display ball = do(px, py) <- ballPos <$> get ball(vx, vy) <- ballVel <$> get ballball $= Ball (px + dt*vx, py + dt*vy) (vx, vy)

clear [ColorBuffer]

color blackline (-ow2) (-oh2) (-ow2) oh2line (-ow2) oh2 ow2 oh2line ow2 oh2 ow2 (-oh2)line ow2 (-oh2) (-ow2) (-oh2)

color redcircle px py 10

swapBufferswhere ow2 = w2 - 50

oh2 = h2 - 50

onMouse ball = domb <- getMouseButton ButtonLeftwhen (mb == Press) (get mousePos >>= updateVel ball)

updateVel ball pos = do(p0x, p0y) <- ballPos <$> get ballv0 <- ballVel <$> get ballsize <- get windowSizelet (p1x, p1y) = mouse2canvas size pos

v1 = scaleV (max minVel $ len v0) $ norm (p1x - p0x, p1y - p0y)ball $= Ball (p0x, p0y) v1where norm v@(x, y) = (x / len v, y / len v)

len (x, y) = sqrt (x*x + y*y)scaleV k (x, y) = (k*x, k*y)

mouse2canvas :: Size -> Position -> (GLfloat, GLfloat)mouse2canvas (Size sx sy) (Position mx my) = (x, y)

where d a b = fromIntegral a / fromIntegral bx = fromIntegral width * (d mx sx - 0.5)y = fromIntegral height * (negate $ d my sy - 0.5)

vertex2f :: GLfloat -> GLfloat -> IO ()vertex2f a b = vertex (Vertex3 a b 0)

-- colors... white, black, red

-- primitivesline :: GLfloat -> GLfloat -> GLfloat -> GLfloat -> IO ()circle :: GLfloat -> GLfloat -> GLfloat -> IO ()

Основные библиотеки | 249

Page 250: Ru Haskell Book

Теперь функция display принимает ссылку на глобальную переменную, которая отвечает за движениешарика. Функция mouse2canvas переводит координаты в окне GLFW в координаты OpenGL. В GLFW начало ко-ординат лежит в левом верхнем углу окна и ось Oy направлена вниз. Мы же переместили начало координатв центр окна и ось Oy направлена вверх.Посмотрим что у нас получилось:

$ ghc --make Animation.hs$ ./Animation

ChipmunkКартинка ожила, но шарик движется не реалистично. Он проходит сквозь стены. Добавим в нашу про-

грамму немного физики. Воспользуемся библиотекой Hipmunk

cabal install Hipmunk

Она даёт возможность вызывать из Haskell функции С-библиотеки Chipmunk. Эта библиотека позволя-ет строить двухмерные физические модели. Основным элементом модели является пространство (Space).К нему мы можем добавлять различные объекты. Объект состоит из двух компонент: тела (Body) и формы(Shape). Тело отвечает за такие физические характеристики как масса, момент инерции, восприимчивость ксилам. По форме определяются моменты столкновения тел. Форма может состоять из нескольких примити-вов: окружностей, линий и выпуклых многоугольников. Также мы можем добавлять различные ограничения(Constraint) они имитируют пружинки, шарниры. Мы можем назначать выполнение IO-действий на столк-новения.Опишем в Hipmunk модель шарика бегающего в замкнутой коробке:

module Main where

import Data.StateVarimport Physics.Hipmunk

main = doinitChipmunkspace <- newSpaceinitWalls spaceball <- initBall space initPos initVelloop 100 space ball

loop :: Int -> Space -> Body -> IO ()loop 0 _ _ = return ()loop n space ball = do

showPosition ballstep space 0.5loop (n-1) space ball

showPosition :: Body -> IO ()showPosition ball = do

pos <- get $ position ballprint pos

initWalls :: Space -> IO ()initWalls space = mapM_ (uncurry $ initWall space) wallPoints

initWall :: Space -> Position -> Position -> IO ()initWall space a b = do

body <- newBody infinity infinityshape <- newShape body (LineSegment a b wallThickness) 0elasticity shape $= nearOnespaceAdd space bodyspaceAdd space shape

initBall :: Space -> Position -> Velocity -> IO BodyinitBall space pos vel = do

body <- newBody ballMass ballMomentshape <- newShape body (Circle ballRadius) 0position body $= pos

250 | Глава 19: Императивное программирование

Page 251: Ru Haskell Book

velocity body $= velelasticity shape $= nearOnespaceAdd space bodyspaceAdd space shapereturn body

------------------------------ inits

nearOne = 0.9999ballMass = 20ballMoment = momentForCircle ballMass (0, ballRadius) 0ballRadius = 10

initPos = Vector 0 0initVel = Vector 10 5

wallThickness = 1

wallPoints = fmap (uncurry f) [((-w2, -h2), (-w2, h2)),((-w2, h2), (w2, h2)),((w2, h2), (w2, -h2)),((w2, -h2), (-w2, -h2))]where f a b = (g a, g b)

g (a, b) = H.Vector a b

h2 = 100w2 = 100

Функция initChipmunk инициализирует библиотеку Chipmunk. Она должна быть вызвана один раз долюбой из функций библиотеки Hipmunk. Функции new[Body|Shape|Space] создают объекты модели. Мы сде-лали стены неподвижными, присвоив им бесконечную массу и момент инерции (initWall). Упругость удараопределяется переменной elasticity, она не может быть больше единицы. Единица обозначает абсолютноупругое столкновение. В документации к Hipmunk не рекомендуют присваивать значение равное единицеиз-за возможных погрешностей округления, поэтому мы выбираем число близкое к единице. После иници-ализации элементов модели мы запускаем цикл, в котором происходит обновление модели (step) и печатьположения шарика. Обратите внимание на то, что координаты шарика никогда не выйдут за установленныерамки.Теперь объединим OpenGL и Hipmunk:

module Main where

import Control.Applicative

import Control.Applicativeimport Data.StateVarimport Data.IORefimport Graphics.UI.GLFWimport System.Exitimport Control.Monad

import qualified Physics.Hipmunk as Himport qualified Graphics.UI.GLFW as Gimport qualified Graphics.Rendering.OpenGL as G

title = ”in the box”

------------------------------ inits

type Time = Double

-- frames per secondfps :: Intfps = 60

Основные библиотеки | 251

Page 252: Ru Haskell Book

-- frame time in millisecondsframeTime :: TimeframeTime = 1000 * ((1::Double) / fromIntegral fps)

nearOne = 0.9999ballMass = 20ballMoment = H.momentForCircle ballMass (0, ballRadius) 0ballRadius = 10

initPos = H.Vector 0 0initVel = H.Vector 0 0

wallThickness = 1

wallPoints = fmap (uncurry f) [((-ow2, -oh2), (-ow2, oh2)),((-ow2, oh2), (ow2, oh2)),((ow2, oh2), (ow2, -oh2)),((ow2, -oh2), (-ow2, -oh2))]where f a b = (g a, g b)

g (a, b) = H.Vector a b

dt :: Doubledt = 0.5

minVel :: DoubleminVel = 10

width, height :: Double

height = 500width = 700

w2, h2 :: Double

h2 = height / 2w2 = width / 2

ow2, oh2 :: Double

ow2 = w2 - 50oh2 = h2 - 50

data State = State{ stateBall :: H.Body, stateSpace :: H.Space}

ballPos :: State -> StateVar H.PositionballPos = H.position . stateBall

ballVel :: State -> StateVar H.VelocityballVel = H.velocity . stateBall

main = doH.initChipmunkinitGLFWstate <- newIORef =<< initStateloop state

loop :: IORef State -> IO ()loop state = do

display stateonMouse statesleep frameTimeloop state

252 | Глава 19: Императивное программирование

Page 253: Ru Haskell Book

simulate :: State -> IO Timesimulate a = do

t0 <- get G.timeH.step (stateSpace a) dtt1 <- get G.timereturn (t1 - t0)

initGLFW :: IO ()initGLFW = do

G.initializeG.openWindow (G.Size (d2gli width) (d2gli height)) [] G.WindowG.windowTitle $= titleG.windowCloseCallback $= exitWith ExitSuccessG.windowSizeCallback $= (\size -> G.viewport $= (G.Position 0 0, size))G.clearColor $= G.Color4 1 1 1 1G.ortho (-dw2) (dw2) (-dh2) (dh2) (-1) 1where dw2 = realToFrac w2

dh2 = realToFrac h2

initState :: IO StateinitState = do

space <- H.newSpaceinitWalls spaceball <- initBall space initPos initVelreturn $ State ball space

initWalls :: H.Space -> IO ()initWalls space = mapM_ (uncurry $ initWall space) wallPoints

initWall :: H.Space -> H.Position -> H.Position -> IO ()initWall space a b = do

body <- H.newBody H.infinity H.infinityshape <- H.newShape body (H.LineSegment a b wallThickness) 0H.elasticity shape $= nearOneH.spaceAdd space bodyH.spaceAdd space shape

initBall :: H.Space -> H.Position -> H.Velocity -> IO H.BodyinitBall space pos vel = do

body <- H.newBody ballMass ballMomentshape <- H.newShape body (H.Circle ballRadius) 0H.position body $= posH.velocity body $= velH.elasticity shape $= nearOneH.spaceAdd space bodyH.spaceAdd space shapereturn body

--------------------------------- graphics

display state = dodrawState =<< get statesimTime <- simulate =<< get statesleep (max 0 $ frameTime - simTime)

drawState :: State -> IO ()drawState st = do

pos <- get $ ballPos stG.clear [G.ColorBuffer]drawWallsdrawBall posG.swapBuffers

drawBall :: H.Position -> IO ()drawBall pos = do

Основные библиотеки | 253

Page 254: Ru Haskell Book

G.color redcircle x y $ d2gl ballRadiuswhere (x, y) = vec2gl pos

drawWalls :: IO ()drawWalls = do

G.color blackline (-dow2) (-doh2) (-dow2) doh2line (-dow2) doh2 dow2 doh2line dow2 doh2 dow2 (-doh2)line dow2 (-doh2) (-dow2) (-doh2)where dow2 = d2gl ow2

doh2 = d2gl oh2

onMouse state = domb <- G.getMouseButton ButtonLeftwhen (mb == Press) (get G.mousePos >>= updateVel state)

updateVel state pos = dosize <- get G.windowSizest <- get statep0 <- get $ ballPos stv0 <- get $ ballVel stlet p1 = mouse2canvas size posballVel st $=

H.scale (H.normalize $ p1 - p0) (max minVel $ H.len v0)

mouse2canvas :: G.Size -> G.Position -> H.Vectormouse2canvas (G.Size sx sy) (G.Position mx my) = H.Vector x y

where d a b = fromIntegral a / fromIntegral bx = width * (d mx sx - 0.5)y = height * (negate $ d my sy - 0.5)

vertex2f :: G.GLfloat -> G.GLfloat -> IO ()vertex2f a b = G.vertex (G.Vertex3 a b 0)

vec2gl :: H.Vector -> (G.GLfloat, G.GLfloat)vec2gl (H.Vector x y) = (d2gl x, d2gl y)

d2gl :: Double -> G.GLfloatd2gl = realToFrac

d2gli :: Double -> G.GLsizeid2gli = toEnum . fromEnum . d2gl

...

Функции не претерпевшие особых изменений пропущены. Теперь наше глобальное состояние (State)содержит тело шара (оно пригодится нам для вычисления его положения) и пространство, в котором живётнаша модель. Стоит отметить функцию simulate. В ней происходит обновление состояния модели. Приэтом мы возвращаем время, которое ушло на вычисление этой функции. Оно нужно нам для того, чтобыпоказывать новые кадры равномерно. Мы вычтем время симуляции из общего времени, которое мы можемпотратить на один кадр (frameTime).

19.2 Боремся с IOКажется, что мы попали в какой-то другой язык. Это совсем не тот элегантный Haskell, знакомый нам по

предыдущим главам. Столько do и IO разбросано по всему коду. И такой примитивный результат в итоге.Если так будет продолжаться и дальше, то мы можем не вытерпеть и бросить и нашу задачу и Haskell…Не отчаивайтесь!Давайте лучше подумаем как свести этот псевдо-Haskell к минимуму. Подумаем какие источники IO

точно будут в нашей программе. Это инициализация GLFW и Hipmunk, клики мышью, обновление модели вHipmunk, также для рисования нам придётся считывать положения шаров. Нам придётся удалять и создавать

254 | Глава 19: Императивное программирование

Page 255: Ru Haskell Book

новые шары, добавляя их к пространству модели. Также в IO происходит отрисовка игры. Hipmunk будет кон-тролировать столкновения шаров, и эти данные нам тоже надо будет считывать из глобальных переменных.Сколько всего! Голова идёт кругом.Но помимо всего этого у нас есть логика игры. Логика игры отвечает за реакцию игрового мира на раз-

личные события. Например столкновение с ”плохим” шаром влечёт к уменьшению жизней, если игрок стал-кивается с бонусным шаром, определённые шары необходимо удалить. Приходит момент и мы выпусткаемновый шар из лузы новый шар. Давайте подумаем как сохранить логику игры в чистоте.Тип IO обычно отвечает за связь с внешним миром, это глаза, уши, руки и ноги программы. Через IO мы

получаем информацию из внешнего мира и отправляем её обратно. Но в нашем случае он проник в сердцепрограммы. За обновление объектов отвечает насыщенная IO библиотека Hipmunk.Мы постараемся побороться с IO-кодом так. Сначала мы выделем те параметры, которые могут быть

обновлены чистыми функциями. Это все те параметры, для которых не нужен Hipmunk. Этот шаг разбиваетнаш мир на два лагеря: ”чистый” и ”грязный”:

data World = World{ worldPure :: Pure, worldDirty :: Dirty }

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

data Query = Remove Ball | HeroVelocity H.Velocity | MakeBall Freq

data Event = Touch Ball | UserClick H.Position

data Sense = Sense{ senseHero :: HeroBall, senseBalls :: [Ball] }

Через Query чистые данные могут рассказать грязным о том, что необходимо удалить шар из игры, об-новить скорость шара игрока или создать новый шар (Freq отвечает за параметры создания шара). Грязныеданные могут рассказать чистым на языке Event и Sense о том, что один из шаров коснулся до шара иг-рока, или игрок кликнул мышкой в определённой точке. Также мы сообщаем все обновлённые положенияпараметры шаров в типе Sense. Тип Event отвечает за события, которые происходят иногда, а тип Sense зате параметры, которые мы наблюдаем непрерывно (это типы глазарук), Query – это язык действий (это типруконог). Нам понадобится ещё один маленький язык, на котором мы будем объясняться с OpenGL.

data Picture = Prim Color Primitive| Join Picture Picture

data Primitive = Line Point Point | Circle Point Radius

data Point = Point Double Doubletype Radius = Double

data Color = Color Double Double Double

Эти три языка станут барьером, которым мы ограничим влияние IO. У нас будут функции:

percept :: Dirty -> IO (Sense, [Event])updatePure :: Sense -> [Event] -> Pure -> (Pure, [Query])react :: [Query] -> Dirty -> IO DirtyupdateDirty :: Dirty -> IO Dirtypicture :: Pure -> Picturedraw :: Picture -> IO ()

Вся логика игры будет происходить в чистой функции updatePure, обновлять модель мира мы будем вupdateDirty. Давайте опять начнём проектироваание сверху-вниз. С этими функциями мы уже можем напи-сать основную функцию цикла игры:

loop :: IORef World -> IO ()loop worldRef = do

world <- get worldRefdrawWorld world

Боремся с IO | 255

Page 256: Ru Haskell Book

(world, dt) <- updateWorld worldworldRef $= worldG.addTimerCallback (max 0 $ frameTime - dt) $ loop worldRef

updateWorld :: World -> IO (World, Time)updateWorld world = do

t0 <- get G.elapsedTime(sense, events) <- percept dirtylet (pure’, queries) = updatePure sense events puredirty’ <- updateDirty =<< react queries dirtyt1 <- get G.elapsedTimereturn (World pure’ dirty’, t1 - t0)where dirty = worldDirty world

pure = worldPure world

drawWorld :: World -> IO ()drawWorld = draw . picture . worldPure

19.3 Определяемся с типамиДавайте подумаем, из чего состоят типы Dirty и Pure. Начнём с Pure. Там точно будет вся информация

необходимая нам для рисования картинки (ведь функция picture определена на Pure). Для рисования намнеобходимо знать положения всех шаров и их типы (они определяют цвет). На картинке мы будем показыватьразную статистику (данные о жизнях, бонусные очки). Также из типа Pure мы будем управлять созданиемшаров. Так мы приходим к типу:

data Pure = Pure{ pureScores :: Scores, pureHero :: HeroBall, pureBalls :: [Ball], pureStat :: Stat, pureCreation :: Creation}

Что нам нужно знать о шаре героя? Нам нужно его положение для отрисовки и модуль вектора скорости(он понадобится нам при обновлении вектора скорости шара игрока):

data HeroBall = HeroBall{ heroPos :: H.Position, heroVel :: H.CpFloat}

Для остальных шаров нам нужно знать только тип шара, его положение и идентификатор шара. По иден-тификатору потом мы сможем понять какой шар удалить из грязных данных:

data Ball = Ball{ ballType :: BallType, ballPos :: H.Position, ballId :: Id}

data BallType = Hero | Good | Bad | Bonusderiving (Show, Eq, Enum)

type Id = Int

Статистика игры состоит из числа жизней и бонусных очков:

data Scores = Scores{ scoresLives :: Int, scoresBonus :: Int}

Как будет происходить создание новых шаров? Если плохих шаров будет слишком много, то играть будетне интересно, игрок слишком быстро проиграет. Если хороших шаров будет слишком много, то игроку также

256 | Глава 19: Императивное программирование

Page 257: Ru Haskell Book

быстро надоест. Будет очень легко. Нам необходимо поддерживать определённый баланс шаров. Созданиешаров будет происходить случайным образом через равные промежутки времени, но создание нового шарабудет зависеть от пропорции шаров на доске в данный момент. Если у нас слишком много плохих шаров,то скорее всего мы создадим хороший шар и наоборот. Если общее число шаров велико, то мы не будемусложнять игроку жизнь новыми шарами, дождёмся пока какие-нибудь шары не покинут пределы поля илине будут уничтожены игроком. Эти рассуждения приводят нас к типам:

data Creation = Creation{ creationStat :: Stat, creationGoalStat :: Stat, creationTick :: Int}

data Stat = Stat{ goodCount :: Int, badCount :: Int, bonusCount :: Int}

data Freq = Freq{ freqGood :: Float, freqBad :: Float, freqBonus :: Float}

Поле creationStat содержит текущее число шаров на поле, поле creationGoalStat – число шаров, к ко-торому мы стремимся. Значение типа Freq содержит веса вероятностей создания нового шара определённоготипа. На каждом шаге мы будем прибавлять единицу к creationTiсk, как только оно достигнет определён-ного значения мы попробуем создать новый шар.Перейдём к грязным данным. Там мы будем хранить информацию, необходимую для обновления модели

в Hipmunk, и значение, в которое GLFW будет записывать состояние мыши, также мы будем следить за тем,кто столкнулся с шаром игрока в данный момент:

data Dirty = Dirty{ dirtyHero :: Obj, dirtyObjs :: IxMap Obj, dirtySpace :: H.Space, dirtyTouchVar :: Sensor H.Shape, dirtyMouse :: Sensor H.Position}

data Obj = Obj{ objType :: BallType, objShape :: H.Shape, objBody :: H.Body}

type Sensor a = IORef (Maybe a)

Особая структура IxMap отвечает за хранение значений вместе с индексами. Пока остановимся на самомпростом представлении:

type IxMap a = [(Id, a)]

19.4 Структура проектаНаметим структуру проекта. У нас уже есть модуль Types.hs. Основной цикл игры будет описан в модуле

Loop.hs. Общие функции обновления состояния будут определены в World.hs, также у нас будет два модуляотвечающие за обновление чистых и грязных данных – Pure.hs и Dirty.hs. Мы выделим отдельный модульдля описания всех констант игры (Inits.hs). Так нам будет удобно настроить игру, когда мы закончим скодом. Отдельный модуль Utils будет содержать все функции общего назначения, преобразования междутипами OpenGL и Hipmunk.

Структура проекта | 257

Page 258: Ru Haskell Book

19.5 Детализируем функции обновления состояния игрыНачнём с восприятия:

module World where

import qualified Physics.Hipmunk as H

import Data.Maybeimport Typesimport Utils

import Pureimport Dirty

percept :: Dirty -> IO (Sense, [Event])percept a = do

hero <- obj2hero $ dirtyHero aballs <- mapM (uncurry obj2ball) $ setIds dirtyObjs aevts1 <- fmap maybeToList $ getTouch (dirtyTouchVar a) $ dirtyObjs aevts2 <- fmap maybeToList $ getClick $ dirtyMouse areturn $ (Sense hero balls, evts1 ++ evts2)where setIds = zip [0..]

-- в Dirty.hsobj2hero :: Obj -> IO HeroBallobj2ball :: Id -> Obj -> IO BallgetTouch :: Sensor H.Shape -> IxMap Obj -> IO (Maybe Event)getClick :: Sensor H.Position -> IO (Maybe Event)

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

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

updatePure :: Sense -> [Event] -> Pure -> (Pure, [Query])updatePure s evts = updateEvents evts . updateSenses s

-- в Pure.hsupdateSenses :: Sense -> Pure -> PureupdateEvents :: [Event] -> Pure -> (Pure, [Query])

В функции react мы предполагаем, что реакции мира на события независимы друг от друга. foldQuery –функция свёртки для типа Query.

import Control.Monad...

react :: [Query] -> Dirty -> IO Dirtyreact = foldr (<=<) return

. fmap (foldQuery removeBall heroVelocity makeBall)

-- в Dirty.hsremoveBall :: Ball -> Dirty -> IO DirtyheroVelocity :: H.Velocity -> Dirty -> IO DirtymakeBall :: Freq -> Dirty -> IO Dirty

Обратите внимание на то, как мы воспользовались функциями foldr, return и <=< для того чтобы нани-зать друг на друга функции типа Dirty -> IO Dirty. Напомню, что функция <=< – это аналог композициидля монадных функций.Обновление модели:

updateDirty :: Dirty -> IO DirtyupdateDirty = stepDirty dt

-- в Dirty.hs

258 | Глава 19: Императивное программирование

Page 259: Ru Haskell Book

stepDirty :: H.Time -> Dirty -> IO Dirty

-- в Inits.hsdt :: H.Timedt = 0.5

Функции рисования поместим в отдельный модуль Graphics.hs

-- переместим из Loop.hs в World.hsdrawWorld :: World -> IO ()drawWorld = draw . picture . worldPure

-- в Graphics.hsdraw :: Picture -> IO ()

-- в Pure.hspicture :: Pure -> Picture

Добавим функцию инициализации игры:

initWorld :: IO WorldinitWorld = do

dirty <- initDirty(sense, events) <- percept dirtyreturn $ World (initPure sense events) dirty

-- в Dirty.hsinitDirty :: IO Dirty-- в Pure.hsinitPure :: Sense -> [Event] -> Pure

19.6 Детализируем дальшеВот так на самом интересном месте…Мы вынуждены прерваться. Я надеюсь, что вы уловили основную

идею метода и сможете закончить эту игру самостоятельно. Вся логика игры будет описана в модуле Pure.hs.Причём в этом модуле будут только чистые функции. Осталось примерно 1000 строк кода. Я не буду выпи-сывать своё решение, если вы где-то запнётесь или у вас что-то не будет получаться, вы можете свериться сним (оно входит в код, что прилагается с книгой).

19.7 Краткое содержаниеВ этой главе мы посмотрели на две интересные библиотеки. Физический движок Hipmunk и графическую

библиотеку OpenGL и узнали метод укрощения императивного кода. Мы разделили состояние игры на двечасти. В одну поместили все те параметры, для которых невозможно обойтись без IO-функций, а в другойте параметры, которые необходимы для реализации логики игры. Все функции, отвечающие за логику игрыявляются чистыми. Параметры императивной части не обновляются сразу, сначала мы делаем с них снимок,потом передаём этот снимок в чистую часть, и она разбирается с тем как их обновлять. Части общаются междусобой на специальных маленьких языках, которые закодированы в типах. Это язык наблюдений (Event), языкреакций (Query) и язык отрисовки игрового мира (Picture).

19.8 УпражненияЗакончите код игры. Или, возможно, при знакомстве с Hipmunk у вас появилась идея новой игры с неве-

роятной динамикой. Ещё лучше! Напишите её. При этом продумайте проект игры так, чтобы IO-типы неразбежались по всей программе.

Детализируем дальше | 259

Page 260: Ru Haskell Book

Глава 20

Музыкальный пример

В этой главе мы напишем музыкальный секвенсор. Мы будем переводить нотную запись в midi-файл спомощью библиотеки HCodecs. Она предоставляет возможность создания midi-файлов по описанию в Haskell.При этом описание напоминает описание самого формата midi. Мы же хотим подняться уровнем выше иописывать музыку нотами и композицией нот.

20.1 Музыкальная нотацияДля начала зададимся выясним: а что же такое музыка с точки зрения нашего секвенсора? Мы ищем

представление музыки, термины, в которых было бы удобно мыслить композитору. При этом необходимопонимать, что наш поиск ограничен средствами низкоуровневого представления музыки. В нашем случаеэто midi-файл. Так например мы можем сразу отбросить представление в виде сигналов, последовательностисэмплов, поскольку мы не сможем реализовать это представление в рамках midi. За ответом обратимся кистории.

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

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

Рис. 20.1: Буквенные обозначения высоты ноты

По длительности ноты различают на: целые, половины, четверти, восьмые, шестнадцатые и так далее.Каждая последующая длительность в два раза меньше предыдущей. Длительность измеряется в долях оттакта. Такты обозначаются сплошной линией, которая перечёркивает все пять линеек нотного стана. Повысоте ноты, зависят от двух целых чисел, это номер октавы и номер ступени лада. В ладе обычно всего 12ступеней. Их обозначают разными именами. Например в латинской нотации их обозначают так:

0 1 2 3 4 5 6 7 8 9 10 11C C♯ D D♯ E F F♯ G G♯ A A♯ BC D♭ D E♭ E F G♭ G A♭ A B♭ Bdo re mi fa sol la ti

В самом нижнем ряду расположены имена нот. Во втором и четвёртом – обозначения нот с диезами ис бемолями. Одна и та же нота может обозначаться по-разному. Буквами обозначают ноты тональности домажор (это семь букв для семи нот), а остальные ноты получают повышением на один шаг с помощью знакадиез ♯ или понижением на один шаг с помощью знака бемоль ♭.

260 | Глава 20: Музыкальный пример

Page 261: Ru Haskell Book

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

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

Протокол midiПротокол midi появился в ответ на бурное развитие синтезаторов. Каждый из синтезаторов предлагал

свои тембры, при этом люди задумались, а нужна ли синтезатору клавиатура? Вопрос кажется абсурдным,если мы думаем об одном синтезаторе, но представьте, что у вас их десять, в каждом свой чем-то особенныйтембр. При этом нам нужно десять разных тембров, но мы вынужденны таскать за собой десять примерноодинаковых клавиатур. Для того чтобы отделить тембр от управления (нажатия на клавиши игроком) былпридуман протокол midi. Протокол midi описывает специфическую для нажатия на клавиши информацию.Производители тембров или генераторов тона, могут научить генератор тона понимать midi. При этом мыможем сделать отдельную клавиатуру, которая не имеет собственного генератора тона, но умеет посылатьсообщения протокола midi, так мы сможем управлять десятью генераторами тона от разных производителейс помощью одной клавиатуры. Такие клавиатуры называют midi-клавиатурами.Познакомимся с терминологией midi. Протокол midi рассчитан на управление синтезаторами в режиме

реального времени. Можно сказать, что midi-файл – это история концерта или выступления, низкоуровневаянотная запись. Каждое движение игрока кодируется событием. Например нажатие на клавишу, отпусканиеклавиши, сила давления на клавишу в определённый момент времени, нажатие педали, поворот реле илисмена тэмбра.Протокол midi изначально задумывался как расширяемый протокол. Каждый производитель тембров

имеет возможность добавить какие-то особенные настройки. При этом те сообщения, которые данный ге-нератор тона не понимает просто игнорируются. Наш секвенсор будет понимать такие события как нажатиена клавишу и отпускание клавиши. Также у нас будут разные инструменты.Установим библиотеку HCodecs с Hackage:

cabal install HCodecs

Теперь заглянем на страницу документации этого пакета (на сайте Hackage), нас интересует модульCodec.Midi, ведь мы хотим создавать именно midi-файлы. Здесь мы видим описание протокола midi, за-кодированное в типах. Посмотрим на тип Message, он описывает midi-сообщения. В первую очередь нас ин-тересуют конструкторы:

NoteOn {channel :: !Channel,key :: !Key,velocity :: !Velocity }

NoteOff {channel :: !Channel,key :: !Key,velocity :: !Velocity }

Восклицательные знаки перед типами означают взрывные шаблоны, о которых мы говорили в главах оленивых вычислениях. Конструктор NoteOn обозначает нажатие клавиши на канале Channel с высотой Key иуровнем громкости Velocity. Конструктор NoteOff обозначает отпускание клавиши, параметры имеют тотже смысл, что и в случае NoteOn.Думаю что такое высота и громкость примерно понятно, но что такое канал? Считается, что один испол-

нитель может управлять сразу несколькими генераторами тона. Управление распределяется по каналам. Накаждом канале мы можем управлять отдельным инструментом. Немного о высоте и громкости. Они кодиру-ются целыми числами из диапазона от 0 до 127. Ноте до первой октавы (C) соответствует цифра 60, ноте ляпервой октавы (A) соответствует номер 69. Одно число кодирует сразу и октаву и ступень лада.Может показаться странным параметр Velocity в конструкторе NoteOff, он обозначает отпускание клави-

ши с определённой громкостью. Обычно этот параметр игнорируется и в него записывают среднее значение64 или начальное значение 0.Также мы будем играть разными инструментами. Инструменты в протоколе midi называются програм-

мами. Мы можем установить определённый инструмент на данном канале с помощью сообщения:

Музыкальная нотация | 261

Page 262: Ru Haskell Book

ProgramChange {channel :: !Channel,preset :: !Preset }

Целое число Preset указывает на код инструмента. Теперь посмотрим, что же такое midi-файл:data Midi = Midi {

fileType :: FileType,timeDiv :: TimeDiv,tracks :: [Track Ticks] }

midi-файл состоит из трёх значений. Это обозначение типа файла:data FileType = SingleTrack | MultiTrack | MultiPattern

По типу midi-файлы могут различаться на файлы с одним треком, файлы с несколькими треками, ифайлы, которые содержат группы треков, которые называют узорами (pattern). По смыслу трек соответствуетпартии инструмента.Тип TimeDiv кодирует скорость записи сообщений. Различают два варианта:

data TimeDive = TicksPerBeat Int| TicksPerSecond Int Int

Первый конструктор говорит о том, что разрешение времени закодировано в формате PPQN, он указы-вает на число ударов в одной четвертной длительности. Второй конструктор говорит о том, что разрешениекодируется в формате SMPTE, оно указывает на число кадров в секунде.Теперь посмотрим, что такое трек:

type Track a = [(a, Message)]

Трек это список событий с временными отсчётами. Время в midi отсчитывается относительно предыдуще-го события. Например в следующей записи три события произошли одновременно и затем спустя 10 тактовпроизошли ещё два события:[(0, e1), (0, e2), (0, e3), (10, e4), (0, e5)]

20.2 Музыкальная запись в виде событийПисать музыку в виде событий midi очень неудобно, пусть даже и через HCodecs, необходимо придумать

надстройку над протоколом midi. Я долго думал об этом и в итоге пришёл к выводу, что наиболее простойи податливый способ представления музыки на нотном уровне реализован в языке Csound. Там ноты пред-ставлены в виде последовательности событий. Каждое событие начинается в определённый момент и длитсянекоторое время. Событие содержит код инструмента и набор параметров, которые могут включать в себягромкость, высоту звука и какие-то специфические для данного инструмента настройки. Обязательнымипараметрами события являются лишь номер инструмента, который играет ноту, начало события и длитель-ность события. Мы ослабим эти ограничения. Событие будет содержать лишь время начала, длительность инекоторое содержание.data Event t a = Event {

eventStart :: t,eventDur :: t,eventContent :: a} deriving (Show, Eq)

Параметр t символизирует время, а параметр a – некоторое содержание события. Мы будем говорить,что в некоторый момент времени произошло значение типа a и оно длилось некоторое время. Треком мыбудем называть набор событий, которые длятся определённой время:data Track t a = Track {

trackDur :: t,trackEvents :: [Event t a]}

Первый параметр указывает на общую длительность трека, а второй содержит события, которые про-изошли. Мы явно указываем длительность трека для того, чтобы иметь возможность представить тишину.Значение тишины будет выглядеть так:silence t = Track t []

Этим мы говорим, что ничего не произошло в течение t единиц времени.

262 | Глава 20: Музыкальный пример

Page 263: Ru Haskell Book

Преобразование событий во времениНаши события привязаны ко времени. Мы можем ввести линейные операции, которые будут изменять

расположение событий во времени. Самый простой способ изменения положения это задержка. Мы можемзадержать появление события, прибавив какое-нибудь число ко времени начала события:

delayEvent :: Num t => t -> Event t a -> Event t adelayEvent d e = e{ eventStart = d + eventStart e }

Ещё одно простое преобразование заключается в изменении масштаба времени, в музыке или анимацииэтой операции соответствует перемотка. Событие начинает происходить быстрее или медленнее:

stretchEvent :: Num t => t -> Event t a -> Event t astretchEvent s e = e{

eventStart = s * eventStart e,eventDur = s * eventDur e }

Для изменения масштаба времени мы умножили временные параметры на число s. Эти операции мыможем перенести и на значения типа Track.

delayTrack :: Num t => t -> Track t a -> Track t adelayTrack d (Track t es) = Track (t + d) (map (delayEvent d) es)

stretchTrack :: Num t => t -> Track t a -> Track t astretchTrack s (Track t es) = Track (t * s) (map (stretchEvent s) es)

Класс преобразований во времениУ нас есть аналогичные операции преобразования во времени для событий и треков, это говорит о том,

что мы можем ввести специальный класс, который объединит в себе эти операции. Назовём его классомTemporal (временной):

class Temporal a wheretype Dur a :: *dur :: a -> Dur adelay :: Dur a -> a -> astretch :: Dur a -> a -> a

В этом классе определён один тип, который обозначает размерность времени, и три метода в дополнениик методам delay и stretch мы добавим метод dur, мы будем считать, что всё что происходит во времениконечно и с помощью метода dur мы всегда можем узнать протяжённость значения их класса Temporal вовремени. Для определения этого класса нам придётся подключить расширение TypeFamilies. Теперь мылегко можем определить экземпляры класса Temporal для Event и Track:

instance Num t => Temporal (Event t a) wheretype Dur (Event t a) = tdur = eventDurdelay = delayEventstretch = stretchEvent

instance Num t => Temporal (Track t a) wheretype Dur (Track t a) = tdur = trackDurdelay = delayTrackstretch = stretchTrack

Композиция трековОпределим две полезные в музыке операции: параллельную и последовательную композицию треков. В

параллельной композиции мы играем два трека одновременно:

(=:=) :: Ord t => Track t a -> Track t a -> Track t aTrack t es =:= Track t’ es’ = Track (max t t’) (es ++ es’)

Теперь общая длительность трека равна длительности большего из треков, а события включают в себясобытия каждого из треков. С помощью преобразований во времени мы можем определить последовательнуюкомпозицию, для этого мы сместим второй трек на длину первого и сыграем их одновременно:

Музыкальная запись в виде событий | 263

Page 264: Ru Haskell Book

(+:+) :: (Ord t, Num t) => Track t a -> Track t a -> Track t a(+:+) a b = a =:= delay (dur a) b

При этом у нас как раз и получится, что мы сначала сыграем целиком трек a, а затем трек b. Теперьопределим аналоги операций =:= и +:+ для списков:

chord :: (Num t, Ord t) => [Track t a] -> Track t achord = foldr (=:=) (silence 0)

line :: (Num t, Ord t) => [Track t a] -> Track t aline = foldr (+:+) (silence 0)

Мы можем определить в терминах этих операций цикличный повтор событий:

loop :: (Num t, Ord t) => Int -> Track t a -> Track t aloop n t = line $ replicate n t

Экземпляры стандартных классовМы можем сделать тип трек экземпляром класса Functor:

instance Functor (Event t) wherefmap f e = e{ eventContent = f (eventContent e) }

instance Functor (Track t) wherefmap f t = t{ trackEvents = fmap (fmap f) (trackEvents t) }

Мы можем также определить экземпляр для класса Monoid. Параллельная композиция будет операциейобъединения, а нейтральным элементом будет тишина, которая длится ноль единиц времени:

instance (Ord t, Num t) => Monoid (Track t a) wheremappend = (=:=)mempty = silence 0

20.3 Ноты в midiС помощью типа Track мы можем описывать всё, что имеет свойство случаться во времени и длиться,

мы можем описывать наборы событий. Операции из класса Temporal и операции последовательной и парал-лельной композиции дают нам возможность собирать сложные наборы событий из простейших. Но для тогочтобы это стало музыкой, нам не хватает нот.Так построим их. Поскольку мы собираемся играть музыку в midi, наши ноты будут содержать только три

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

data Note = Note {noteInstr :: Instr,noteVolume :: Volume,notePitch :: Pitch,isDrum :: Bool }

Итак нота содержит код инструмента, громкость и высоту и ещё один параметр. По последнему пара-метру можно узнать сыграна нота на барабане или нет. В midi ноты для ударных обрабатываются особымобразом. Десятый канал выделен под ударные, при этом номер инструмента игнорируется, а вместо этоговысота звука кодирует номер ударного инструмента. Теперь определимся с типами параметров:

type Instr = Inttype Volume = Inttype Pitch = Int

Целые числа соответствуют целым числам в протоколе midi. Значения для типов Volume и Pitch лежат вдиапазоне от 0 до 127.Введём специальное обозначение для музыкального типа Track:

type Score = Track Double Note

264 | Глава 20: Музыкальный пример

Page 265: Ru Haskell Book

Синонимы для нотВысота нотыМузыкантам ближе буквенные обозначения для нот нежели коды midi. Определим удобные синонимы:

note :: Int -> Scorenote n = Track 1 [Event 0 1 (Note 0 64 (60+n) False)]

Эта функция строит трек, который содержит одну ноту. Нота длится одну целую длительность играетсяна инструменте с кодом 0, на средней громкости. Параметр функции задаёт смещение от ноты до первойоктавы. Определим остальные ноты:

a, b, c, d, e, f, g,as, bs, cs, ds, es, fs, gs,af, bf, cf, df, ef, ff, gf :: Score

c = note 0; cs = note 1; d = note 2; ds = note 3;...

Первая буква содержит буквенное обозначение ноты, а вторая либо s (от англ. sharp диез) или f (отангл. flat бемоль). Все эти ноты находятся в первой октаве, но смещением высоты на 12 единиц мы легкоможем смещать эти ноты в любую другую октаву:

higher :: Int -> Score -> Scorehigher n = fmap (\a -> a{ notePitch = 12*n + notePitch a })

lower :: Int -> Score -> Scorelower n = higher (-n)

high :: Score -> Scorehigh = higher 1

low :: Score -> Scorelow = lower 1

С помощью этих функций мы легко можем смещать группы нот в любую октаву. Функция higher прини-мает число октав, на которые необходимо сместить вверх высоту во всех нотах трека. Смещение высоты на12 определяет смещение на одну октаву. Остальные функции определены в через функцию higher.

Длительность нотыПока что наши ноты длятся 1 единицу времени. Но нам бы хотелось иметь в распоряжении и другие дли-

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

bn, hn, qn, en, sn :: Score -> Score

-- (brewis note) (half note) (quater note)bn = stretch 2; hn = stretch 0.5; qn = stretch 0.25;

-- (eighth note) (sizth note)en = stretch 0.125; sn = stretch 0.0625;

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

Громкость нотыПока мы умеем создавать ноты средней громкости, но мы можем определить преобразователи на манер

тех, что изменяли высоту звука октавами:

louder :: Int -> Score -> Scorelouder n = fmap $ \a -> a{ noteVolume = n + noteVolume a }

quieter :: Int -> Score -> Scorequieter n = louder (-n)

Ноты в midi | 265

Page 266: Ru Haskell Book

Смена инструментаИзначально мы создаём ноты, которые играются на инструменте с кодом 0, в протоколе General Midi этот

номер соответствует роялю. Но с помощью класса Functor мы легко можем изменить инструмент:instr :: Int -> Score -> Scoreinstr n = fmap $ \a -> a{ noteInstr = n, isDrum = False }

drum :: Int -> Score -> Scoredrum n = fmap $ \a -> a{ notePitch = n, isDrum = True }

Согласно протоколу midi в случае ударных инструментов высота звука кодирует инструмент. Поэтомув функции drum мы изменяем именно поле notePitch. Создадим также несколько синонимов для созданиянот, которые играются на барабанах. В этом случае нам не важна высота звука но важна громкость:bam :: Int -> Scorebam n = Track 1 [Event 0 1 (Note 0 n 35 True)]

Номер 35 кодирует ”бочку”.

ПаузыСлово silence верно отражает смысл, но оно слишком длинное. Давайте определим несколько синони-

мов:rest :: Double -> Scorerest = silence

wnr = rest 1; bnr = bn wnr; hnr = hn wnr;qnr = qn wnr; enr = en wnr; snr = sn wnr;

20.4 Перевод в midiТеперь мы можем составить какую нибудь мелодию:

q = line [c, c, hn e, hn d, bn e, chord [c, e]]

Мыможем составлять мелодии, но пока мы не умеем их интерпретировать. Для этого нам нужно написатьфункцию:render :: Score -> Midi

Мы реализуем простейший случай. Будем считать, что у нас только 15 инструментов, а все остальныеинструменты – ударные. Мы запишем нашу музыку на один трек midi-файла, распределив 15 неударныхинструментов по разным каналам. Ещё одно упрощение заключается в том, что мы зададим фиксированноеразрешение по времени для всех возможных мелодий. Будем считать, что 96 ударов для одной четверти намдостаточно. Принимая во внимания эти посылки мы можем написать такую функцию:import qualified Codec.Midi as M

render :: Score -> Midirender s = M.Midi M.SingleTrack (M.TicksPerBeat divisions) [toTrack s]

divisions :: M.Ticksdivisions = 96

toTrack :: Score -> M.TracktoTrack = undefined

Мы загрузили модуль Codec.Midi под псевдонимом M, так мы сможем отличать низкоуровневые опре-деления от тех, что мы определили сами. Теперь перед каждым именем из модуля Codec.Midi необходимописать приставку M.В нашей упрощённой реализации на одном канале может играть только один инструмент. В самом начале

мы назначим инструмент на канал с помощью сообщения ProgramChange. Для этого нам необходимо понятькакому инструменту какой канал соответствует. В библиотеке HCodecs каналы идут от нуля до 15. Девятыйканал предназначен для ударных. Представим, что у нас есть функция, которая распределяет нотную записьпо инструментам:

266 | Глава 20: Музыкальный пример

Page 267: Ru Haskell Book

type MidiEvent = Event Double Note

groupInstr :: Score -> ([[MidiEvent]], [MidiEvent])

Эта функция принимает нотную запись, а возвращает пару. Первый элемент содержит список списков нотдля неударных инструментов, каждый подсписок содержит ноты только для одного инструмента. Второйэлемент пары содержит все ноты для ударных инструментов. Представим также, что у нас есть функция,которая превращает эту пару в набор midi-сообщений:

mergeInstr :: ([[MidiEvent]], [MidiEvent]) -> M.Track Double

Наши отсчёты времени записаны в виде значений типа Double, Нам необходимо перейти к целочислен-ным Ticks. Представим, что такая функция у нас уже есть:

tfmTime :: M.Track Double -> M.Track M.Ticks

Тогда функция toTrack примет вид:

toTrack :: Score -> M.Track M.TickstoTrack = tfmTime . mergeInstr . groupInstr

Все три составляющие функции пока не определены. Начнём с функции tfmTime. Нам необходимо от-сортировать события во времени для того, чтобы мы смогли перейти из абсолютных отсчётов во времени вотносительные. Специально для этого в библиотеке HСodecs определена функция:

fromAbsTime :: Num a -> Track a -> Track a

Также нам понадобится функция:

type Time = Double

fromRealTime :: TimeDiv -> Trrack Time -> Track Ticks

Она проводит квантование во времени. С помощью неё мы преобразуем отсчёты в Double в целочисленныеотсчёты. С помощью этих функций мы можем определить функцию timeDiv так:

import Data.List(sortBy)import Data.Function (on)...

tfmTime :: M.Track Double -> M.Track M.TickstfmTime = M.fromAbsTime . M.fromRealTime timeDiv .

sortBy (compare ‘on‘ fst)

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

sortBy :: (a -> a -> Ordering) -> [a] -> [a]

Она принимает функцию упорядочивания и список. Мы воспользовались этой функцией, потому что намнеобходимо отсортировать элементы списка сообщений по значению временных отсчётов. Функцию упоря-дочивания мы составляем с помощью специальной функции on, которая определена в модуле Data.Function.С этой функцией мы уже сталкивались, когда говорили о функциях высшего порядка, она принимает функ-цию двух аргументов и функцию одного аргумента и словно ”подкладывает” вторую функцию под первую:

Prelude Data.Function> :t onon :: (b -> b -> c) -> (a -> b) -> a -> a -> c

Теперь напишем функцию mergeInstr. Она устанавливает инструменты на каналы и преобразует событияв последовательность midi-сообщений. При этом мы различаем сообщения для ударных и сообщения для всехостальных инструментов:

Перевод в midi | 267

Page 268: Ru Haskell Book

mergeInstr :: ([[MidiEvent]], [MidiEvent]) -> M.Track DoublemergeInstr (instrs, drums) = concat $ drums’ : instrs’

where instrs’ = zipWith setChannel ([0 .. 8] ++ [10 .. 15]) instrsdrums’ = setDrumChannel drums

setChannel :: M.Channel -> [MidiEvent] -> M.Track DoublesetChannel = undefined

setDrumChannel :: [MidiEvent] -> M.Track DoublesetDrumChannel = undefined

Имя instrs’ указывает на последовательность списков сообщений для каждого неударного инструмента.Функция setChannel принимает номер канала и список событий. По ним она строит список midi-сообщений.Определим эту функцию:

setChannel :: M.Channel -> [MidiEvent] -> M.Track DoublesetChannel ch ms = case ms of

[] -> []x:xs -> (0, M.ProgramChange ch (instrId x)) : (fromEvent ch =<< ms)

instrId = noteInstr . eventContent

fromEvent :: M.Channel -> MidiEvent -> M.Track DoublefromEvent = undefined

Первым событием мы присоединяем событие, которое устанавливает на данном канале определённыйинструмент. По построению программы все ноты в переданном списке играются на одном и том же инстру-менте, поэтому мы узнаём идентификатор инструмента из первого элемента списка. У нас появилась новаянеопределённая функция fromEvent она переводит сообщение в список midi-сообщений:

fromEvent :: M.Channel -> MidiEvent -> M.Track DoublefromEvent ch e = [

(eventStart e, noteOn n),(eventStart e + eventDur e, noteOff n)]where n = clipToMidi $ eventContent e

noteOn n = M.NoteOn ch (notePitch n) (noteVolume n)noteOff n = M.NoteOff ch (notePitch n) 0

clipToMidi :: Note -> NoteclipToMidi n = n {

notePitch = clip $ notePitch n,noteVolume = clip $ noteVolume n }where clip = max 0 . min 127

Определив эти функции, мы легко можем написать и функцию setDrumChannel она переводит сообщениядля ударных инструментов в midi-сообщения:

setDrumChannel :: [MidiEvent] -> M.Track DoublesetDrumChannel ms = fromEvent drumChannel =<< ms

where drumChannel = 9

Для ударных инструментов выделен отдельный канал. Считается, что все они происходят на 10 канале.Поскольку в библиотеке HCodecs первый канал называется нулевым, мы будем записывать все сообщения надевятый канал.Мы переводим событие в два midi-сообщения, первое говорит о том, что мы начали играть ноту, а второе

говорит о том, что мы закончили её играть. Функция clipToMidi приводит значения для высоты и громкостив диапазон midi.Нам осталось определить только одну функцию. Эта функция распределяет события по инструментам.

Сначала мы разделим события на те, что играются на ударных и неударных инструментах, а затем разделим”неударные” ноты по инструментам:

import Control.Arrow(first, second)import Data.List(sortBy, groupBy, partition)...

groupInstr :: Score -> ([[MidiEvent]], [MidiEvent])

268 | Глава 20: Музыкальный пример

Page 269: Ru Haskell Book

groupInstr = first groupByInstrId .partition (not . isDrum . eventContent) . trackEventswhere groupByInstrId = groupBy ((==) ‘on‘ instrId) .

sortBy (compare ‘on‘ instrId)

В этом определении мы воспользовались двумя новыми стандартными функциями из модуля Data.List.Функция partition разделяет список на пару списков. В первом списке находятся все те элементы, длякоторых заданный предикат вернул True, а во втором списке – все остальные элементы исходного списка:

Prelude Data.List> :t partitionpartition :: (a -> Bool) -> [a] -> ([a], [a])

Функция groupBy превращает список в список списков:

Prelude Data.List> :t groupBygroupBy :: (a -> a -> Bool) -> [a] -> [[a]]

Если бинарная функция на соседних элементах исходного списка вернула True, то они помещаются водин подсписок. Эта функция используется для того чтобы сгруппировать элементы списка по какому-нибудьпризнаку. При этом для того чтобы сгруппировать элементы по идентификатору инструмента, мы сначалаотсортировали события по значению идентификатора. После этого значения с одинаковыми идентификато-рами стали соседними и мы сгруппировали их с помощью groupBy.Функция first применяет функцию к первому элементу пары. Вот мы и закончили, можно послушать ре-

зультаты. На самом деле остались два нюанса. В функции setChannel мы полагаем, что мелодия начинаетсяв момент времени t = 0, но на практике это может оказаться не так, мы можем сместить ноты функциейdelay в отрицательную сторону. Тогда первые ноты будут содержать отрицательное время начала события.Но мы можем исправить эту ситуацию, сместив все ноты на время самой первой ноты, конечно смещатьнеобходимо только в том случае если время окажется отрицательным:

alignEvents :: [MidiEvent] -> [MidiEvent]alignEvents es

| d < 0 = map (delay (abs d)) es| otherwise = eswhere d = minimum $ map eventStart es

Вызовем эту функцию сразу после функции trackEvents в функции groupInstr. Второй нюанс заключа-ется в том, что каждый трек в midi-файле должен заканчиваться специальным сообщением, в библиотекеHCodecs оно обозначается с помощью конструктора TrackEnd. В самом конце необходимо добавить сообще-ние (0, TrackEnd):

toTrack :: Score -> M.Track M.TickstoTrack = addEndMsg . tfmTime . mergeInstr . groupInstr

addEndMsg :: M.Track M.Ticks -> M.Track M.TicksaddEndMsg = (++ [(0, M.TrackEnd)])

Теперь мы можем проверить, что у нас получилось. Создадим файл:

module Main where

import System

import Trackimport Scoreimport Codec.Midi

out = (>> system ”timidity tmp.mid”) .exportFile ”tmp.mid” . render

В функции out мы переводим нотную запись в значение типа Midi, затем сохраняем это значение в файлеtmp.mid и в самом конце запускаем файл с помощью проигрывателя timidity. Вместо timidity вы можетевоспользоваться вашим любимым проигрывателем midi-файлов. Теперь загрузим модуль Main в интерпре-татор. Послушаем ноту до:

*Main> out c

Перевод в midi | 269

Page 270: Ru Haskell Book

Далее следуют сообщения из проигрывателя timidity и долгожданный звук. Мы слышим ноту до, сыг-ранную на рояле. Наберём какую-нибудь мелодию:*Main> let x = line [c, hn e, hn e, low b, c]*Main> out x

Сыграем в два раза быстрее, на другом инструменте:*Main> out $ instr 15 $ hn x

Сыграем канон. Канон это когда одна и та же мелодия ведётся в разных голосах с запаздыванием. Сыграемдвухголосный канон:*Main> out $ instr 80 (loop 3 x) =:= delay 2 (instr 65 $ low $ loop 3 x)

Номера инструментов можно посмотреть по справке к протоколу General Midi. Это дополнение к прото-колу midi определяет какие номера каким инструментам должны соответствовать. Звучит ужасно, но звучит!

20.5 ПримерОпираясь на примитивы композиции, которые мы определил в модуле Score, мы можем написать мело-

дию. Ниже приведён небольшой пример. Инструменты:closedHiHat = drum 42; rideCymbal = drum 59; cabasa = drum 69;maracas = drum 70; tom = drum 45;

flute = instr 73; piano = instr 0;

Ударная секция:b1 = bam 100b0 = bam 84

drums1 = loop 80 $ chord [tom $ line [qn b1, qn b0, hnr],maracas $ line [hnr, hn b0]]

drums2 = quieter 20 $ cabasa $ loop 120 $ en $ line [b1, b0, b0, b0, b0]

drums3 = closedHiHat $ loop 50 $ en (line [b1, loop 12 wnr])

drums = drums1 =:= drums2 =:= drums3

Уже сейчас мы можем загрузить эту партию в интерпретатор и послушать, вызвав out drums. Аккорды кмелодии:c7 = chord [c, e, b]gs7 = chord [low af, c, g]g7 = chord [low g, low bf, f]

harmony = piano $ loop 12 $ lower 1 $ bn $ line [bn c7, gs7, g7]

Мелодия:ac = louder 5

mel1 = bn $ line [bnr, subMel, ac $ stretch (1+1/8) e, c,subMel, enr]where subMel = line [g, stretch 1.5 $ qn g, qn f, qn g]

mel2 = loop 2 $ qn $ line [subMel, ac $ bn ds, c, d, ac $ bn c, c, c, wnr,subMel, ac $ bn g, f, ds, ac $ bn f, ds, ac $ bn c]

where subMel = line [ac ds, c, d, ac $ bn c, c, c]

mel3 = loop 2 $ line [pat1 (high c) as g, pat1 g f d]where pat1 a b c = line [pat a, loop 3 qnr, wnr,

pat b, qnr, hnr, pat c, qnr, hnr]pat x = en (x +:+ x)

mel = flute $ line [mel1, mel2, mel3]

270 | Глава 20: Музыкальный пример

Page 271: Ru Haskell Book

Добавим в конце звук тарелки:cha = delay (dur mel1 + dur mel2) $ loop 10 $ rideCymbal $ delay 1 b1

Соберём всё вместе и послушаем:res = chord [drums, harmony, high $ mel, louder 40 $ cha, rest 0]

main = out res

В конце стоит rest 0 для того чтобы было удобно глушить инструменты комментированием.

20.6 Эффективное представление музыкальной нотацииРеализация, которую мы рассмотрели не эффективна, Мы могли бы определить тип Track и по-другому.

Мы очень часто пользуемся операцией delay через операцию line. Так в выражении:q = line [s1, s2, line [loop 2 s3, s4], s5]

Мы будем несколько раз обходить элемент s3 для каждого применения line. К примеру сначала мысмести все элементы на 3, потом сместим на 5, потом на 10, но вместо этого мы могли бы сразу сместитьвсе элементы на 18 за один проход. Для этого мы можем закодировать преобразования событий во временив типе Track:data Track t a = Track {

trackDur :: t,trackEvents :: TList t a

data TList t a = Empty | Single a | Append (TList t a) (TList t a)| TFun (Tfm t) (TList t a)

data Tfm t = Tfm !t !t

Тип TList позволяет проводить быстрое объединение списков. Дополнительный конструктор TFun обо-значает линейное преобразование списка во времени. Линейное преобразование кодируется двумя числами,это масштаб и смещение. Мы считаем, что события в конструкторе Single начинаются в момент времени 0и длятся 1 единицу времени. Так например событие, которое произошло на 2 единице времени и длилось 4единицы можно представить так:TFun (4 2) Single a

Значение Tfm k d обозначает линейную функцию

f(x) = kx+ d

Для того чтобы получить настоящие отсчёты по времени мы применяем её к временным координатам”не преобразованного” события, т.е. события Event 0 1 a.Единственное, что нам нужно для того чтобы встроить этот вариант в библиотеку это написать функцию:

fromTList :: TList t a -> [Event t a]

И конечно переопределить все функции композиции. Но все функции, которые отвечают за перевод изTrack в Midi останутся прежними.

20.7 Краткое содержаниеВ этой главе мы построили секвенсор для создания midi-файлов. Мы воспользовались библиотекой

HCodecs и создали над ней небольшую надстройку.В нашей библиотеке примитивными конструкциями были события, параллельная композиция (одновре-

менное воспроизведение) и преобразование событий во времени (сдвиг и масштабирование). Все остальныеоперации выражались через эти простейшие операции. Отметим, что есть и другие подходы. Например вбиблиотеках Haskore и Euterpea примитивными конструкциями является единичное событие (без отметокво времени) и параллельная и последовательная композиции. Подход, который мы рассмотрели в более об-щем виде реализован в библиотеках temporal-media, temporal-music-notation и temporal-music-notation-demo.

Эффективное представление музыкальной нотации | 271

Page 272: Ru Haskell Book

20.8 Упражнения• Попробуйте написать какую-нибудь мелодию.• Подумайте каких операций не хватает. Например было бы удобно иметь возможность вырезать из ме-лодии куски. Так в примере у нас остались хвосты от ударной секции, определите операцию, котораяпозволяет убрать лишнее.• Превратите музыкальный пример в бибилиотеку: выделите основные функции, напишите документа-цию.

272 | Глава 20: Музыкальный пример

Page 273: Ru Haskell Book

Приложения

Начало работы с HaskellКомпилятор

Для программирования в Haskell нам понадобится компилятор. Мы будем пользоваться наиболее разви-тым компилятором – GHC. Лучше всего устанавливать его вместе с Haskell Platform:

http://hackage.haskell.org/platform/

Haskell Platform содержит стабильную версию компилятора и много хороших, проверенных временембиблиотек. Если по каким-то причинам установить Haskell Platform не удалось. Не отчаивайтесь, можнозагрузить компилятор с сайта GHC:

http://www.haskell.org/ghc/

И далее установить все необходимые библиотеки с Hackage с помощью cabal.

Среда разработкиДля Haskell существует очень мало сред разработки. Обычно на Haskell программируют в каких-нибудь

продвинутых текстовых редакторах (vim, Emacs, scite, kate, notepad++). Отметим всё же среду разработкиLeksah (http://leksah.org/), она написана на Haskell и её можно установить с Hackage. Также отметимредактор yi. Он написан на Haskell и его также можно установить с Hackage.Если вы не хотите разбираться с новым текстовым редактором или средой разработки, и вам нужна лишь

подсветка синтаксиса можно воспользоваться gedit. Пишем код в gedit, сохраняем, переключаемся на ghci,пробуем, обновляем, пробуем, при случае компилируем или собираем в пакет. Всё это можно делать и вgedit.

| 273

Page 274: Ru Haskell Book

ЛитератураОHaskell написано много интересных книг и статей, но все они на английском. На русском языке выходит

электронный журнал ”Практика функционального программирования” (fprog.ru). Пока в нём доминируютдва языка – это Erlang и Haskell.Я бы хотел рассказать о тех книгах и статьях, которые мне помогли. Все они приняли активное участие

в создании этой книги.

Книги• Miran Lipovaca. Learn You A Haskell For A Great Good.Очень хорошая книга для начинающих, Haskell в картинках. Весёлая и познавательная книга1.http://learnyouahaskell.com/

• Hal Daume III. Yet Another Haskell Tutorial.Ещё одна очень хорошая книга для начинающих. Без картинок, но всё по делу.www.cs.utah.edu/~hal/docs/daume02yaht.pdf

• Paul Hudak. Haskell School of Expression.Книга, которая иллюстрирует основные принципы функционального программирования на примереHaskell. Главные достоинства – много текста об общих принципах и интересные приложения, картинки,музыка, анимация, управление роботами и всё это на Haskell.

• Paul Hudak. Haskell School of Music.Пол Хьюдак увлекается не только Haskell, но и музыкой. Он написал книгу, которая целиком посвященаописанию музыки в Haskell:http://www.cs.yale.edu/homes/hudak/Papers/HSoM.pdf

http://haskell.cs.yale.edu/

• Bryan O’Sullivan, Don Stewart, John Goerzen. Real World Haskell.Очень полезная книга в помощь тем, кто хочет научиться писать настоящие, серьёзные программы.Авторы подробно изучают вопросы, связанные с применением Haskell на практике.http://book.realworldhaskell.org/

Тематический сборникОсновы

• John Hughes. Why Functional Programming Matters

• Mark P. Jones. Functional Programming with Overloading and Higher-Order Polymorphism.

• Евгений Кирпичев. Элементы функциональных языков программирования, журнал Практика функци-онального программирования.

• Paul Hudak, John Hughes, Simon Peyton Jones, Philip Wadler. A History of Haskell: Being Lazy With Class.

• Simon Thompson. Programming It in Haskell.

• Justin Bailey. Haskell Cheat Sheet.blog.codeslower.com/static/CheatSheet.pdf

Разработка программ сверху-вниз

• Дмитрий Астапов. Давно не брал я в руки шашек, журнал Практика Функционального программиро-вания.

1Обновление: книга переведена на русский, вышла в издательстве ДМК Пресс

274 | Приложения

Page 275: Ru Haskell Book

Функторы и Монады• Conor McBride, Ross Paterson. Applicative programming with effects. Статья об аппликативных функторах.• Philip Wadler. The Essence of Functional Programming.Статья, в которой впервые зашла речь о применении монад в Haskell.• Tarmo Uustalu, Varmo Vene. The Essence of Dataflow Programming.Статья о комонадах, но есть много интересного и о монадах.

Ленивые вычисления• Douglas McIlroy. Power Series, Power Serious.• Дмитрий Астапов. Реурсия+мемоизация=динамическое программирование, журнал Практика функ-ционального программирования.• Сергей Зефиров. Лень бояться, журнал Практика функционального программирования.• Jerzy Karczmarczuk. Specific “scientific” data structures, and their processing.

Структурная рекурсия• Graham Hutton. A tutorial on the universality and expressiveness of fold• Jeremy Gibbons. Origami Programming.• Jeremy Gibbons, Geraint Jones. The Under-Appreciated Unfold.

Лямбда-исчисление• Шалак В.И. Шейнфинкель и комбинаторная логика.• Paul Hudak: Conception, Evolution, and Application of Functional Programming Languages.Длинная статья о развитии функциональных языков. Там есть главы о лямбда-исчислении.• Бенджамин Пирс. Типы в языках программирования.Большая книга о теории типов.http://newstar.rinet.ru/~goga/tapl/

• Денис Москвин. Системы типизации лямбда-исчисления.Курс видео-лекций.http://www.lektorium.tv/course/?id=22797

• John Harrison. Introduction to Functional Programming.Курс лекций по функциональному программированию, который читался в Университете Кэмбридж.

Теория категорийДве очень хорошие книги для начинающих:• Maarten M. Fokkinga. Gentle Introduction to Category Theory.wwwhome.cs.utwente.nl/~fokkinga/mmf92b.pdf

• Steve Awodey. Category Theory.• Eugenia Cheng, Simon Willerton aka TheCatsters. Курс видео-лекций на youtube.http://www.scss.tcd.ie/Edsko.de.Vries/ct/catsters/linear.php http://www.youtube.com/user/TheCatsters

Статьи по категориальным типам:• Varmo Vene. Categorical Programming with Inductive and Coinductive Types. Phd-тезиз.• Erik Meijer, Graham Hutton. Bananas in Space: Extending Fold and Unfold to Exponential Types.• Martin Erwig. Categorical Programming with Abstract Data Types.• Martin Erwig. Metamorphic Programming: Structured Recursion for Abstract Data Types.

Литература | 275

Page 276: Ru Haskell Book

Практика

• Conal Elliott. Denotational design with type class morphisms.• Johan Tibell. High Performance Haskell. Слайды с выступления.• Simon Marlow. Parallel and Concurrent Programming in Haskell.• Simon Peyton Jones. Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, andforeign-language calls in Haskell.• Edward Z. Yang. Блог о Haskell в картинках. Много полезной информации о лени и устройстве ghc.http://blog.ezyang.com/about/

И все-все-всеЕсли вдруг у вас возникли вопросы по Haskell, и рядом с вами не оказалось того, кто мог бы на них

ответить, и в книгах нет ответа, вы можете спросить у сообщества Haskell, в haskell-cafe, там вам быстро и срадостью ответят:

http://www.haskell.org/mailman/listinfo/haskell-cafe

Сообщество Haskell славится радушием и терпимостью к начинающим. Там много информации о выпус-ках новых библиотек, конференциях, обучающих программах и просто разговоры о том-о-сём.Также стоит отметить журнал Monad.Reader:

http://themonadreader.wordpress.com/

276 | Приложения

Page 277: Ru Haskell Book

Обзор HackageЧисло пакетов, загруженных на Hackage, уже перевалило за 2000. В Hackage легко заблудиться. Очень

часто не разберёшься какой из пакетов выбрать. К тому же многие из них заброшены или просто не подходятдля использования в серьёзных приложениях. Но среди них есть и очень хорошие пакеты. Некоторые из нихвключены в Haskell Platform. Ниже приведён тематический обзор наиболее популярных пакетов.

Стандартные библиотекиВсе приведённые в этом подразделе библиотеки включены в Haskell Platform.Полный список библиотек для Haskell Platform можно посмотреть на сайте http://lambda.haskell.

org/hp-tmp/docs.

• Начало-всех-начал: baseБиблиотека включает в себя все стандартные определения, например модули Prelude, Data.List,Control.Monad и многие другие.

• Стандартные монады: mtlВключает монады State, Writer, Reader и другие.

• Контейнеры: containersАссоциативные массивы, множества, последовательности, деревья.

• Массивы: array

• Графы: fgl

• Архиваторы: zlib

• Вычисление по значению: deepseqОбычная функция seq, позволяет привести данное выражение к слабой заголовочной нормальной фор-ме, если нам всё же необходимо вычислить значение полностью, мы можем воспользоваться функциейdeepseq из одноимённой библиотеки.

• Параллельное программирование: stm и parallel

• Временная арифметика, календарь: time

• Парсинг: parsec

• Регулярные выражения: regex-base, regex-posix

• Построение структурированного текста: pretty

• Тестирование программ: HUnit, QuickCheck

• Управление файловой системой: directory

• Работа с путями к файлам/директориям: filepath

• Сетевые библиотеки: network, HTTP, cgi.

• 3д Графика: OpenGL, GLUT.

• Монадные трансформеры: transformersМы не коснулись этой темы, но вот краткое пояснение: монадные трансформеры позволяют комбини-ровать несколько монад. Например, если нам нужно использовать чтение-запись в файл совместно сизменяемым состоянием.

Обзор Hackage | 277

Page 278: Ru Haskell Book

Эффективные типы данных• Списки: dlist – эффективное объединение списков.Если вы часто пользуетесь операцией ++, то необходимо заботиться о том, чтобы скобки всегда группи-ровались вправо. Как в a++(b++(c++d)). Иначе время объединения из линейного превратится в квад-ратичное. Библиотека dlist предоставляет специальный тип списков, для которых не важно как груп-пируются скобки при объединении. Время объединения всегда будет линейным.• Строки: bytestringЕсли ваша программа загружена обработкой строк, и работает слишком медленно, рассмотрите вари-ант перехода со стандартных строк на тип ByteString, это может на порядок увеличить быстродей-ствие.• Текст: text или utf8-stringРабота с текстом в формате Unicode. Часто проблемы возникают при необходимости обработки рус-ского текста закодированного в Unicode. Для решения этой проблемы можно воспользоваться однойиз этих библиотек.• Двоичные данные: binary или cereal – Сериализация/десериализация данных.• Случайные числа: mersenne-random-pure64Эффективный генератор случайных чисел.• Ввод-вывод: iterateeЭффективная реализация ввода-вывода. Если вам нужно читать или писать данные из большого числафайлов, эта библиотека может существенно помочь.• Контейнеры: unordered-containersАльтернатива стандартной библиотеке containers. Эффективные типы Map и Set.• Последовательности: fingertreeИспользуются для работы с очередями различного типа.• Массивы: vectorЭффективный тип для представления массивов. Замена стандартному типу Data.Array.• Матрицы: hmatrix, repa

И все-все-все• Парсинг: parsec или attoparsec• Языки разметки: pandoc, xhtml, tagsoup, blaze-html, html• XML: xml, HaXml• JSON: json, aeson• Web: happstack, snap, yesod, hakyll• Сетевые библиотеки: network, HTTP, cgi, curl• Графика: diagrams, gnuplot, SDL• 3д графика: OpenGL, GLFW, GLUT• Базы данных: HDBC• GUI: wxHaskell, gtk2hs• Оценка производительности программ: criterion• Статистика: statistics• Парсинг и генерация кода Haskell: haskell-src-exts• FRP: reactive, reactive-banana, yampa• Линейная алгебра: vector-space, hmatrix

278 | Приложения