«Моноид» имеет значение идентичности, например 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 - это моноид без идентичности, например, непустой список. Абелев моноид - это моноид, который также является коммутативным, как сложение и умножение (но не списки или строки, для которых порядок важен). Вы также можете просто сказать коммутативный моноид или, если хотите, вообще не говорить о них. Но распространяйте информацию о моноидах. Они буквально повсюду - независимо от того, пользуетесь ли вы их полезными свойствами или нет. Очевидно, вы должны.