«Моноид» имеет значение идентичности, например 0 для сложения и 1 для умножения:

0 + value = value

1 * value = value

Естественно, идентичность идет в обоих направлениях:

value + 0 = value

value * 1 = value

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

(0 + 1) + 2 = 0 + (1 + 2)

(1 * 2) * 3 = 1 * (2 * 3)

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

[] ++ [1,2,3] = [1,2,3]

[1,2,3] ++ [] = [1,2,3]

[1,2,3] ++ [1,2,3] = [1,2,3,1,2,3]

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

"abc" ++ "" = "abc"
"" ++ "abc" = "abc"
"abc" ++ "xyz" = "abcxyz"

++ - оператор Haskell для добавления в списки. Я думаю, это намного лучше, чем набирать [1,2,3].append([1,2,3]) или что-то подобное. И это позволяет более читабельно размещать последовательные операции:

[1,2,3] ++ [4,5,6] ++ [7,8,9] = [1,2,3,4,5,6,7,8,9]

Моноид списков определяется в Haskell примерно так:

instance Monoid [a] where
  mempty = []
  mappend = (++)

mempty по-хаскелльски означает значение идентичности: «моноид пустой». mappend - это ассоциативная операция: «моноидное добавление». Называть вещи сложно.

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

instance Monoid All where
  mempty = All True
  mappend (All x) (All y) = All (x && y)
 
instance Monoid Any where
  mempty = Any False
  mappend (Any x) (Any y) = Any (x || y)

Это удобные логические моноиды для выражения конъюнкции и дизъюнкции. All полезен для определения истинности всего заданного набора значений, например логических и (здесь &&), а Any определяет, истинны ли какие-либо из них, например логическое или (||):

> All True <> All False
All {getAll = False}
> All True <> All True
All {getAll = True}
> Any True <> Any False
Any {getAny = True}
> Any True <> Any True
All {getAll = True}
> Any True <> Any False <> Any False <> Any False
Any {getAny = True}

Символ <> является псевдонимом для mappend. Это немного упрощает объединение операций добавления в цепочку. Поскольку mappend - это двоичная операция, такая как + или *, почему бы и здесь не использовать двоичный оператор? Также обратите внимание, что результаты вычисления моноидальных выражений, приведенных выше, заключены в фигурные скобки в соответствии с синтаксическим соглашением. Фактически, нам нужна эта упаковка, потому что иначе было бы трудно различить разные моноиды для одних и тех же типов значений. Например, суммы отличаются от произведений для чисел, поэтому мы не можем просто использовать сложение и умножение моноидально, не указав, какой моноид мы хотим использовать. Нам необходимо предоставить эту информацию, используя соответствующие моноидальные оболочки:

> Sum 1 <> Sum 2
Sum {getSum = 3}
> getProduct (Product 2 <> Product 2)
4

Как видите, getSum и getProduct - это просто функции доступа, и вы можете использовать их для извлечения значений из Sum и Product, если это необходимо.

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

> foldMap Sum [1,2,3]
Sum {getSum = 6}
> foldMap Product [1,2,3]
Product {getProduct = 6}
> foldMap All [True, True, True]
All {getAll = True}
> foldMap Any [False, False, True]
Any {getAny = True}

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

Есть еще другие моноиды, о которых стоит знать:

> Just [1,2,3] <> Nothing
Just [1,2,3]
> Just [] <> Just [1,2,3]
Just [1,2,3]

Just и Nothing - конструкторы для типа Maybe, который кодирует возможность того, что ожидаемое значение может отсутствовать. Поскольку Maybe имеет Monoid реализацию, мы можем mappend значения, содержащиеся в Just, если эти значения являются моноидами. Nothing служит идентификатором в этом случае.

> First (Just 1) <> First (Just 2)
First {getFirst = Just 1}
> Last (Just 1) <> Last (Just 2)
Last {getLast = Just 2}
> foldMap First [Just True, Nothing, Just False]
First {getFirst = Just True}
> foldMap Last [Just True, Nothing, Just False]
Last {getLast = Just False}

First - это моноид, который, учитывая серию Maybe значений, захватывает первое, которое является Just, а не Nothing. Last, как и ожидалось, делает обратное. Используя foldMap, мы также можем применить эти моноидальные конструкторы к спискам Maybe значений.

> Dual [1,2,3] <> Dual [4,5,6]
Dual {getDual = [4,5,6,1,2,3]}
> getDual $ foldMap (Dual . First) [Just True, Nothing, Just False]
First {getFirst = Just False}
> getDual $ foldMap (Dual . Last) [Just True, Nothing, Just False]
Last {getLast = Just True}

Последний пример немного странный и используется нечасто. Dual переворачивает аргументы соответствующего mappend. Как видите, он меняет функциональность First и Last и, как моноид для Maybe, также требует, чтобы значения, которые он содержит, были моноидами. Моноиды внутри моноидов: они прекрасны.

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