Первые два закона утверждают, что значение mempty
должно вести себя как единица по отношению к функции mappend
, а третий говорит, что функция mappend
должна быть ассоциативна (порядок, в котором мы используем функцию mappend
для сведения нескольких моноидных значений в одно, не имеет значения). Язык Haskell не проверяет определяемые экземпляры на соответствие этим законам, поэтому мы должны быть внимательными, чтобы наши экземпляры действительно выполняли их.
Познакомьтесь с некоторыми моноидами
Теперь, когда вы знаете, что такое моноиды, давайте изучим некоторые типы в языке Haskell, которые являются моноидами, посмотрим, как выглядят экземпляры класса Monoid
для них, и поговорим об их использовании.
Списки являются моноидами
Да, списки являются моноидами! Как вы уже видели, функция ++
с пустым списком []
образуют моноид. Экземпляр очень прост:
instance Monoid [a] where
mempty = []
mappend = (++)
Для списков имеется экземпляр класса Monoid
независимо от типа элементов, которые они содержат. Обратите внимание, что мы написали instance Monoid [a]
, а не instance Monoid []
, поскольку класс Monoid
требует конкретный тип для экземпляра.
При тестировании мы не встречаем сюрпризов:
ghci> [1,2,3] `mappend` [4,5,6]
[1,2,3,4,5,6]
ghci> ("один" `mappend` "два") `mappend` "три"
"одиндватри"
ghci> "один" `mappend` ("два" `mappend` "три")
"одиндватри"
ghci> "один" `mappend` "два" `mappend` "три"
"одиндватри"
ghci> "бах" `mappend` mempty
"бах"
ghci> mconcat [[1,2],[3,6],[9]]
[1,2,3,6,9]
ghci> mempty :: [a]
[]
Обратите внимание, что в последней строке мы написали явную аннотацию типа. Если бы было написано просто mempty
, то интерпретатор GHCi не знал бы, какой экземпляр использовать, поэтому мы должны были сказать, что нам нужен списковый экземпляр. Мы могли использовать общий тип [a]
(в отличие от указания [Int]
или [String]
), потому что пустой список может действовать так, будто он содержит любой тип.
Поскольку функция mconcat
имеет реализацию по умолчанию, мы получаем её просто так, когда определяем экземпляр класса Monoid
для какого-либо типа. В случае со списком функция mconcat
соответствует просто функции concat
. Она принимает список списков и «разглаживает» его, потому что это равнозначно вызову оператора ++
между всеми смежными списками, содержащимися в списке.
Законы моноидов действительно выполняются для экземпляра списка. Когда у нас есть несколько списков и мы объединяем их с помощью функции mappend
(или ++
), не имеет значения, какие списки мы соединяем первыми, поскольку так или иначе они соединяются на концах. Кроме того, пустой список действует как единица, поэтому всё хорошо.
Обратите внимание, что моноиды не требуют, чтобы результат выражения a `mappend` b
был равен результату выражения b `mappend` a
. В случае со списками они очевидно не равны:
ghci> "один" `mappend` "два"
"одиндва"
ghci> "два" `mappend` "один"
"дваодин"
И это нормально. Тот факт, что при умножении выражения 3 * 5
и 5 * 3
дают один и тот же результат, – это просто свойство умножения, но оно не выполняется для большинства моноидов.
Типы Product и Sum
Мы уже изучили один из способов рассматривать числа как моноиды: просто позволить бинарной функции быть оператором *
, а единичному значению – быть 1
. Ещё один способ для чисел быть моноидами состоит в том, чтобы в качестве бинарной функции выступал оператор +
, а в качестве единичного значения – значение 0
:
ghci> 0 + 4
4
ghci> 5 + 0
5
ghci> (1 + 3) + 5
9
ghci> 1 + (3 + 5)
9
Законы моноидов выполняются, потому что если вы прибавите 0 к любому числу, результатом будет то же самое число. Сложение также ассоциативно, поэтому здесь у нас нет никаких проблем.
Итак, в нашем распоряжении два одинаково правомерных способа для чисел быть моноидами. Какой же способ выбрать?.. Ладно, мы не обязаны выбирать! Вспомните, что когда имеется несколько способов определения для какого-то типа экземпляра одного и того же класса типов, мы можем обернуть этот тип в декларацию newtype
, а затем сделать для нового типа экземпляр класса типов по-другому. Можно совместить несовместимое.
Модуль Data.Monoid
экспортирует для этого два типа: Product
и Sum
.
Product
определён вот так:
newtype Product a = Product { getProduct :: a }