五分钟理解什么是 Monad
更好的阅读体验
前置芝士
基础的 Haskell 语法
引言
对于很多想要了解函数式编程(Functional Programming)或者是 Haskell 的 OIer 而言,Monad 是一个非常不友好的概念,但当你理解了它之后你就会不理解为什么你之前不理解它(
一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已,这有什么难以理解的?
(上面这句话其实并不是 Monad 的严格定义,因为是“自函子范畴上的一个幺半群”的东西不一定是 Monad,比如 Applicative 也符合上述定义。这句话的出处是一篇著名的恶搞文章,所以也不要太认真地对待这句话。)
由于 Monad 这一概念本身对新手并不友好,大众 Monad 的误解有一箩筐,包括但不限于:
- Monads are impure.
- Monads are about effects.
- Monads are about state.
- Monads are about imperative sequencing.
- Monads are about IO.
- Monads are dependent on laziness.
- Monads are a “back-door” in the language to perform side-effects.
- Monads are an embedded imperative language inside Haskell.
- Monads require knowing abstract mathematics.
- Monads are unique to Haskell.
虽然在实际的开发中,Monad 的应用确实很复杂,但如果只是要理解 Monad 的概念的话其实很简单。(?)
解释
Functor
要理解 Monad 是什么,首先要理解 Functor 是什么。
Functor 可以理解为一个装着函数或值的盒子。 准确地说,这种盒子是“类型构造子”,符合一定规则的类型构造子才能被称为函子(Functor):
- 对任何对象
X \in \mathcal{C} ,恒有F(id_X) = id_{F(X)} 。
- 对任何态射
f:X \rightarrow Y ,g:Y \rightarrow Z ,恒有F(g \circ f) = F(g) \circ F(f) 。换言之,函子会保持单位态射与态射的复合。
用 Maybe 类型来举例就是:
data Maybe a = Just a | Nothing
这种数据类型无论在哪里都很常用,在 C++ 等语言里我们也经常处理除了 int,double 之外的各种复杂数据类型。但 Functor 并不是一个盒子那么简单,它还有一个重要定义是 fmap
:
class Functor f where
fmap :: (a -> b) -> f a -> f b
具体来说,就是一个函数,输入一个 输入一个值输出一个值的函数 f(x) 和一个 Functor x,返回一个 Functor f(x),简单来说就是对盒子里的值进行操作。
举个栗子:
Prelude> fmap (*3) Just 2
Just 6
总而言之,Functor 就是一个装着值的盒子,并且可以用 fmap
函数来用一个普通函数生成另一个 Functor。
Applicative
Applicative 是一种特殊的 Functor,它除了 fmap
外还实现了另一个函数 <*>
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
(了解面对对象的同学可以理解为 Applicative 继承了 Functor)
<*>
函数(熟悉 C++ 的同学可以理解为一个运算符)用来给给一个装在盒子里的值施加一个装在盒子里的函数,举个栗子:
Prelude> Just (*3) <*> Just 2
Just 6
准确的说,<*>
函数接受一个f (a -> b)
,返回一个f a -> f b
。即输入一个含有函数的 Applicative 和一个含有值的 Applicative,输出一个含有结果的 Applicative。
《Haskell 趣学指南》中有一个典型的示例:
myAction :: IO String
myAction = (++) `fmap` getLine <*> getLine
这段代码输入两行字符串并拼接,返回对应的 IO Action
,它具体做了什么?首先,geiLine
的返回值是一个 IO Action
,它也属于一种 Applicative,所以也实现了<*>
函数。Applicative 作为 Functor 的子集当然也实现了 fmap
,换句话说 (++)
fmapgetLine
实际上是一个IO ([Char] -> [Char])
,即一个含有函数的 Applicative,所以还要使用<*>
函数将其应用到另一个IO Action
即getLine
,最后的结果也是一个 Applicative(IO Action
)。
下面这个例子可能更好理解:
Prelude> (+) `fmap` Just 2 <*> Just 8
Just 10
前面的 fmap (+) Just 2
的值实际上等于Just (+2)
,所以整个表达式就等于Just (+2) <*> Just 8
,即Just 10
。
Applicative的定义:一个实现了<*>
函数的 Functor
总而言之,Applicative 就是一个装着值的盒子,并且可以用 fmap
函数来用一个普通函数生成另一个 Applicative,并且还可以用<*>
函数来用一个装在盒子(Functor)里的函数生成另一个 Applicative。
Monad
现在你已经了解了所有前置知识,可以了解 Monad 了。(大雾)
Monad 是一种特殊的 Applicative,它除了 <*>
外还实现了另一个函数 >>=
class (Applicative m) => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
>>=
函数用把一个装在盒子里的值直接扔进一个“处理数据并自动打包盒子的函数”,举个栗子:
threeTimes x = Just (x * 3)
Prelude> threeTimes 2
Just 6
但是盒子里的值是无法被直接取出的,所以我们不能直接用这个函数处理装在盒子里的值,所以此时要用到 Monad:
Prelude> Just 2 >>= threeTimes
Just 6
总而言之,Monad 就是一个装着值的盒子,并且可以用 fmap
函数来用一个普通函数生成另一个 Monad,并且还可以用<*>
函数来用一个装在盒子(Functor)里的函数生成另一个 Monad,并且还可以用>>=
函数来用一个“处理数据并自动打包盒子的函数”生成另一个 Monad。
这只是一个通俗的解释,更形式化的,一个 Monad 必须符合以下三条规则才能被称为 Monad:
(return x) >>= f == f x -- left unit law
m >>= return == m -- right unit law
(m >>= f) >>= g == m >>= (\x -> f x >>= g) -- associativity law
Haskell 中的 return
是指把一个值打包进 Monad 里,和其他语言中的 return
没什么关系。
如果你要实现自己的 Monad,则必须符合上述三条规则。
在数学中,符合上述三条定律的东西被称为幺半群。敏锐的读者可以立即察觉到,Monad 就是一个幺半群。
Left Unit Law
这条规则指的是,将一个值打包进 Monad 然后再>>=
到一个函数,等同于直接将值应用于这个函数。
Prelude> (return 3) >>= (\x -> Just (x * 2))
Just 6
Prelude> (\x -> Just (x * 2)) 3
Just 6
考虑到>>=
的定义,这是比较显然的。
Right Unit Law
这条规则指的是,将一个 Monad >>=
到return
函数,得到的是原来的 Monad。用形式化的语言来说就是“Monad 是一个自相似的几何结构”。
Prelude> Just 3 >>= return
Just 3
我们可以参照 >>=
的实现来理解:
instance Monad Maybe where
return x = Just x
(>>=) Nothing f = Nothing
(>>=) (Just x) f = f x
注意第四行代码,这里的Just
中的值被拆出来放进函数f
里,而这里的f
就是return
,所以又产生了原先的 Monad。
Associativity Law
>>=
函数符和结合律。
Prelude> f x = Just (x + 3)
Prelude> g x = Just (x * 2)
Prelude> (Just 1 >>= f) >>= g
Just 8
Prelude> Just 1 >>= (\x -> f x >>= g)
Just 8
这里的结合律体现的不是很明显,我们考虑形如a -> v
普通函数之间的运算.
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = (\x -> f (g x))
则有
(f . g). h == f . (g . h)
那么对于形如Monad m => a -> m b
的函数,有
(<=<) :: (Monad m) => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = (\x -> g x >>= f)
易得
(f <=< g) <=< h == f <=< (g <=< h)
示例如下:
Prelude> f x = Just (x + 3)
Prelude> g x = Just (x * 2)
Prelude> h x = Just (x + 5)
Prelude> ((f <=< g) <=< h) 7
Just 27
Prelude> (f <=< (g <=< h)) 7
Just 27
如果读者了解幺半群的概念,就会意识到这里return
就是<=<
运算的幺元(有时也称为单位元):
Prelude> (f <=< return) 7
Just 10
Prelude> (return <=< f) 7
Just 10
Monad 辟谣
谣言 1:Monad 不纯
回答:你才不纯!你全家都不纯!
则
幺半群是指对于一个半群
对于任意
则
例如
在 Haskell 中幺半群是这么定义的:
class Monoid m where
mempty :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mempty
其中 mempty
就是半群中的幺元,mappend
就是半群中的二元运算。