Сообщений 1    Оценка 635        Оценить  
Система Orphus

Мягкое введение в Haskell

Авторы: Пол Хьюдак
Джон Петерсон
Yale University
Джозеф Фасел
Los Alamos National Laboratory

Перевод: Денис Москвин
SoftJoys Computer Academy

Источник: A Gentle Introduction To Haskell
Материал предоставил: RSDN Magazine #1-2007
Опубликовано: 24.04.2007
Исправлено: 15.04.2009
Версия текста: 1.0
8 Стандартные классы Haskell
8.1 Классы равенства и упорядоченности
8.2 Класс перечислений
8.3 Классы Read и Show
8.4 Производные воплощения
9 О монадах
9.1 Монадические классы
9.2 Встроенные монады
9.3 Использование монад
10 Числа
10.1 Структура числового класса
10.2 Конструируемые числа
10.3 Приведение чисел и перегруженные литералы
10.4 Числовые типы по умолчанию
11 Модули
11.1 Квалифицированные имена
11.2 Абстрактные типы данных
11.3 Прочие свойства
12 Ловушки типизации
12.1 Let-связанный полиморфизм
12.2 Перегрузка чисел
12.3 Ограничение мономорфизма
13 Массивы
13.1 Типы индексов
13.2 Создание массивов
13.3 Аккумуляция
13.4 Инкрементальные обновления
13.5 Пример: умножение матриц
14 Следующий этап
15 Благодарности
Ссылки

8 Стандартные классы Haskell

В этом разделе мы представляем предопределённые стандартные классы типов Haskell. Мы несколько упростили эти классы, опустив в них некоторые (менее интересные) методы; описание языка Haskell содержит более полную информацию. Следует также отметить, что некоторые из этих стандартных классов являются частью стандартных библиотек Haskell; они описаны в Haskell Library Report.

8.1 Классы равенства и упорядоченности

Классы Eq и Ord уже обсуждались выше. Определение Ord в Prelude несколько сложнее упрощённой версии, представленной ранее. В частности, отметим метод compare:

data Ordering           =  EQ | LT | GT
compare                 :: Ord a => a -> a -> Ordering

Достаточно метода compare, чтобы определить все остальные методы этого класса (через умолчания); такой подход является наилучшим для создания воплощений Ord.

8.2 Класс перечислений

Класс Enum содержит набор операций, которые являются основой синтаксического сахара арифметических последовательностей; например, арифметическая последовательность [1,3..] замещается на enumFromThen 1 3 (формальную трансляцию см. в §3.10). Теперь ясно, что выражения арифметических последовательностей могут использоваться для создания списков любого типа, являющегося воплощением Enum. Это касается не только большинства числовых типов, но также и Char, так что, например, ['a'..'z'] обозначает список латинских букв нижнего регистра в алфавитном порядке. Более того, перечислительные типы, определённые пользователем, наподобие Color, могут легко быть объявлены воплощениями класса Enum. В этом случае:

    [Red .. Violet]     =>     [Red, Green, Blue, Indigo, Violet]

Отметим, что такая последовательность является арифметической, в том смысле, что инкремент между значениями один и тот же, хотя эти значения и не числа. Большинство типов-воплощений Enum можно отобразить на целые числа ограниченного диапазона; для них функции fromEnum и toEnum преобразуют друг в друга Int и тип из Enum.

8.3 Классы Read и Show

Воплощения класса Show – это те типы, которые могут быть преобразованы в строку символов (обычно для целей ввода-вывода). Класс Read обеспечивает операции, разбирающие строки для получения значений, которые эти строки могут представлять. Простейшей функцией класса Show является show:

show                    :: (Show a) => a -> String

Совершенно естественно, что show принимает произвольную величину подходящего типа и возвращает её представление в виде строки символов (списка символов); результатом show (2+2) будет 4. Это прекрасно, однако обычно нам приходится создавать более сложные строки, которые могут содержать представления многих величин, как в:

"The sum of " ++ show x ++ " and " ++ show y ++ " is " ++ show (x+y) ++ "."

и, помимо всего прочего, все эти конкатенации оказываются немного неэффективными. В частности, представим себе функцию, представляющую двоичные деревья из Раздела 2.2.1 в виде строк, которая правильным образом помечает вложенные поддеревья и разделяет левые и правые ветви (при условии, что тип элемента можно представить в виде строки):

showTree              :: (Show a) => Tree a -> String
showTree (Leaf x)     =  show x
showTree (Branch l r) =  "<" ++ showTree l ++ "|" ++ showTree r ++ ">"

Поскольку (++) имеет линейную временную сложность по длине своего левого аргумента, сложность showTree потенциально квадратично зависит от размера дерева.

Для восстановления линейной сложности имеется функцией shows:

shows :: (Show a) => a -> String -> String

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

show x               = shows x ""

Мы можем использовать shows для определения более эффективной версии showTree, у которой тоже будет аргумент – строковой аккумулятор:

showsTree                :: (Show a) => Tree a -> String -> String
showsTree (Leaf x)     s = shows x s
showsTree (Branch l r) s = '<' : showsTree l ('|' : showsTree r ('>' : s))

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

type ShowS           = String -> String

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

showsTree              :: (Show a) => Tree a -> ShowS
showsTree (Leaf x)     = shows x
showsTree (Branch l r) = ('<':) . showsTree l . ('|':) . showsTree r . ('>':)

При этой трансформации происходит нечто более важное, чем просто получение более опрятного кода: мы повышаем уровень представления с объектного уровня (в нашем случае это строки) до функционального уровня. Мы можем думать о типе showsTree как о сообщающем, что showsTree преобразует дерево в строковый вид. Функции, подобные, ('<' :) или ("a string" ++), являются примитивными функциями; мы строим более сложные функции с помощью функциональной композиции.

Научившись превращать деревья в строки, возьмёмся за обратную задачу. Основная идея – это парсер для типа a, который представляет собой функцию, принимающую строку, и возвращающую список пар (a, String) [9]. Библиотека Prelude обеспечивает синоним типа для таких функций:

type ReadS a            = String -> [(a,String)]

Обычно парсер возвращает одноэлементный список, содержащий значение типа a, которое было считано из входной строки, и остаток строки, следующий за тем, что было синтаксически разобрано. Однако если синтаксический разбор оказался невозможен, результатом будет пустой список, а если строку можно разобрать несколькими способами (неоднозначность), результирующий список будет содержать более одной пары. Стандартная функция reads представляет собой парсер для любого воплощения Read:

reads               :: (Read a) => ReadS a

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

readsTree           :: (Read a) => ReadS (Tree a)
readsTree ('<':s)   =  [(Branch l r, u) | (l, '|':t) <- readsTree s,
                                          (r, '>':u) <- readsTree t ]
readsTree s         =  [(Leaf x, t)     | (x,t)      <- reads s]
ПРИМЕЧАНИЕ

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

Давайте подробно проанализируем определение этой функции. Рассматриваются два основных случая. Если первый символ анализируемой строки – это '<', то мы имеем дело с представлением ветви (branch), в противном случае мы имеем дело с листом (leaf). В первом случае для оставшейся части входной строки s, следующей за открывающейся угловой скобкой, любой возможный синтаксический разбор должен быть tree Branch l r со строкой-остатком u. При этом должны выполняться следующие условия:

  1. Дерево l может быть синтаксически разобрано по началу строки s.
  2. Оставшаяся строка (следующая за представлением l) начинается с символа '|'. Обозначим хвост этой строки через t.
  3. Дерево r может быть синтаксически разобрано, начиная с t.
  4. Строка, оставшаяся после этого разбора, начинается с '>', и u – её хвост.

Отметим выразительную силу, достигаемую за счет комбинирования сопоставления с образцом и list comprehension: форма получившегося синтаксического разбора задаётся главным выражением list comprehension, первые два из приведённых выше условий выражаются первым генератором («(l, '|':t) извлекается из списка разборов строки s»), а оставшиеся два условия выражаются вторым генератором.

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

