所以,我现在正在学习 Haskell,我想确认或揭穿我对 monoid 的理解。
我从阅读CIS194课程中了解到,monoid 基本上是用于在自定义集上定义自定义二进制操作的“API”。
比起我去告诉自己更多信息,我偶然发现了大量非常令人困惑的教程试图澄清这件事,所以我不再那么确定了。
我有不错的数学背景,但我只是被所有的隐喻弄糊涂了,我正在寻找明确的是/否答案来回答我对幺半群的理解。
所以,我现在正在学习 Haskell,我想确认或揭穿我对 monoid 的理解。
我从阅读CIS194课程中了解到,monoid 基本上是用于在自定义集上定义自定义二进制操作的“API”。
比起我去告诉自己更多信息,我偶然发现了大量非常令人困惑的教程试图澄清这件事,所以我不再那么确定了。
我有不错的数学背景,但我只是被所有的隐喻弄糊涂了,我正在寻找明确的是/否答案来回答我对幺半群的理解。
来自沃尔夫拉姆:
幺半群是在关联二元运算下闭合的集合,并且在 S 中具有单位元素 I,使得对于 S 中的所有 a,Ia=aI=a。
来自维基:
在抽象代数(数学的一个分支)中,幺半群是具有单个关联二元运算和单位元素的代数结构。
所以你的直觉或多或少是正确的。
您只应该记住,它不是为 Haskell 中的“自定义集”定义的,而是一种类型。区别很小(因为类型论中的类型与集合论中的集合非常相似)但是您可以为其定义 Monoid 实例的类型不必是表示数学集合的类型。
换句话说:类型描述了属于该类型的所有值的集合。Monoid 是一个“接口”,它声明任何声称遵守该接口的类型都必须提供一个标识值,一个组合该类型的两个值的二元运算,并且为了使所有通用 Monoid 操作能够满足一些方程,这些方程应该满足按预期工作(例如对 monoid 值列表的通用求和),并且不会产生不合逻辑/不一致的结果。
另外,请注意,该集合(类型)中存在一个标识元素是类型成为 Monoid 类的实例所必需的。
例如,自然数在两个加法下形成一个 Monoid (identity = 0
):
0 + n = n
n + 0 = n
以及乘法(identity = 1
):
1 * n = n
n * 1 = n
++
还列出了(identity = )下的幺半群[]
:
[] ++ xs = xs
xs ++ [] = xs
同样,类型函数a -> a
在组合下形成一个幺半群(恒等式 = 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 <>
。
然而,幺半群概念的价值在于它与其他类型和类的关系。从文化上讲,我们认为这<>
是将相同类型的两个事物连接/附加在一起的自然方式。
考虑这个例子:
{-# LANGUAGE OverloadedStrings #-}
import Data.Monoid
greet x = "Hello, " <> x
该函数greet
具有极强的多态性——x
可以是字符串、字节字符串或文本,仅举几例。此外,在每种情况下,它基本上都按照您的期望进行 - 它附加x
到字符串“Hello,”。
此外,有很多算法可以处理任何可以积累的东西,这些算法非常适合泛化到 Monoid。例如考虑类中的foldMap
函数Foldable
:
foldMap :: Monoid m => (a -> m) -> t a -> m
不仅foldMap
概括了折叠结构的想法,而且我可以通过替换正确的 Monoid 实例来概括如何执行累加。
如果我有一个t
包含 Ints 的可折叠结构,我可以使用foldMap
monoidSum
来获得 Ints 的总和,或者使用 withProduct
来获得产品等。
最后,使用<>
提供了便利。例如,有大量不同的 Set 实现,但对于所有这些实现s <> t
来说,始终是两个集合s
和t
(相同类型)的并集。这使我能够编写与集合的底层实现无关的代码,从而简化我的代码。许多其他数据结构也是如此,例如序列、树、映射、优先级队列等。