Так же, как при использовании умножения и сложения, мы обычно явно указываем бинарные функции вместо оборачивания их в значения newtype и последующего использования функций mappend и mempty. Функция mconcat кажется полезной для типов Any и All, но обычно проще использовать функции or и and. Функция or принимает списки значений типа Bool и возвращает True, если какое-либо из них равно True. Функция and принимает те же значения и возвращает значение True, если все из них равны True.
Моноид Ordering
Помните тип Ordering? Он используется в качестве результата при сравнении сущностей и может иметь три значения: LT, EQ и GT, которые соответственно означают «меньше, чем», «равно» и «больше, чем».
ghci> 1 `compare` 2
LT
ghci> 2 `compare` 2
EQ
ghci> 3 `compare` 2
GT
При использовании чисел и значений типа Bool поиск моноидов сводился к просмотру уже существующих широко применяемых функций и их проверке на предмет того, проявляют ли они какое-либо поведение, присущее моноидам. При использовании типа Ordering нам придётся приложить больше старания, чтобы распознать моноид. Оказывается, его экземпляр класса Monoid настолько же интуитивен, насколько и предыдущие, которые мы уже встречали, и кроме того, весьма полезен:
instance Monoid Ordering where
mempty = EQ
LT `mappend` _ = LT
EQ `mappend` y = y
GT `mappend` _ = GT
Экземпляр определяется следующим образом: когда мы объединяем два значения типа Ordering с помощью функции mappend, сохраняется значение слева, если значение слева не равно EQ. Если значение слева равно EQ, результатом будет значение справа. Единичным значением является EQ. На первый взгляд, такой выбор может показаться несколько случайным, но на самом деле он имеет сходство с тем, как мы сравниваем слова в алфавитном порядке. Мы смотрим на первые две буквы, и, если они отличаются, уже можем решить, какое слово шло бы первым в словаре. Если же первые буквы равны, то мы переходим к сравнению следующей пары букв, повторяя процесс[13].
Например, сравнивая слова «EQ является единичным значением, обратите внимание, что если бы мы втиснули одну и ту же букву в одну и ту же позицию в обоих словах, их расположение друг относительно друга в алфавитном порядке осталось бы неизменным; к примеру, слово «
Важно, что в экземпляре класса Monoid для типа Ordering выражение x `mappend` y не равно выражению y `mappend` x. Поскольку первый параметр сохраняется, если он не равен EQ, LT `mappend` GT в результате вернёт LT, тогда как GT `mappend` LT в результате вернёт GT:
ghci> LT `mappend` GT
LT
ghci> GT `mappend` LT
GT
ghci> mempty `mappend` LT
LT
ghci> mempty `mappend` GT
GT
Хорошо, так чем же этот моноид полезен? Предположим, мы пишем функцию, которая принимает две строки, сравнивает их длину и возвращает значение типа Ordering. Но если строки имеют одинаковую длину, то вместо того, чтобы сразу вернуть значение EQ, мы хотим установить их расположение в алфавитном порядке.
Вот один из способов это записать:
lengthCompare :: String –> String –> Ordering
lengthCompare x y = let a = length x `compare` length y
b = x `compare` y
in if a == EQ then b else a
Результат сравнения длин мы присваиваем образцу a, результат сравнения по алфавиту – образцу b; затем, если оказывается, что длины равны, возвращаем их порядок по алфавиту.
Но, имея представление о том, что тип Ordering является моноидом, мы можем переписать эту функцию в более простом виде:
import Data.Monoid
lengthCompare :: String –> String –> Ordering
lengthCompare x y = (length x `compare` length y) `mappend`(x `compare` y)
Давайте это опробуем:
ghci> lengthCompare "ямб" "хорей"
LT
ghci> lengthCompare "ямб" "хор"
GT