Примем на веру на некоторое время, что Integer (как и многие другие типы) – воплощение ReadShow), снабжённый методом reads, который ведёт себя ожидаемым образом, т.е.

(reads "5 золотых колец") :: [(Integer,String)] => [(5, " золотых колец")]

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

readsTree "<1|<2|3>>" => [(Branch (Leaf 1) (Branch (Leaf 2) (Leaf 3)), "")]
readsTree "<1|2"      => []

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

lex                     :: ReadS String

Функция lex обычно возвращает одноэлементный список, содержащий пару строк: первую лексему входной строки и оставшуюся часть ввода. Лексические правила те же, что и в программах на Haskell, включая комментарии, которые lex пропускает, так же как и пробелы. Если входная строка пуста или содержит только пробелы и комментарии, lex вернёт [("","")]; если ввод не пуст в указанном смысле, однако также не содержит ни одной правильной лексемы после произвольного числа пробелов и комментариев в начале ввода, lex вернёт [].

При использовании этого лексического анализатора наш парсер деревьев примет вид:

readsTree    :: (Read a) => ReadS (Tree a)
readsTree s  = [(Branch l r, x) | ("<", t) <- lex s,
                                  (l, u)   <- readsTree t,
                                  ("|", v) <- lex u,
                                  (r, w)   <- readsTree v,
                                  (">", x) <- lex w ]
               ++                 
               [(Leaf x, t)     | (x, t)   <- reads s ]

Теперь мы можем использовать readsTree и showsTree для объявления воплощения Read: (Read a) => Tree a и воплощения Show: (Show a) => Tree a. Это позволит нам использовать обобщённые перегруженные функции из библиотеки Prelude для синтаксического разбора и вывода в строковое представление деревьев. Более того, в этом случае мы автоматически получаем возможность синтаксического разбора и преобразования в строку многих других типов, содержащих деревья как составную часть, например, [Tree Integer]. Оказывается, readsTree и showsTree имеют почти правильный тип для методов Show и Read. Методы showsPrec и readsPrec представляют собой параметризованные версии shows and reads. Дополнительный параметр – это уровень приоритета, используемый для правильной расстановки скобок в выражениях, содержащих инфиксные конструкторы. Для типов, подобных Tree, этот приоритет можно проигнорировать. Воплощения Show и Read для Tree имеют вид:

instance Show a => Show (Tree a) where
    showsPrec _ x = showsTree x

instance Read a => Read (Tree a) where
    readsPrec _ s = readsTree s

Воплощению Show можно дать альтернативное определение в терминах showTree:

instance Show a => Show (Tree a) where
    show t = showTree t

Это, однако, будет менее эффективно, чем версия с ShowS. Отметим, что класс Show определяет методы по умолчанию и для showsPrec и для show, позволяя пользователю определять только один из них в объявлении воплощения. Поскольку эти умолчания взаимно рекурсивны, объявление воплощения, которое не определяет ни одну из этих функций, при их вызове войдёт в бесконечный цикл. Другие классы, например Num, тоже имеют такие «взаимоблокирующие умолчания».

Отсылаем заинтересовавшегося читателя к §D за деталями классов Read и Show.

Мы можем протестировать воплощения Read и Show, применяя (read . show) к разным деревьям (результатом должно быть тождество), здесь read является специализацией reads:

read :: (Read a) => String -> a

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

8.4 Производные воплощения

Вспомним воплощение Eq для деревьев, введённое нами в Разделе 5, подобные объявления просто (и скучно) давать: мы требуем, чтобы типы элементов, хранящихся в листьях, были сравнимы на предмет равенства; затем два листа объявляются равными, если они содержат равные элементы, а две ветви равны, если равны их левые и правые поддеревья соответственно. Любые два других дерева неравны:

instance (Eq a)  => Eq (Tree a) where
    (Leaf x)     == (Leaf y)       = x == y
    (Branch l r) == (Branch l' r') = l == l' && r == r'
    _            == _              = False

К счастью, не требуется заниматься этими скучными вещами каждый раз, когда нам нужен оператор равенства для нового типа; воплощение Eq может быть произведено автоматически из объявления data, если мы дадим такое указание:

data Tree a = Leaf a | Branch (Tree a) (Tree a) deriving Eq

Конструкция deriving неявно производит объявление воплощения Eq, как раз такое, какое приведено в Разделе 5. Воплощения Ord, Enum, Ix, Read и Show также можно сгенерировать с помощью конструкции deriving.

ПРИМЕЧАНИЕ

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

Производное воплощение Ord для Tree несколько сложнее воплощения Eq:

instance (Ord a)  => Ord (Tree a) where
    (Leaf _)     <= (Branch _)     = True
    (Leaf x)     <= (Leaf y)       = x <= y
    (Branch _)   <= (Leaf _)       = False
    (Branch l r) <= (Branch l' r') = l == l' && r <= r' || l <= l'

Здесь задаётся лексикографический порядок: конструкторы упорядочены в порядке их появления в объявлении data, а аргументы конструкторов сравниваются слева направо. Напомним, что встроенный тип списка семантически эквивалентен обычному типу с двумя конструкторами. Его полное объявление фактически выглядело бы так:

data [a]           = [] | a : [a] deriving (Eq, Ord) -- псевдо-код

(Списки также имеют воплощения Show и Read, но эти воплощения не являются производными.) Производные воплощения Eq и Ord для списков ведут себя обыкновенным образом, в частности, строки символов, являясь списками символов, упорядочиваются так, как это определено типом Char, лежащем в их основе, при этом начальная подстрока при сравнении меньше, чем более длинная строка; например, "cat" < "catalog".

На практике воплощения Eq и Ord почти всегда являются производными, а не определёнными пользователем. В действительности своё собственное определение предикатов равенства и порядка следует давать с некоторым трепетом, аккуратно обеспечивая поддержку ожидаемых алгебраических свойств отношений равенства и полного порядка. К примеру, нетранзитивный предикат (==) может оказаться настоящим бедствием, приводя в замешательство читателя программы, и ставя в тупик те ручные или автоматические программные трансформации, которые полагаются на то, что предикат (==) является чем-то подобным равенству по определению. Тем не менее, иногда оказывается необходимым обеспечить воплощения Eq или Ord, отличные от тех, которые могли бы быть произведены автоматически; возможно наиболее важный пример такой ситуации – это абстрактный тип данных, в котором различные конкретные значения могут представлять одно и тоже абстрактное значение.

Перечислительный тип может иметь производное воплощение класса Enum, и порядок здесь вновь задаётся порядком конструкторов в объявлении data. Например:

data Day = Sunday | Monday | Tuesday | Wednesday
         | Thursday | Friday | Saturday           deriving (Enum)

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

   [Wednesday .. Friday]    =>    [Wednesday, Thursday, Friday]
   [Monday, Wednesday ..]   =>    [Monday, Wednesday, Friday]

Производные воплощения класса Read (Show) допустимы для всех типов, тип компонентов которых тоже является воплощением Read (Show). (Воплощения Read и Show для большинства стандартных типов обеспечивается библиотекой Prelude. Некоторые типы, как, например, тип функции (->), имеют воплощение Show, (это не соответствует описанию §6.3.3 – прим.пер.) но без соответствующего воплощения Read). Текстовое представление, определяемое производным воплощением Show, согласуется с видом константного выражения Haskell запрашиваемого типа. Например, если мы добавим Show и Read в конструкцию deriving для типа Day, описанного выше, мы получим:

   show [Monday .. Wednesday]   =>   "[Monday,Tuesday,Wednesday]"

9 О монадах

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

Этот раздел, возможно, менее «лёгок», чем остальные. Здесь мы обратимся не только к деталям языка, которые включают в себя монады, но также попытаемся показать более широкую картину: почему монады являются таким важным инструментом и как они используются. Нет единого способа объяснения монад, который бы подходил для всех; множество объяснений можно найти на сайте haskell.org. Другим хорошим введением в практическое программирование с использованием монад может служить [10].

9.1 Монадические классы

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

