在Haskell中, 所有Monad都为Applicative函子, 而所有Applicative函子都为Functor, 即
1 | class Applicative m => Monad m |
因此, 要想了解Monad, 首先要了解Functor和Applicative.
Functor, 函子
引例
大多数语言拥有空
概念, 表示不存在的值, 例如C中的NULL
1 |
表示空指针, 可以转换为任意指针类型;
在Scala中, null
表示空引用, 其类型为Null
, 是任意引用类型的子类型, 所以null
可以转换为任意引用类型:
1 | val foo: Null = null |
在Kotlin和Swift等语言中, 使用SomeType?
标识可空类型, 例如
1 | val a: Int? = 0 |
本质上, 可以为空的类型为空
与任意类型的和类型, 在Haskell中实现为Maybe
类型:
1 | data Maybe a = Nothing | Just a |
假设有如下Kotlin定义
1 | data class User(val name: String) |
对于其实例user
, 可以通过user.name
访问name, 即
1 | User -> String // user.name 中的 "." |
对于可空类型User?
, 考虑safe call运算符?.
,
1 | val name = user?.name // 从user中取出name, 否则返回null |
则实际完成了以下操作:
1 | Maybe User -> Maybe String // user?.name 中的 "?." |
也就是通过使用?.
运算符将(User -> String)
映射为(Maybe User -> Maybe String)
.
一般地, 对于任意函数(a -> b)
都可以映射得到(Maybe a -> Maybe b)
, 以fmap表示, 则可以有如下实现:
1 | fmap :: (a -> b) -> Maybe a -> Maybe b |
Functor包含了对这种映射的抽象. 事实上, Maybe也是最简单的Functor.
定义
函子在范畴论中定义如下:
设
- 将每个对象
映射至一对象 上 - 将每个态射
映射至一态射 上, 使之满足下列条件:
2.1. 对任何对象, 恒有
2.2. 对任何态射, 恒有 .
换言之, 函子会保持单位态射与态射的复合.
一个由一范畴映射至其自身的函子称之为“自函子”.
以Maybe为例:
- 1 即为构造
Just
- 2 为函数
fmap
- 2.1 表示不变
fmap id === id
- 2.2 表示组合
fmap (f . g) === (fmap f) . (fmap g)
在Haskell中, Functor表示为类型类:
1 | class Functor f where |
成为Functor需要满足条件2.1与2.2; 这里可以验证Maybe的上述fmap实现满足条件.
Applicative函子
假设有如下Kotlin定义
1 | val f: ((A) -> B)? = null |
那么将f施用给a会有点麻烦, 可以使用以下表达式:
1 | val g: (A?) -> B? = { a -> a?.let { f?.invoke(it) } } |
完成((A) -> B)?
到(A?) -> B?
的映射, Applicative函子包含了对这种映射的抽象.
定义
Applicative是Functor和Monad的中间结构, 在Haskell标识为类型类:
1 | class Functor f => Applicative f where |
pure
将值装入Applicative, 对应Maybe即为Just
;(<*>)
将装入Applicative的函数映射为装入Applicative中的值间的函数, 对应Maybe为Maybe (a -> b) -> Maybe a -> Maybe b
成为Applicative函子需满足以下条件:
1 | pure id <*> v = v // 不变 |
Maybe的Applicative实现:
1 | instance Applicative Maybe where |
Applicative使函子拥有了将装入函子的函数应用给装入函子的值的能力.
Monad, 单子
引例
假设有如下Kotlin定义
1 | data class User(val name: String?) |
那么对于可空类型User?实例user, 调用safe call运算符“user?.name”时, 理论上会得到String??类型, 但是“可以为空的可空类型”理应与可空类型等价, 所以String??自动折叠为String?类型.
在Haskell中用Maybe来表示, 即
1 | (Just a | Nothing) | Nothing === Just a | Nothing |
也就是
1 | Maybe (Maybe String) === Maybe String |
但事实并非如此, Maybe (Maybe String)
与 Maybe String
是两种不同的类型; Monad则可以进行这种折叠操作, 或者避免出现这种嵌套类型.
定义
Monad在范畴论中定义如下:
分别是单位
与乘法
(左右皆为 的自然变换). 此处 与 经水平复合而得. (两者皆为 的自然变换). 此处 表示由函子 到自身的恒等自然变换.
在Haskell中, Monad表示为类型类:
1 | class Applicative m => Monad m where |
与范畴论定义对应如下:
- return构造即为单位
- (>>=)函数为乘法
表示结合, 即 `m >>= (\x -> k x >>= h) === (m >>= k) >>= h` 表示左右单位, 即 `m >>= return === m` 和 `return a >>= k === k a`
Maybe的Monad可以如下实现:
1 | instance Monad Maybe where |
回到引例, 我们可以这样对Maybe (Maybe String)
进行折叠:
1 | a :: Maybe (Maybe String) |
或者避免产生Maybe (Maybe String)
:
1 | user :: Maybe User |
Monad是自函子范畴上的幺半群
半群
定义:
集合S和其上的二元运算·:S×S→S。若·满足结合律,即:∀x,y,z∈S,有(x·y)·z=x·(y·z), 则称有序对(S,·)为半群, 运算·称为该半群的乘法.
之所以称为半群, 是因为半群只满足群的部分性质.
在Haskell中, 半群用类型类Semigroup表示:
1 | class Semigroup a where |
(<>)
为半群的二元运算.
幺半群 (单位半群)
幺半群即含有幺元(单位元)的半群, 即: ∃e∈S,使得∀s∈S, e·s=s·e=s, 则S称为幺半群(独异点).
在Haskell中, 幺半群用Monoid表示:
1 | class Semigroup a => Monoid a where |
mempty
为幺半群的单位元.
幺半群的例子
(Int, *) e = 1
1 * x == x * 1 == ×
(a * b) * c == a * (b * c)
(Int, +) e = 0
0 + x == x + 0 == x
(a + b) + c == a + (b + c)
(String, ++) e = “”
"" ++ x == x ++ "" == x
(a ++ b) ++ c == a ++ (b ++ c)
Monad幺半群
Monad的范畴论定义中, 若将
对于Haskell中的Monad, 其单位元为return, 乘法为(>>=):
- 左单位元:
m >>= return === m
- 右单位元:
return a >>= k === k a
- 结合:
m >>= (\x -> k x >>= h) === (m >>= k) >>= h
Monad例子
List, 集合
1 | data List a = Nil | Cons a (List a) |
Result, 错误处理
1 | data Result e a = Err e | Ok a |
Promise, 异步
1 | data Promise e a = Reject e | Resolve a |
参考资料
https://zh.wikipedia.org/wiki/函子
https://zh.wikipedia.org/wiki/單子_(範疇論)
https://zh.wikipedia.org/wiki/半群
https://hackage.haskell.org/package/base-4.18.0.0/docs/src/GHC.Base.html