ghci> 3 * (2 * (8 * 5))
240
ghci> "ой" ++ ("лю" ++ "ли")
"ойлюли"
ghci> ("ой" ++ "лю") ++ "ли"
"ойлюли"
Мы называем это свойство ассоциативностью. Оператор *
ассоциативен, оператор ++
тоже. Однако оператор –
, например, не ассоциативен, поскольку выражения (5 – 3) – 4
и 5 – (3 – 4)
возвращают различные результаты.
Зная об этих свойствах, мы наконец-то наткнулись на моноиды!
Класс типов Monoid
1
является единицей по отношению к оператору *
, а значение []
является единицей по отношению к оператору ++
. В мире языка Haskell есть множество других моноидов, поэтому существует целый класс типов Monoid
. Он предназначен для типов, которые могут действовать как моноиды. Давайте посмотрим, как определён этот класс типов:
class Monoid m where
mempty :: m
mappend :: m –> m –> m mconcat :: [m] –> m
mconcat = foldr mappend mempty
Класс типов Monoid
определён в модуле Data.Monoid
. Давайте потратим некоторое время, чтобы как следует с ним познакомиться.
Прежде всего, нам видно, что экземпляры класса Monoid
могут быть определены только для конкретных типов, потому что идентификатор m
в определении класса типов не принимает никаких параметров типа. В этом состоит отличие от классов Functor
и Applicative
, которые требуют, чтобы их экземплярами были конструкторы типа, принимающие один параметр.
Первой функцией является mempty
. На самом деле это не функция, поскольку она не принимает параметров. Это полиморфная константа вроде minBound
из класса Bounded
. Значение mempty
представляет единицу для конкретного моноида.
Далее, у нас есть функция mappend
, которая, как вы уже, наверное, догадались, является бинарной. Она принимает два значения одного типа и возвращает ещё одно значение того же самого типа. Решение назвать так функцию mappend
было отчасти неудачным, поскольку это подразумевает, что мы в некотором роде присоединяем два значения. Тогда как оператор ++
действительно принимает два списка и присоединяет один в конец другого, оператор *
на самом деле не делает какого-либо присоединения – два числа просто перемножаются. Когда вы встретите другие экземпляры класса Monoid
, вы поймёте, что большинство из них тоже не присоединяют значения. Поэтому избегайте мыслить в терминах присоединения; просто рассматривайте mappend
как бинарную функцию, которая принимает два моноидных значения и возвращает третье.
Последней функцией в определении этого класса типов является mconcat
. Она принимает список моноидных значений и сокращает их до одного значения, применяя функцию mappend
между элементами списка. Она имеет реализацию по умолчанию, которая просто принимает значение mempty
в качестве начального и сворачивает список справа с помощью функции mappend
. Поскольку реализация по умолчанию хорошо подходит для большинства экземпляров, мы не будем сильно переживать по поводу функции mconcat
. Когда для какого-либо типа определяют экземпляр класса Monoid
, достаточно реализовать всего лишь методы mempty
и mappend
. Хотя для некоторых экземпляров функцию mconcat
можно реализовать более эффективно, в большинстве случаев реализация по умолчанию подходит идеально.
Законы моноидов
Прежде чем перейти к более конкретным экземплярам класса Monoid
, давайте кратко рассмотрим законы моноидов.
Вы узнали, что должно иметься значение, которое действует как тождество по отношению к бинарной функции, и что бинарная функция должна быть ассоциативна. Можно создать экземпляры класса Monoid
, которые не следуют этим правилам, но такие экземпляры никому не нужны, поскольку, когда мы используем класс типов Monoid
, мы полагаемся на то, что его экземпляры ведут себя как моноиды. Иначе какой в этом смысл? Именно поэтому при создании экземпляров класса Monoid
мы должны убедиться, что они следуют нижеприведённым законам:
• mempty
`mappend`
x
=
x
• x
`mappend`
mempty
=
x
• (x
`mappend`
y)
`mappend`
z
=
x
`mappend`
(y
`mappend`
z)