Монада конструируется поверх полиморфного типа, такого как IO. Сама монада определяется объявлением воплощения, ассоциируя такой тип с одним или со всеми монадическими классами: Functor, Monad и MonadPlus. Ни один из монадических классов не допускает образования производных воплощений. Кроме IO, два других типа из библиотеки Prelude являются членами монадических классов: списки ([]) и Maybe.

С математической точки зрения монады управляются набором законов (laws), которые должны выполняться для монадических операций. Идея законов не специфична для монад: Haskell включает другие операции, которые, по крайней мере неформально, управляются законами. Например, x /= y и not (x == y) должны возвращать одно и то же значение для любого типа сравниваемых значений. Однако гарантии этого отсутствуют: и ==, и /= являются отдельными методами класса Eq, и нет способа убедиться в том, что == и /= связаны таким образом. В этом же смысле вводимые здесь монадические законы не «навязываются» Haskell, однако должны выполняться для любых воплощений монадических классов. Монадические законы дают представление о внутренней структуре монад: изучая их, мы надеемся дать почувствовать, как монады используются.

Класс Functor, уже обсуждавшийся в Разделе 5, определяет одну операцию fmap. Отображающая (map) функция применяет операцию к объектам внутри контейнера (полиморфный тип может рассматриваться как контейнер для значений другого типа), возвращая контейнер структурно идентичный исходному. Эти законы применяются к операции fmap в классе Functor:

         fmap id             =  id
         fmap (f . g)        =  fmap f . fmap g

Эти законы гарантируют, что структура контейнера не будет изменена fmap, и что содержимое контейнера не будет переупорядочено в результате отображающей (mapping) операции.

Класс Monad определяет два основных оператора: >>= (связывание) и return.

infixl 1  >>, >>=

class  Monad m  where
    (>>=)        :: m a -> (a -> m b) -> m b
    (>>)         :: m a -> m b -> m b
    return       :: a -> m a
    fail         :: String -> m a

    m >> k       = m >>= \_ -> k

Операции связывания, >> и >>=, комбинируют два монадических значения, в то время как операция return вставляет значение в монаду (контейнер). Сигнатура >>= помогает понять эту операцию: ma >>= \v -> mb комбинирует монадическое значение ma, содержащее значение типа a, и функцию, которая принимает значение v типа a, возвращая монадическое значение mb. Результатом является комбинирование ma и mb в монадическое значение, содержащее b. Функция >> используется, когда функция не нуждается в значении, произведённом первым монадическим оператором.

Точный смысл связывания зависит, конечно, от конкретной монады. Например, в монаде IO связывание x >>= y последовательно выполняет два действия, передавая результат первого действия во второе. Для других встроенных монад, списков и типа Maybe, эти монадические операции могут пониматься в смысле передачи нуля или более значений из одного вычисления в следующее. Вскоре мы увидим это на примерах.

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

do e1 ; e2       =       e1 >> e2
do p <- e1; e2   =       e1 >>= \p -> e2

Если образец во второй форме do опровержим, неудача сопоставления с ним вызывает операцию fail. Это может сгенерировать ошибку (как в монаде IO) или вернуть «ноль» (как в монаде списка). Таким образом, более сложная трансляция такова:

do p <- e1; e2  =   e1 >>= (\v -> case v of p -> e2; _ -> fail "s")

где "s" – это строка, указывающая местоположение инструкции do для возможного использования в сообщении об ошибке. Например, в монаде ввода-вывода такое действие, как 'a' <- getChar, вызовет fail, если будет набран символ, отличный от 'a'. Это, в свою очередь, приведёт к завершению программы, поскольку в монаде ввода-вывода fail вызывает error.

Законы, управляющие >>= и return, таковы:

           return a >>= k           =  k a
           m >>= return             =  m
           xs >>= return . f        =  fmap f xs
           m >>= (\x -> k x >>= h)  =  (m >>= k) >>= h

Класс MonadPlus используется для монад, которые имеют элемент ноль и операцию плюс:

class (Monad m) => MonadPlus m where
    mzero           :: m a
    mplus           :: m a -> m a -> m a

Элемент ноль подчиняется следующим законам:

           m >>= \x -> mzero  =  mzero
           mzero >>= m        =  mzero

Для списков значение ноль – это [ ], пустой список. Монада ввода-вывода не имеет нулевого элемента и не принадлежит этому классу.

Законы, управляющие оператором mplus, таковы:

           m `mplus` mzero  =  m
           mzero `mplus` m  =  m

В монаде списка оператор mplus – это обычный оператор конкатенирования списков.

9.2 Встроенные монады

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

В случае списков монадическое связывание вызывает объединение набора вычислений для каждого элемента списка. При использовании со списками сигнатура >>= принимает вид:

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

То есть в случае заданного списка элементов типа a и функции, отображающей a на список элементов типа b, связывание применяет эту функцию к каждому входящему a и возвращает все сгенерированные списки b, сконкатенированные в один список. Функция return создаёт одноэлементный список. Эти операции уже должны быть знакомыми вам: list comprehension легко можно выразить через монадические операции, определённые для списков. Три следующих выражения делают одно и то же, используя разные синтаксические конструкции:

[(x,y) | x <- [1,2,3] , y <- [1,2,3], x /= y]

do x <- [1,2,3]
   y <- [1,2,3]
   True <- return (x /= y)
   return (x,y)

[1,2,3] >>= (\ x -> [1,2,3] >>= (\y -> return (x/=y) >>=
   (\r -> case r of True -> return (x,y)
                    _    -> fail "")))

Последнее определение зависит от определения метода fail в этой монаде как пустого списка. По существу каждая <- генерирует набор значений, который передаётся в оставшуюся часть монадического вычисления. Таким образом, x <- [1,2,3] вызывает оставшуюся часть монадического вычисления три раза, по разу на каждый элемент списка. Возвращаемое выражение, (x,y), будет вычислено для всех возможных комбинаций окружающих его связываний. В этом смысле монада списка может рассматриваться, как описывающая функции с многозначными аргументами. Например, такая функция:

mvLift2                 :: (a -> b -> c) -> [a] -> [b] -> [c]
mvLift2 f x y           =  do x' <- x
                              y' <- y
                              return (f x' y')

превращает обычную функцию с двумя аргументами (f) в функцию с множеством значений (списками аргументов), возвращающую значение для каждой возможной комбинации двух входящих аргументов. Например:

     mvLift2 (+) [1,3] [10,20,30]      =>   [11,21,31,13,23,33]
     mvLift2 (\a b->[a,b]) "ab" "cd"   =>   ["ac","ad","bc","bd"]
     mvLift2 (*) [1,2,4] []            =>   []

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

Монада, определённая для Maybe, подобна монаде списка: значение Nothing служит эквивалентом [], а Just x эквивалентно [x].

9.3 Использование монад

Объяснение монадических операторов и связанных с ними законов на самом деле не показывает, для чего можно использовать монады. Что они на самом деле обеспечивают – это модульность. То есть, при монадическом определении операций, мы можем спрятать внутренние механизмы способом, позволяющим прозрачным образом добавлять новые свойства в монаду. Статья Вадлера [10] – прекрасный пример того, как монады могут использоваться для создания модульных программ. Мы начнём с монады, взятой прямо из этой статьи, монады состояния, а затем построим более сложную монаду с похожим определением.

Монада состояния, построенная вокруг типа состояния S, выглядит таким образом:

data SM a = SM (S -> (a,S)) -- Монадический тип

instance Monad SM where
   -- определяет распространение состояния
   SM c1 >>= fc2        = SM (\s0 -> let (r,s1) = c1 s0
                                         SM c2 = fc2 r in
                                        c2 s1)
   return k             = SM (\s -> (k,s))

-- выделяет состояние из монады
readSM                  :: SM S
readSM                  =  SM (\s -> (s,s))

-- обновляет состояние в монаде
updateSM                :: (S -> S) -> SM () -- изменяет состояние
updateSM f              =  SM (\s -> ((), f s))

