所以,我目前正在学习Haskell,并且希望确认或驳斥我对单子的理解。
从阅读CIS194课程中我得出的结论是,单子基本上是用于在自定义集合上定义自定义二元操作的“API”。
然后我去了解更多信息,我遇到了大量非常令人困惑的教程,试图澄清这件事,所以我不太确定了。
我有不错的数学背景,但我被所有比喻搞混了,现在寻求明确的是/否回答我的单子理解。
所以,我目前正在学习Haskell,并且希望确认或驳斥我对单子的理解。
从阅读CIS194课程中我得出的结论是,单子基本上是用于在自定义集合上定义自定义二元操作的“API”。
然后我去了解更多信息,我遇到了大量非常令人困惑的教程,试图澄清这件事,所以我不太确定了。
我有不错的数学背景,但我被所有比喻搞混了,现在寻求明确的是/否回答我的单子理解。
Monoid
就不是一个单子。 - duplode来自沃尔夫拉姆:
一个幺半群是一个集合,它在一个关联的二元操作下封闭,并且具有S中的身份元素I,使得对于所有a在S中,Ia=aI=a。
来自维基:
在抽象代数学中,幺半群是一种具有单个关联二元操作和恒等元素的代数结构。
所以你的直觉或多或少是正确的。
你应该记住的是,它不是为Haskell中的“自定义集”定义的,而是为类型定义的。区别很小(因为类型理论中的类型非常类似于集合理论中的集合),但可以定义Monoid实例的类型不需要表示数学集合的类型。
换句话说:类型描述了属于该类型的所有值的集合。 Monoid是一个“接口”,它声明任何声称遵守该接口的类型必须提供标识值,将两个该类型的值组合的二元操作,并且这些方程式应满足为了使所有通用Monoid操作正常工作(例如通用总和列表中的幺半群值)并且不产生不合逻辑/不一致的结果。
此外,请注意,该集合(类型)中存在一个身份元素是成为Monoid类的实例所必需的。
例如,自然数在加法下形成幺半群(标识= 0
):
0 + n = n
n + 0 = n
还有乘法(单位元为1
):
1 * n = n
n * 1 = n
同时,列表在 ++
下也构成一个幺半群(单位元为 []
):
[] ++ 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)
MyInt
值的列表执行 mconcat
操作,你将始终会得到 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的良好候选对象。例如,请考虑来自Foldable
类的foldMap
函数:
foldMap :: Monoid m => (a -> m) -> t a -> m
foldMap
不仅泛化了对结构的折叠,而且可以通过替换正确的Monoid实例来泛化如何执行累积。t
,我可以使用Sum
monoid和Product
monoid等在foldMap
中获得它们的总和或积等。<>
提供了方便。例如,有大量不同的Set实现,但对于它们所有的实现,s <> t
始终是两个相同类型的集合s
和t
的并集。这使我能够编写不关心底层set实现的代码,从而简化我的代码。同样的事情也适用于许多其他数据结构,例如:sequences, trees, maps, priority queues等。