我对数学概念属性符号掌握得不好, 所以理解比较慢,
这篇文章是从概念性的内容去理解 Monad, 不精确, 可能具体到数学概念也不准确.
但是希望提供一个比较直观的方式去了解, Monad 是怎么来的?
概念上简单说 Monad 是"自函子范畴上的一个幺半群".
新概念很多, 函子, 自函子, 范畴, 半群, 幺半群.
模糊地讲, 这些就是数学的概念, 集合啦, 集合元素间的映射啦, 单位元啦,
Monad 概念
模糊理解的话, 函子可以当做是函数, a -> b
的映射, 当然也可以比函数更抽象点的东西,
然后"自函子", 涉及到类型的概念, 函子从一个集合 A 到另一个集合 B,
但我们把程序所有东西都放在一起的话, 函子可以认为是从 A 到 A 自己了, 所以是"自函子".
"范畴"我解释不来, 大致是这些函子的构成和关系, 具体看何幻的文章.
从前面这部分看, Haskell 把程序当成各种集合还有映射来看待了,
程序中, 无论值的变化, 甚至副作用的变化, 全都纳入到范畴里边来理解.
然后幺半群呢? 要理解这些概念, 就要知道相关的几个概念,
- 原群(Magma)
一个集合, 然后集合上的元素的二元操作, 操作结果都在这个集合内部,
一个例子, 比如 { true false }
, 还有二元操作 and
or
,
任何二元操作的结果都在集合内.
- 半群(Semigroup)
半群在原群的基础上增加了一个条件, 满足结合律:
比如所有非空的字符串的集合, 以及 concat
操作."a" `concat` "b"
得到 "ab"
还在集合内,
然后 ("a" `concat` "b") `concat` "c"
得到 "abc"
,
然后 "a" `concat` ("b" `concat` "c")
得到 "abc"
,
两者是等价的, 满足结合律.
- 幺半群(Monoid)
幺半群, 在半群的基础上增加一个条件, 存在幺元,
幺元跟任何元素 a
结合得到的都是 a
本身,
例子的话, 在上面这个非空字符串的例子当中再加上空字符串,
那么 "" `concat` "a"
得到 "a"
,
然后 "a" `concat` ""
得到 "a"
,
两者等价的, ""
就是这个集合的幺元.
- 群(Group)
群在幺半群的基础上在加上了一个条件, 存在逆元,
一个例子比如整数的集合, 其中(x + y) + z = x + (y + z)
满足结合律,x + 0 = 0 + x
存在幺元,x + (-x) = 0
存在逆元,
所以整数的集合就是一个幺半群了.
当然这个叙述就不精确了, 但你大致应该知道幺半群对应什么了,
集合, 二元操作闭合, 交换律, 幺元.
特别是字符串这个例子, 可以看到程序当中明显会有很多这样的对应,
我们大量使用数组, 数组也是满足这几个条件的,
还是那个 concat
操作, 闭合, 交换律, 幺元([]
), 都是成立,
然后数值计算, +
, *
这两个操作, 闭合, 交换律, 幺元, 也是存在的.
然后需要绕过来理解一下了, 对于函数, 对于副作用, 是不是也是幺半群?
函数吧, 有 f g h
三个函数, 然后有个复合函数的操作 compose
,(f `compose` g) `compose` h
是一个,f `compose` (g `compose` h)
是另一个,compose
是右边函数先计算的, 所以总是现在 h(x)
先计算, 最终是 f(g(h(x)))
.
这个是结合律.
可以直观理解一下, 函数结合知识结合的函数, 最终调用还是固定的顺序的.
另外幺元就是 function(x){ return x }
这个函数了. 左右都一样.
然后副作用呢, 我们把副作用拿函数包一下, function(){ someEffect() }
,
然后按照函数 compose
的操作, 还是有结合律的,
然后我们定义一个副作用为空的 Unit
操作, 可以作为幺元,
因为是空的副作用, 所以放在左边放在右边都是不影响的.
所以函数, 副作用, 这些也还是能按照幺半群来理解和约束的.
这样想下去, 程序里边大量的内容都是可以套用幺半群去理解了.
而关键位置就是结合律还有幺元, 对于函数来说就是函数复合还有 (x) => x
函数.
既然幺半群是程序当中普遍存在的结构, 也就能直接地接受这个概念了.
然后"自函子范畴上的幺半群", 也就是说限定在"自函子"而不是"普通集合元素"的幺半群了.
这个不能准确描述, 但是应该足够提供一些观念上的理解了, Monad 是怎么出来的...
Monad class
在 Haskell 里定义又要再另外理解了, 首先对幺半群来说还是清晰的,
而交换律作为 law 没有写在 class 的定义当中了,
class Monoid m where
-- 定义元素
mempty :: m
-- 定义闭合二元操作
mappend :: m -> m -> m
-- 定义多个元素的结合, 默认是用的 List 表示
-- 满足结合律, 所以 foldr 是从右边开始结合的, 跟左边结合效果一致
mconcat :: [m] -> m
mconcat = foldr mappend mempty
对应的 Monad 版本, 也有着跟 Monoid 对应的一些函数的结构,
class Monad m where
-- 定义幺元
return :: a -> m a
-- 对应上边的二元操作 mappend
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
-- 对应上边的多个元素的组合 mconcat
join :: m => m (m a) -> m a
这边容易分歧的用法出现了, 首先幺元的定义:
-- 在 Monoid 当中是
mappend :: m
-- 在 Monad 当中是
return :: a -> m a
-- 或者有的地方用是 pure 的名称
pure :: a -> m a
-- 那么, Monoid 中的二元操作
mappend :: m -> m -> m
-- 到了 Monad, 应该是
mappend :: m a -> m a -> m a
不过我们实际看到的是两个类型变量, a
b
:
-- 包含从 a 到 m b 态射的一个过程
(>>=) :: m a -> (a -> m b) -> m b
-- 包含 a 也包含 b 但是 a 被丢弃的过程, 比较可能是通过副作用丢弃了
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
这个仔细想想也可以理解, 比如 List Int
, 整数的数组,
经过映射之后, 可能还是 List Int
, 也可能通过 length
得到 Int
,
不过在 m a
这个类型约束当中, 不会是纯的 Int
了,
可能是 List String
, 比如 [1,2,3,4].map(JSON.stringify)
,
可能是 List Boolean
, 比如 [1,2,3,4].map(x => x > 0)
,
总之这个地方由于态射的存在而变化了.
至于为什么要这样定义, 如果说 a -> m b
这个过程不需要跟 m 发生作用,
那么我们用态射, 直接用 Functor 就能达成了,
class Functor f where
fmap :: (a -> b) -> f a -> f b
但是存在情况, 就是需要跟 m 发生作用的, 比如 IO, 就必然会,
然后是 flatMap 的情况, 计算过程要跟 List 这个构造器发生作用:
Prelude> (>>=) [1,2,3,4] (\x -> [0..x])
[0,1,0,1,2,0,1,2,3,0,1,2,3,4]
IO Monad 的特殊性在于主流语言习惯性不去管 IO,
但是按照 Monoid 这套理解下来, IO 确实用是这样的结构.
其他
里边的概念都太抽象了, 特别是范畴相关的, 这个写得不太能自圆其说.