我对幺半群的理解是否有效?

Is my understanding of monoid valid?

所以,我目前正在学习 Haskell,我想确认或揭穿我对幺半群的理解。

What I figured out from reading CIS194 course is that monoid is basically "API" for defining custom binary operation on custom set.

比起我去了解更多信息,我偶然发现了大量非常令人困惑的教程试图澄清这件事,所以我不再那么确定了。

我有不错的数学背景,但我只是对所有隐喻感到困惑,我正在寻找明确的yes/no答案来解释我对幺半群的理解。

From Wikipedia:

In abstract algebra, a branch of mathematics, a monoid is an algebraic structure with a single associative binary operation and an identity element.

我认为你的理解是正确的。从编程的角度来看,Monoid 是一个接口,有两个必须实现的"methods"。

您的描述中似乎唯一缺少的部分是 "identity",没有它您描述的是 Semigroup

任何具有 "zero" 或 "empty" 的东西以及组合两个值的方法都可以是 Monoid。需要注意的一件事是,set/type 可能以不止一种方式成为 Monoid,例如通过 addition 和身份 0 的数字,或 multiplication 身份 1.

来自 Wolfram:

A monoid is a set that is closed under an associative binary operation and has an identity element I in S such that for all a in S, Ia=aI=a.

来自维基:

In abstract algebra, a branch of mathematics, a monoid is an algebraic structure with a single associative binary operation and an identity element.

所以你的直觉或多或少是正确的。

您只应记住,它不是为 Haskell 中的 "custom set" 定义的,而是一种类型。区别很小(因为类型论中的类型与集合论中的集合非常相似)但是您可以为其定义 Monoid 实例的类型不必是表示数学集合的类型。

换句话说:一个类型描述了属于该类型的所有值的集合。 Monoid 是一个 "interface" 声明任何声称遵守该接口的类型都必须提供一个标识值,一个结合该类型的两个值的二元运算,并且有一些方程式应该满足这些方程以便所有通用 Monoid操作按预期工作(例如幺半群值列表的一般求和)而不产生 illogical/inconsistent 结果。

另外,请注意,要使类型成为 Monoid 的实例,需要在该集合(类型)中存在标识元素 class。

例如自然数在两个加法下形成一个幺半群(identity = 0):

0 + n = n
n + 0 = n

以及乘法(恒等式=1):

1 * n = n
n * 1 = n

还列出 ++ 下的一个幺半群(identity = []):

[] ++ xs = xs
xs ++ [] = xs

此外,类型 a -> a 的函数在组合下形成一个幺半群 (identity = id)

id . f = f
f . id = f

所以请务必记住,Monoid 不是关于表示集合的类型,而是关于当被视为集合时的类型,可以这么说。


作为错误构造的 Monoid 实例的示例,请考虑:

import Data.Monoid

newtype MyInt = MyInt Int deriving Show

instance Monoid MyInt where
  mempty = MyInt 0
  mappend (MyInt a) (MyInt b) = MyInt (a * b)

如果您现在尝试 mconcat 一个 MyInt 值的列表,您将始终得到 MyInt 0 作为结果,因为标识值 0 和二元运算*一起玩不好:

λ> mconcat [MyInt 1, MyInt 2]
MyInt 0

在基本层面上你是对的 - 它只是一个 API 的二元运算符,我们用 <> 表示。

然而,幺半群概念的价值在于它与其他类型和 classes 的关系。从文化上讲,我们已经决定 <> 是 joining/appending 两个相同类型的东西放在一起的自然方式。

考虑这个例子:

{-# LANGUAGE OverloadedStrings #-}

import Data.Monoid

greet x = "Hello, " <> x

函数 greet 具有极高的多态性 - x 可以是字符串、字节字符串或文本,仅举几例。此外,在每种情况下,它基本上都按照您的预期进行 - 它将 x 附加到字符串 `"Hello, ".

此外,有很多算法可以处理任何可以累积的东西,这些都是泛化到 Monoid 的很好的候选者。例如,考虑来自 Foldable class:

foldMap 函数
foldMap ::  Monoid m => (a -> m) -> t a -> m

不仅 foldMap 概括了折叠结构的想法,而且我可以概括如何通过替换正确的 Monoid 实例来执行累加。

如果我有一个包含 Ints 的可折叠结构 t,我可以使用 foldMapSum 幺半群来获得 Ints 的总和,或者使用 Product获取产品等

最后,使用 <> 提供了便利。例如,有大量不同的 Set 实现,但对于所有这些实现,s <> t 始终是两个集合 st(相同类型)的并集。这使我能够编写与集合的底层实现无关的代码,从而简化了我的代码。对于许多其他数据结构也可以这样说,例如序列、树、映射、优先队列等