-- запускает вычисление в монаде SM
runSM                   :: S -> SM a -> (a,S)
runSM s0 (SM c)         =  c s0

Этот пример определяет новый тип, SM, как вычисление, неявно содержащее тип S. То есть, вычисление типа SM t определяет значение типа t, которое также взаимодействует с состоянием типа S (через чтение и запись). Определяется SM просто: он состоит из функций, которые принимают состояние и производят два результата: возвращаемое значение (произвольного типа) и изменённое состояние. Здесь мы не можем использовать синоним типа: мы нуждаемся в имени типа, таком как SM, для использования в объявлениях воплощений. Часто здесь используется объявление newtype вместо data.

Объявление воплощения определяет, как последовательно выполнить два вычисления и как определяется пустое вычисление. Связывание в последовательность (оператор >>=) определяет вычисление (обозначенное конструктором SM), которое передаёт начальное состояние s0 в вычисление c1, затем передаёт значение r, происходящее из указанного вычисления, в функцию, которая возвращает второе вычисление c2. Наконец, состояние, происходящее из c1, передаётся в c2 и окончательный результат – это результат c2.

Определение return проще: return вообще не меняет состояния; он служит для упаковки значения в монаду.

Имея в наличии базовые монадические операции >>= и return, определяющие последовательность, мы ещё нуждаемся в некоторых монадических примитивах. Монадические примитивы – это просто операции, которые используются внутри абстракции монады и подключаются к внутренним механизмам, выполняющим работу монады. Например, в монаде IO такие операторы, как putChar, являются примитивами, поскольку они имеют дело с внутренней работой монады IO. Аналогично наша монада состояния использует два примитива: readSM and updateSM. Отметим, что они зависят от внутренней структуры монады – изменение определения типа SM потребует изменения этих примитивов.

пределения readSM и updateSM просты: readSM извлекает состояние из монады, делая его доступным для наблюдения, а updateSM позволяет пользователю изменить состояние внутри монады. (Можно также было бы использовать writeSM в качестве примитива, но изменение (update) – в большинстве случаев более естественный способ работы с состоянием.)

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

В более широком ракурсе то, что мы пытаемся сделать, заключается в определении всего вычисления целиком, как серии шагов (функций с типом SM a), соединённых в последовательность с помощью >>= и return. Эти шаги могут взаимодействовать с состоянием (через readSM или updateSM) или игнорировать состояние. Однако использование (или неиспользование) этого состояния спрятано: мы не меняем вызовы наших вычислений и их последовательность в зависимости от того используют они S или нет.

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

Рассмотрим вычисления, которые требуют ресурсов некоторого вида. Если ресурс доступен, вычисление продолжается; когда ресурс недоступен – вычисление приостанавливается. Мы используем тип R, чтобы обозначить контролируемое нашей монадой вычисление, которое использует ресурс. Определение R имеет вид:

data R a = R (Resource -> (Resource, Either a (R a)))

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

Воплощение Monad для R таково:

instance Monad R where
   R c1 >>= fc2         = R (\r -> case c1 r of
                              (r', Left v)    -> let R c2 = fc2 v in
                                                   c2 r'
                              (r', Right pc1) -> (r', Right (pc1 >>= fc2)))
   return v             = R (\r -> (r, (Left v)))

Тип Resource используется таким же образом, как состояние в монаде состояния. Данное определение читается так: чтобы скомбинировать два «ресурсонесущих» вычисления c1 и fc2 (функция, производящая c2), передайте инициализирующий ресурс в c1. В результате получится одно из двух:

Приостановка должна принимать во внимание второе вычисление: pc1 приостанавливает только первое вычисление c1, то есть мы должны связать с ним c2, чтобы осуществить приостановку всего вычисления. Определение return оставляет ресурсы неизменными при перемещении v в монаду.

Это объявление воплощения определяет базовую структуру монады, но не задаёт способ использования ресурсов. Данная монада может использоваться для управления многими типами ресурсов или для реализации многих различных типов политик использования ресурсов. В качестве примера продемонстрируем очень простое определение ресурса: мы положим, что Resource – это Integer, представляющий доступные шаги вычислений:

type Resource           = Integer

Следующая функция делает шаг, пока такой шаг доступен:

step                    :: a -> R a
step v                  =  c where
                              c = R (\r -> if r /= 0 then (r-1, Left v)
                                                     else (r, Right c))

Конструкторы Left и Right являются частью типа Either. Данная функция продолжает вычисление в R, возвращая v до тех пор, пока доступен хотя бы один ресурс (шаг вычислений). Если ни одного шага не доступно, функция step приостанавливает текущее вычисление (эта «приостановка» удерживается в c) и передаёт приостановленное вычисление обратно в монаду.

Итак, мы имеем инструмент (монаду), позволяющий определить последовательность «ресурсонесущих» вычислений, и мы можем выразить (с помощью step) порядок использования ресурса. Теперь нужно обратиться к тому, как выражаются вычисления в этой монаде.

Рассмотрим инкрементирующую функцию нашей монады:

inc                     :: R Integer -> R Integer
inc i                   =  do iValue <- i
                              step (iValue+1)

Здесь инкремент определяется как единичный шаг вычислений. Оператор <- необходим, чтобы «вытащить» значение аргумента из монады; тип iValue – это Integer, а не R Integer.

Это определение не особо удовлетворительно, хотя бы в сравнении со стандартным определением инкремента. Нельзя ли изменить существующие операторы, вроде +, так, чтобы они работали в нашем монадическом мире? Мы начнём с набора «поднимающих» функций lifting. Они привносят в монаду существующую функциональность. Рассмотрим определение lift1 (оно слегка отличается от liftM1 из библиотеки Monad):

lift1                   :: (a -> b) -> (R a -> R b)
lift1 f                 =  \ra1 -> do a1 <- ra1
                                      step (f a1)

Здесь принимается функция f с одним аргументом, и создаётся функция в R, которая выполняет «поднятую» функцию за один шаг. При использовании lift1 функция inc принимает вид:

inc                     :: R Integer -> R Integer
inc i                   =  lift1 (i+1)

Это уже лучше, но всё ещё не идеально. Сначала добавим lift2:

lift2                   :: (a -> b -> c) -> (R a -> R b -> R c)
lift2 f                 =  \ra1 ra2 -> do a1 <- ra1
                                          a2 <- ra2
                                          step (f a1 a2)

Отметим, что эта функция явно задаёт порядок вычислений в «поднятой» функции: вычисление, производящее a1, происходит до вычисления для a2.

Используя lift2, мы можем создать новую версию == в монаде R:

(==*)                   :: Ord a => R a -> R a -> R Bool
(==*)                   =  lift2 (==)

Мы вынуждены использовать слегка отличающееся имя для этой новой функции, поскольку == уже занято, но в некоторых случаях мы можем использовать одно и то же имя для «поднятой» и «неподнятой» функций. Следующее объявление воплощения позволяет использовать в R все операторы из Num:

instance Num a => Num (R a) where
  (+)                   = lift2 (+)
  (-)                   = lift2 (-)
  negate                = lift1 negate
  (*)                   = lift2 (*)
  abs                   = lift1 abs
  fromInteger           = return . fromInteger

Функция fromInteger неявно применяется ко всем целым константам в программе на Haskell (см. Раздел 10.3); данное определение позволяет целым константам иметь тип R Integer. Теперь мы, наконец, можем записать инкремент в совершенно естественном стиле:

inc                     :: R Integer -> R Integer
inc x                   =  x + 1

Отметим, что мы не можем «поднять» класс Eq тем же способом, что и класс Num: сигнатура ==* несовместима с допустимыми перегрузками ==, поскольку результат ==* – это R Bool, а не Bool.

Для выражения представляющих интерес вычислений в R нам понадобится условный оператор. Поскольку мы не можем использовать if (он требует, чтобы условие имело тип Bool, а не R Bool), мы назовём эту функцию ifR:

ifR                     :: R Bool -> R a -> R a -> R a
ifR tst thn els         =  do t <- tst
                              if t then thn else els

Теперь мы готовы к большей программе в монаде R:

fact                    :: R Integer -> R Integer
fact x                  =  ifR (x ==* 0) 1 (x * fact (x-1))

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

Теперь мы готовы на самом деле запустить некоторые программы. Следующая функция запускает программу в R для заданного максимального числа вычислительных шагов:

run                     :: Resource -> R a -> Maybe a
run s (R p)             =  case (p s) of
                             (_, Left v) -> Just v
                             _           -> Nothing

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

          run 10 (fact 2)      =>       Just 2
          run 10 (fact 20)     =>       Nothing

И, наконец, мы можем добавить в эту монаду некоторую интересную функциональность. Рассмотрим следующую функцию:

(|||)                   :: R a -> R a -> R a

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

c1 ||| c2                =  oneStep c1 (\c1' -> c2 ||| c1')
   where
        oneStep          :: R a -> (R a -> R a) -> R a
        oneStep (R c1) f =
             R (\r -> case c1 1 of
                         (r', Left v)    -> (r+r'-1, Left v)
                         (r', Right c1') -> -- r' должен быть 0
                          let R next = f c1' in
                            next (r+r'-1))

Функция делает шаг в c1, возвращая его значение для завершённого c1, или, если c1 возвращает приостановленное вычисление (c1'), она вычисляет c2 ||| c1'. Функция oneStep делает один шаг, либо возвращая вычисленное значение, либо передавая оставшееся вычисление в f. Определяется oneStep просто: она передаёт c1 величину 1 в качестве аргумента-ресурса. Если мы достигли окончательного значения, оно возвращается, при этом приводится в порядок возвращаемый счётчик шагов (вычисление может завершиться без выполнения шага, так что возвращаемый счётчик ресурсов не обязательно равен 0). Если вычисление приостановлено, подправленный счётчик ресурсов передаётся в завершающее отложенное вычисление (continuation).

Теперь мы можем вычислять выражения вроде run 100 (fact (-1) ||| (fact 3)) без зацикливания, поскольку два вычисления чередуются (наше определение fact зацикливается для -1). На базе этой структуры можно создать множество вариаций. Например, мы можем расширить состояние, включив трассировку шагов вычисления. Мы можем также внедрить эту монаду внутрь стандартной монады IO, позволяя вычислениям в R взаимодействовать с внешним миром.

Хотя этот пример, возможно, и обладает повышенной сложностью по сравнению с другими в этом руководстве, он служит цели демонстрации мощности монад, как инструмента определения базовой семантики системы. Мы также привели этот пример как модель небольшого языка предметной области (Domain Specific Language, DSL), для определения которых Haskell особенно хорош. На Haskell создано множество других DSL, примеры которых можно найти на haskell.org. Особенно интересны Fran – язык реактивной анимации и Haskore – язык компьютерной музыки.

10 Числа

Haskell обладает богатой коллекцией численных типов, основанной на численных типах Scheme [7], которые в свою очередь основаны на Common Lisp [8]. (Эти языки, однако, динамически типизированы). Стандартные типы включают целые фиксированного и произвольного диапазона, рациональные числа, сформированные для каждого типа целых, вещественные одинарной и двойной точности и комплексные с плавающей точкой. Мы обрисуем здесь в общих чертах основные характеристики классов числовых типов, и отошлём читателя к §6.4 за подробностями.

10.1 Структура числового класса

Числовые классы типов (класс Num и производные от него) ответственны за многие стандартные классы Haskell. Отметим также, что Num является подклассом Eq, но не Ord; последнее объясняется тем, что предикаты порядка не применимы к комплексным числам. Однако, подкласс Real класса Num является также и подклассом Ord.

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

(+), (-), (*)           :: (Num a) => a -> a -> a
negate, abs             :: (Num a) => a -> a

negate – это функция, применяемая вместо префиксного оператора «-»; мы не можем вызвать его как (-), поскольку это будет функцией вычитания, поэтому для замены выбрано это имя. Например, -x*y эквивалентно negate (x*y). (Префиксный минус имеет тот же приоритет, что и инфиксный, который, конечно, ниже, чем приоритет умножения.)

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

Класс Integral обеспечивает операторы целочисленного деления и взятия остатка. Стандартными воплощениями Integral являются Integer (неограниченные или математические целые, также известные как «большие числа» («bignums»)) и Int (ограниченные, машинные целые, с диапазоном, эквивалентным по крайней мере 29-битным двоичным числам со знаком). Конкретная реализация Haskell может обеспечивать другие целые типы вдобавок к этим. Отметим, что Integral является подклассом Real, а не напрямую Num; это значит, что никто не пытался обеспечить гауссовы целые.

ПРИМЕЧАНИЕ

Гауссовы целые – это комплексные числа с целыми вещественной и мнимой частями – прим. пер.

Все остальные числовые типы попадают в класс Fractional, который обеспечивает обычный оператор деления (/). Следующий подкласс, Floating, содержит тригонометрические, логарифмические и экспоненциальные функции.

Подкласс RealFrac классов Fractional и Real предоставляет функцию properFraction, которая разделяет число на целую и вещественную части, и коллекцию функций, которые округляют число до целого по разным правилам:

properFraction          :: (Fractional a, Integral b) => a -> (b,a)
truncate, round,
floor, ceiling:         :: (Fractional a, Integral b) => a -> b

Подкласс RealFloat классов Floating и RealFrac предоставляет несколько специальных функций для эффективного доступа к компонентам числа с плавающей точкой: экспоненте (exponent) и мантиссе (significand). Стандартные типы Float и Double относятся к классу RealFloat.

10.2 Конструируемые числа

Стандартные числовые типы Int, Integer, Float и Double являются примитивами. Остальные создаются из них с помощью конструкторов типов.

Complex (из библиотеки Complex) является конструктором типа, создающим комплексный тип в классе Floating из типа, относящегося к RealFloat:

data (RealFloat a) => Complex a = !a :+ !a deriving (Eq, Text)

Символы « – это флаги строгости; они обсуждались в разделе 6.3. Отметим контекст RealFloat a, ограничивающий типы аргументов; таким образом, стандартные комплексные типы – это Complex Float и Complex Double. Из объявления data также видно, что комплексные числа записываются как x :+ y; аргументы представляют собой декартовы вещественную и мнимую части соответственно. Поскольку :+ является конструктором данных, мы можем использовать его при сопоставлении с образцом:

conjugate               :: (RealFloat a) => Complex a -> Complex a
conjugate (x:+y)        =  x :+ (-y)

Аналогично, конструктор типа Ratio (из библиотеки Rational) создаёт рациональный тип (то есть дробь) в классе RealFrac из воплощения Integral (Rational – это синоним типа для Ratio Integer). Ratio, однако, является абстрактным конструктором типа. Вместо конструктора данных, вроде :+, для дробей используется функция ‘%’, формирующая рациональное число из двух целых. Вместо сопоставления с образцом обеспечиваются функции, выделяющие компоненты (числитель и знаменатель):

(%)                     :: (Integral a) => a -> a -> Ratio a
numerator, denominator  :: (Integral a) => Ratio a -> a

В чём причина такой разницы? Комплексные числа в декартовом представлении уникальны – то, что упаковано конструктором :+, не имеет нетривиальных классов эквивалентности. С другой стороны, дроби не уникальны, но имеют каноническую форму (получающуюся как результат сокращения), которую реализация абстрактного типа данных обязана поддерживать; например, необязательно, что numerator (x%y) равен x, в то время как вещественная часть x:+y всегда равна x.

10.3 Приведение чисел и перегруженные литералы

Библиотека Prelude и стандартные библиотеки содержат ряд перегруженных функций, служащих для явного приведения:

fromInteger             :: (Num a) => Integer -> a
fromRational            :: (Fractional a) => Rational -> a
toInteger               :: (Integral a) => a -> Integer
toRational              :: (RealFrac a) => a -> Rational
fromIntegral            :: (Integral a, Num b) => a -> b
fromRealFrac            :: (RealFrac a, Fractional b) => a -> b
fromIntegral            =  fromInteger . toInteger
fromRealFrac            =  fromRational . toRational

Две из них используются неявно, обеспечивая перегруженные числовые литералы. Целые числа (без десятичной точки) в действительности эквивалентны применению функции fromInteger к численному значению, рассматриваемому как Integer. Аналогично, числа с плавающей точкой рассматриваются как применение fromRational к численному значению, рассматриваемому как Rational. Таким образом, 7 имеет тип (Num a) => a, а 7.3 имеет тип (Fractional a) => a. Это означает, что можно использовать числовые литералы в обобщённых числовых функциях, например:

halve                   :: (Fractional a) => a -> a
halve x                 =  x * 0.5

Этот, довольно непрямой, путь перегрузки чисел имеет то дополнительное преимущество, что метод интерпретации числа, как относящегося к данному типу, может быть указан в объявлении воплощения Integral или Fractional (поскольку fromInteger и fromRational являются операторами этих классов, соответственно). Например, воплощение Num для (RealFloat a) => Complex a содержит метод:

fromInteger x           =  fromInteger x :+ 0

Здесь определяется, что воплощение Complex для fromInteger производит комплексное число, чья вещественная часть предоставляется подходящим воплощением RealFloat для fromInteger. Таким же способом перегруженные числа могут использоваться даже в численных типах, определённых пользователем (например, в кватернионах).

В качестве другого примера напомним первое определение inc из Раздела 2:

inc                     :: Integer -> Integer
inc n                   =  n+1

При игнорировании сигнатуры типа наиболее общим типом inc будет (Num a) => a->a. Однако явная сигнатура типа допустима, поскольку она является более частной, чем основной тип (более общая сигнатура типа привела бы к статической ошибке). Сигнатура типа имеет эффект ограничения типа inc, и в данном случае приводит к тому, что нечто вроде inc (1::Float) окажется неверно типизированным.

10.4 Числовые типы по умолчанию

Рассмотрим следующее определение функции:

rms                     :: (Floating a) => a -> a -> a
rms x y                 =  sqrt ((x^2 + y^2) * 0.5)

Функция возведения в степень (^) (один из трёх различных стандартных операторов возведения в степень с разными типизациями, см. §6.8.5) имеет тип (Num a, Integral b) => a -> b -> a, и, поскольку 2 имеет тип (Num a) => a, тип x^2 таков (Num a, Integral b) => a. Здесь возникает проблема: нет способа разрешить перегрузку, связанную с переменной типа b, поскольку она присутствует в контексте, но нашла способ исчезнуть из выражения типа. По существу программист указал, что x должен быть возведён в квадрат, но не указал, следует ли возводить в квадрат, используя значение двойки типа Int или Integer. Конечно, мы можем исправить это:

rms x y            = sqrt ((x ^ (2::Integer) + y ^ (2::Integer)) * 0.5)

Ясно, однако, что подобный способ довольно утомителен.

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

show (read "xyz")

Какой тип имеет строка, которую предполагается прочитать? Это более серьёзная проблема, чем неоднозначность возведения в степень, поскольку там подойдёт любое воплощение Integral, а здесь можно ожидать сильно различающегося поведения, в зависимости от того, какое воплощение Text будет использовано для разрешения неоднозначности.

По причине различия между числовым и общим случаями проблемы неоднозначной перегрузки Haskell обеспечивает решение, ограничивающееся только числами: каждый модуль может содержать объявление умолчания (default declaration), состоящее из ключевого слова default, за которым следует заключённый в скобки и разделённый запятыми список, содержащий числовые монотипы (типы, не содержащие переменных). Когда встречается неоднозначная переменная типа (такая как b, описанная выше), если хотя бы один из её классов является числовым и все её классы – стандартные, принимается во внимание список умолчаний, и используется первый тип из списка, который удовлетворяет контексту переменной типа. Например, если действует объявление default (Int, Float), неоднозначное возведение в степень, приведённое выше, будет разрешено типом Int. (Подробности см. §4.3.4.)

«Умолчанием по умолчанию» является (Integer, Double), но (Integer, Rational, Double) также может подойти. Очень осторожные программисты могут предпочесть default (), что обеспечивает отсутствие умолчаний.

11 Модули

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

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

Дизайн модуля в Haskell относительно консервативен: пространства имён модулей совершенно плоские, а модули ни в каком смысле не «первоклассные». Модули имеют алфавитно-цифровые имена, которые должны начинаться с буквы в верхнем регистре. Формальная связь между модулем Haskell и файловой системой, которая (обычно) обеспечивает его поддержку, отсутствует. В частности, нет связи между именем модуля и именем файла, и более одного модуля могут предположительно находиться в одном файле (один модуль даже может охватывать несколько файлов). Конечно, конкретные реализации обычно принимают соглашения о более сильной связи между модулями и файлами.

Технически, модуль представляет собой одно большое объявление, которое начинается с ключевого слова module; вот пример модуля, который называется Tree:

module Tree ( Tree(Leaf,Branch), fringe ) where

data Tree a                =  Leaf a | Branch (Tree a) (Tree a)

fringe                     :: Tree a -> [a]
fringe (Leaf x)            =  [x]
fringe (Branch left right) =  fringe left ++ fringe right

Тип Tree и функция fringe уже должны быть знакомы вам; они приводились в качестве примеров в разделе 2.2.1.

Благодаря ключевому слову where отбивка действует на верхнем уровне модуля, таким образом, все объявления должны быть выровнены по одной колонке (обычно по первой). Отметим также, что имя модуля совпадает с именем типа, это допустимо.

Этот модуль явно экспортирует Tree, Leaf, Branch и fringe. Если список экспорта, следующий за ключевым словом module, опущен, все имена, доступные на верхнем уровне модуля, будут экспортированы (в примере выше все они экспортируются явно, так что результат будет тот же). Отметим, что имя типа и его конструкторы группируются вместе, как в Tree(Leaf,Branch). В качестве сокращения мы можем также написать Tree(..). Допустимо также экспортировать только часть конструкторов. Имя в списке экспорта не обязано быть локальным по отношению к экспортируемому модулю, любое имя в области видимости может быть включено в список экспорта.

Модуль Tree может теперь быть импортирован в некоторый другой модуль:

module Main (main) where
import Tree ( Tree(Leaf,Branch), fringe )

main = print (fringe (Branch (Leaf 1) (Leaf 2)))

Различные элементы, импортированные в модуль или экспортированные из модуля, называются сущностями (entities). Обратите внимание на явный список импорта в объявлении import; если опустить его, то это приведёт к импорту всех сущностей, экспортируемых из Tree.

11.1 Квалифицированные имена

Имеется очевидная проблема, связанная с прямым импортом имён в пространство имён модуля. Что, если два импортируемых модуля содержат различные сущности с одинаковыми именами? Haskell решает эту проблему, позволяя использовать квалифицированные имена (qualified names). Декларация импорта может использовать ключевое слово qualified, при этом импортированные имена должны использовать имя импортируемого модуля в качестве префикса. За такими префиксами должен следовать символ «.» без окружающих пробелов.

Квалификаторы являются частью лексического синтаксиса. Поэтому A.x и A . x представляют собой совершенно разные вещи; первое – это квалифицированное имя, а второе – это использование инфиксной функции «.».

Например, используя введённый выше модуль Tree:

module Fringe(fringe) where
import Tree(Tree(..))

fringe :: Tree a -> [a] -- Другое определение fringe
fringe (Leaf x) = [x]
fringe (Branch x y) = fringe x

module Main where
import Tree ( Tree(Leaf,Branch), fringe )
import qualified Fringe ( fringe )

main = do print (fringe (Branch (Leaf 1) (Leaf 2)))
          print (Fringe.fringe (Branch (Leaf 1) (Leaf 2)))

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

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

11.2 Абстрактные типы данных

Помимо управления пространствами имён модули обеспечивают единственный в Haskell путь для создания абстрактных типов данных (abstract data types, ADT). Ключевым свойством ADT является то, что представляющий тип (representation type) оказывается скрытым; все операции над ADT осуществляются на абстрактном уровне, который не зависит от представления. Например, хотя тип Tree достаточно прост для того, чтобы не было необходимости делать его абстрактным, подходящий ADT для него мог бы включать следующие операции:

data Tree a             -- только имя типа
leaf                    :: a -> Tree a
branch                  :: Tree a -> Tree a -> Tree a
cell                    :: Tree a -> a
left, right             :: Tree a -> Tree a
isLeaf                  :: Tree a -> Bool

Модуль, поддерживающий этот ADT, таков:

module TreeADT (Tree, leaf, branch, cell,
                left, right, isLeaf) where

data Tree a             =  Leaf a | Branch (Tree a) (Tree a)

leaf                    =  Leaf
branch                  =  Branch
cell (Leaf a)           =  a
left (Branch l r)       =  l
right (Branch l r)      =  r
isLeaf (Leaf _)         =  True
isLeaf _                =  False

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

11.3 Прочие свойства

Ниже приведён краткий обзор некоторых других аспектов системы модулей. Подробности см. в Описании.

Хотя система модулей Haskell относительно консервативна, имеется много правил, касающихся импорта и экспорта величин. Многие из них очевидны, например, недопустимость импорта в одну область видимости двух различных сущностей, имеющих одно и то же имя. Другие не столь очевидны, например, в рамках программы не может быть более одного объявления воплощения для данной комбинации типа и класса. Читателю следует справляться о подробностях в Описании (§5).

12 Ловушки типизации

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

12.1 Let-связанный полиморфизм

Любой язык, использующий систему типов Хиндли-Милнера, имеет ограничение, называемое let-связанным полиморфизмом (let-bound polymorphism), поскольку идентификаторы, не связанные использованием конструкций let или where (или верхним уровнем модуля), ограничены в отношении их полиморфизма. В частности, лямбда не может быть использована двумя различными путями. Например, данная программа не верна:

let f g = (g [], g 'a') -- выражение неверно типизировано
in f (\x->x)

поскольку g, связанная с лямбда-абстракцией, чей основной тип a->a, используется внутри f двумя различными способами: один раз с типом [a]->[a], а другой раз с типом Char->Char.

12.2 Перегрузка чисел

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

average xs              =  sum xs / length xs -- Неправильно!

Оператор (/) требует дробного (fractional) аргумента, а результат length имеет тип Int. Ошибка типизации должна быть исправлена явным приведением:

average                 :: (Fractional a) => [a] -> a
average xs              =  sum xs / fromIntegral (length xs)

12.3 Ограничение мономорфизма

Система типов Haskell имеет ограничение, связанное с классами типов, которые отсутствуют в системе типов Хиндли-Милнера: ограничение мономорфизма (monomorphism restriction). Причина этого ограничения связана с тонкой неоднозначностью типов и объяснена во всех подробностях в Описании (§4.5.5). Простое объяснение таково:

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

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

Наиболее часто нарушение этого ограничения происходит при частичном применении функций, как в этом определении sum из библиотеки Prelude:

sum                     =  foldl (+) 0

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

sum                     :: (Num a) => [a] -> a

Отметим также, что эта проблема не возникла бы, если бы мы написали:

sum xs                  =  foldl (+) 0 xs

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

13 Массивы

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

Различают два главных подхода к функциональным массивам: инкрементальное и монолитное определения. В инкрементальном случае мы имеем функцию, которая производит пустой массив заданного размера, и другую, которая принимает массив, индекс и значение, производя новый массив, который отличается от старого только данным индексом. Очевидно, что наивная реализация такой семантики массива будет крайне неэффективной, либо требуя новой копии массива для каждого последовательного переопределения, либо занимая линейное время при просмотре массива. Таким образом, попытка реализовать этот подход всерьёз приводит использованию сложного статического анализа и интеллектуальных механизмов времени исполнения во избежание избыточного копирования. С другой стороны, монолитный подход конструирует массив за один раз, без ссылок на промежуточные значения массива. Хотя Haskell содержит оператор инкрементального изменения массива, в основном механизм массивов монолитен.

Массивы не являются частью Prelude – операторы массивов содержаться в стандартных библиотеках. Любой модуль, использующий массивы, должен импортировать модуль Array.

13.1 Типы индексов

Библиотека Ix определяет класс типа для индексов массива:

class (Ord a) => Ix a where
    range      :: (a,a) -> [a]
    index      :: (a,a) a -> Int
    inRange    :: (a,a) -> a -> Bool

Объявления воплощений обеспечиваются для типов Int, Integer, Char, Bool и кортежей типов Ix вплоть до 5 элементов; кроме того, производные воплощения могут быть автоматически созданы для типов перечислений и кортежей. Указанные примитивные типы рассматриваются как индексы вектора, а кортежи – как индексы для многомерных прямоугольных массивов. Отметим, что первый аргумент каждой операции класса Ix – это пара индексов; она обычно задаёт границы массива (первый и последний индекс). Например, границами 10-элементного массива с отсчётом от 0 и индексами типа Int будет пара (0,9), а матрица 100 на 100 с отсчётом от 1 может иметь границы ((1,1),(100,100)). (Во многих других языках такие границы записывались бы в виде, подобном 1:100, 1:100, но представленная форма лучше удовлетворяет системе типов, поскольку каждая граница имеет тот же тип, что и индекс в общем.)

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

range (0,4)           =>   [0,1,2,3,4]
range ((0,0),(1,2))   =>   [(0,0),(0,1),(0,2),(1,0),(1,1),(1,2)]

Предикат inRange определяет, лежит ли индекс внутри заданной пары границ (для типов кортежей такая проверка осуществляется покомпонентно). Наконец, операция index позволяет адресовать некоторый элемент массива: для заданной пары границ и индекса в их диапазоне эта операция производит ординальное число индекса внутри диапазона, отсчитываемое от 0; например:

          index (1,9) 2               =>   1
          index ((0,0),(1,2)) (1,1)   =>   4

13.2 Создание массивов

Функция создания монолитного массива в Haskell формирует массив по его границам и списку пар индекс-значение (ассоциативный список):

array              :: (Ix a) => (a,a) -> [(a,b)] -> Array a b

Вот, например, определение массива квадратов чисел от 1 до 100:

squares            =  array (1,100) [(i, i*i) | i <- [1..100]]

Это выражение типично для массива – оно использует list comprehension для создания ассоциативного списка; фактически, такое использование приводит к тому, что выражение для массива очень напоминает array comprehension в языке Id [6].

Обращение к элементу массива по индексу выполняется с помощью инфиксного оператора «, а границы массива могут быть получены из функции bounds:

            squares!7        =>   49
            bounds squares   =>   (1,100)

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

mkArray            :: (Ix a) => (a -> b) -> (a,a) -> Array a b
mkArray f bnds     =  array bnds [(i, f i) | i <- range bnds]

Таким образом, мы можем определить squares как mkArray (\i -> i * i) (1,100).

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

fibs   :: Int -> Array Int Int
fibs n =  a where a = array (0,n) ([(0, 1), (1, 1)] ++
                                   [(i, a!(i-2) + a!(i-1)) | i <- [2..n]])

Другим примером такой реккурентности может служить матрица «волнового фронта» размером n на n, в которой элементы первой строки и первого столбца имеют значение 1, а другие элементы являются суммой их соседей, расположенных западнее, северо-западнее и севернее:

wavefront    :: Int -> Array (Int,Int) Int
wavefront n  =  a where
                a = array ((1,1),(n,n))
                     ([((1,j), 1) | j <- [1..n]] ++
                      [((i,1), 1) | i <- [2..n]] ++
                      [((i,j), a!(i,j-1) + a!(i-1,j-1) + a!(i-1,j))
                                  | i <- [2..n], j <- [2..n]])

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

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

13.3 Аккумуляция

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

accumArray :: (Ix a) => (b->c->b) -> b -> (a,a) -> [(a,c)] -> Array a b

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

hist          :: (Ix a, Integral b) => (a,a) -> [a] -> Array a b
hist bnds is  =  accumArray (+) 0 bnds [(i, 1) | i <- is, inRange bnds i]

Предположим, что мы имеем набор измерений на интервале [a; b] и хотим разбить интервал на декады, а затем посчитать число измерений, попадающих в каждую декаду:

decades      :: (RealFrac a) => a -> a -> [a] -> Array Int Int
decades a b  =  hist (0,9) . map decade
                where decade x = floor ((x - a) * s)
                      s        = 10 / (b - a)

13.4 Инкрементальные обновления

Кроме функции создания монолитного массива в Haskell имеется функция инкрементального обновления массива, записываемая как инфиксный оператор //. В простейшем случае изменение i–го элемента массива a на v записывается так: a // [(i, v)]. Причина наличия квадратных скобок в том, что правым аргументом (//) является ассоциативный список, обычно содержащий подходящее подмножество индексов массива:

(//)               :: (Ix a) => Array a b -> [(a,b)] -> Array a b

Как и для функции array, чтобы значения были определёнными, индексы в этом ассоциативном списке должны быть уникальными. Вот, например, функция, меняющая местами две строки матрицы:

swapRows :: (Ix a, Ix b, Enum b) => a -> a -> Array (a,b) c -> Array (a,b) c
swapRows i i' a = a // ([((i ,j), a!(i',j)) | j <- [jLo..jHi]] ++
                        [((i',j), a!(i ,j)) | j <- [jLo..jHi]])
                  where ((iLo,jLo),(iHi,jHi)) = bounds a

Присутствующая здесь конкатенация двух отдельных list comprehension над одним и тем же списком индексов j является, однако, несколько неэффективной; это похоже на реализацию двух циклов вместо одного в императивном языке. Не бойтесь, в Haskell мы можем совершить оптимизацию, эквивалентную слиянию циклов:

swapRows i i' a =  a // [assoc | j <- [jLo..jHi],
                                 assoc <- [((i ,j), a!(i',j)),
                                           ((i',j), a!(i, j))] ]
                   where ((iLo,jLo),(iHi,jHi)) = bounds a

13.5 Пример: умножение матриц

Мы завершим наше введение в массивы Haskell известным примером умножения матриц, воспользовавшись перегрузкой, чтобы определить весьма общую функцию. Поскольку требуется только умножение и сложение типа элементов матриц, мы получим функцию, которая умножает матрицы любого числового типа, не прикладывая особых усилий. К тому же, если мы будем внимательны, применяя только (!) и операции из Ix к индексам, мы получим обобщённость по типам индексов; на самом деле, не требуется, чтобы все четыре типа индексов строк и столбцов были одинаковыми. Для простоты, однако, мы требуем, чтобы типы индекса столбца левого сомножителя и строки правого сомножителя совпадали, и, более того, чтобы границы были равны:

matMult     :: (Ix a, Ix b, Ix c, Num d) =>
               Array (a,b) d -> Array (b,c) d -> Array (a,c) d
matMult x y =  array resultBounds
                     [((i,j), sum [x!(i,k) * y!(k,j) | k <- range (lj,uj)])
                                   | i <- range (li,ui),
                                     j <- range (lj',uj') ]
        where ((li,lj),(ui,uj))     = bounds x
              ((li',lj'),(ui',uj')) = bounds y
              resultBounds
                | (lj,uj)==(li',ui') = ((li,lj'),(ui,uj'))
                | otherwise = error "matMult: несогласованные границы"

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

matMult x y    =  accumArray (+) 0 resultBounds
                             [((i,j), x!(i,k) * y!(k,j))
                                     | i <- range (li,ui),
                                       j <- range (lj',uj')
                                       k <- range (lj,uj) ]
        where ((li,lj),(ui,uj))       = bounds x
              ((li',lj'),(ui',uj'))   = bounds y
              resultBounds
                | (lj,uj)==(li',ui')  = ((li,lj'),(ui,uj'))
                | otherwise           = error "matMult: несогласованные границы"

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

genMatMult      :: (Ix a, Ix b, Ix c) =>
                   ([f] -> g) -> (d -> e -> f) ->
                   Array (a,b) d -> Array (b,c) e -> Array (a,c) g
genMatMult sum' star x y =
      array resultBounds
            [((i,j), sum' [x!(i,k) `star` y!(k,j) | k <- range (lj,uj)])
                                 | i <- range (li,ui),
                                   j <- range (lj',uj') ]
        where ((li,lj),(ui,uj))      = bounds x
              ((li',lj'),(ui',uj'))  = bounds y
              resultBounds
                | (lj,uj)==(li',ui') = ((li,lj'),(ui,uj'))
                | otherwise          = error "matMult: несогласованные границы"

Фанаты APL признают полезность функций, подобных этим:

genMatMult maximum (-)
genMatMult and (==)

Для первой из них аргументами являются числовые матрицы, а (i; j)-ый элемент результата представляет собой максимальную разницу между соответствующими элементами i-ой строки и j-го столбца ввода. Во втором случае аргументами являются матрицы любого сравнимого на равенство типа, а результат представляет собой булеву матрицу, в которой элемент (i; j) имеет значение True тогда и только тогда, когда i-ая строка первого аргумента и j-ый столбец второго равны как векторы.

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

Подсказка: Используйте операцию index для определения длин.

14 Следующий этап

Большая коллекция ресурсов, посвященных Haskell, доступна на сайте haskell.org. Здесь можно найти компиляторы, демонстрации, статьи и много полезной информации о Haskell и функциональном программировании. Компиляторы и интерпретаторы Haskell работают почти на любом оборудовании и почти на всех операционных системах. Система Hugs имеет небольшой размер и перенесена на множество платформ – это прекрасный инструмент для изучения Haskell.

15 Благодарности

Благодарим Patricia Fasel и Mark Mundt из Лос-Аламоса, а также Nick Carriero, Charles Consel, Amir Kishon, Sandra Loosemore, Martin Odersky и David Rochberg из Йельского университета за их быструю вычитку ранних черновиков этого манускрипта. Особые благодарности Erik Meijer за его обширные комментарии к новому материалу, добавленному к версии 1.4 этого руководства.

Ссылки

  1. R. Bird. Introduction to Functional Programming using Haskell. Prentice Hall, New York, 1998.
  2. A.Davie. Introduction to Functional Programming System Using Haskell. Cambridge University Press, 1992.
  3. P. Hudak. Conception, evolution, and application of functional programming languages. ACM Computing Surveys, 21(3):359--411, 1989.
  4. Simon Peyton Jones (editor). Report on the Programming Language Haskell 98, A Non-strict Purely Functional Language. Yale University, Department of Computer Science Tech Report YALEU/DCS/RR-1106, Feb 1999.
  5. Simon Peyton Jones (editor) The Haskell 98 Library Report. Yale University, Department of Computer Science Tech Report YALEU/DCS/RR-1105, Feb 1999.
  6. R.S. Nikhil. Id (version 90.0) reference manual. Technical report, Massachusetts Institute of Technology, Laboratory for Computer Science, September 1990.
  7. J. Rees and W. Clinger (eds.). The revised3 report on the algorithmic language Scheme. SIGPLAN Notices, 21(12):37--79, December 1986.
  8. G.L. Steele Jr. Common Lisp: The Language. Digital Press, Burlington, Mass., 1984.
  9. P. Wadler. How to replace failure by a list of successes. In Proceedings of Conference on Functional Programming Languages and Computer Architecture, LNCS Vol. 201, pages 113--128. Springer Verlag, 1985.
  10. P. Wadler. Monads for Functional Programming In Advanced Functional Programming , Springer Verlag, LNCS 925, 1995.


Эта статья опубликована в журнале RSDN Magazine #1-2007. Информацию о журнале можно найти здесь
    Сообщений 1    Оценка 635        Оценить