现在开始学习Monad(单子)!听说很简单:
一个单子(Monad)说白了不过就是自函子范畴上的一个幺半群而已
好吧,在下还是从头看吧…
half函数
这次先不看概念,从使用的角度开始,假设有这样一个half函数:
1
2
3
|
half x = if even x
then Just (x `div` 2)
else Nothing
|
代码很简单,如果x是偶数就返回 Just x / 2
,如果是奇数就返回Nothing
,half的结果是一个Maybe,这里的half不能很方便的compose,比如我们想实现这样的逻辑:
1
|
half . half $ 8 -- half(half(8))
|
这是不能执行的,因为half的返回值是Maybe Int
,而它的参数类型是Int
,我们不能把Maybe Int
传给half函数,正常情况下我们需要在每一步都判断一下Maybe里的值是Just还是Nothing。
也就是说half函数是上边这样的,如果我们传一个3给它,他会返回一个被Maybe值–Nothing
,但是我们不能传一个Maybe值给half函数:
这时候如果我们想要把Maybe值硬塞给half函数该怎么办呢?我们需要一个东西把Maybe容器里封装的值抽出来,这就是Monad的作用,据说它长成下边的样子:
来看看Monad的庐山真面目。
Monad定义
Monad的定义如下
1
2
3
4
5
6
|
class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
fail :: String -> m a
|
其中 (>>=)
称为bind函数,(>>)
和 fail
两个函数是有默认实现的,所以在我们实现Monad实例的时候只需要实现return
和(>>=)
就可以了。
需要注意的是,Applicative是Monad的一个超类,也就是说
- Monad也是一个Functor和一个Applicative,所以
fmap
、pure
、(<*>)
都可以和Monad一起使用
- 实现Monad实例也需要实现Functor和Applicative实例
几个Monad实例
Maybe monad
Maybe的Monad实现如下
1
2
3
|
instance Monad Maybe where
Nothing >>= fun = Nothing
Just val >>= fun = fun val
|
有了这个Monad,我们就可以优雅的实现多个half的串联了:
1
2
3
4
5
|
> Just 20 >>= half >>= half >>= half
Nothing
> Just 64 >>= half >>= half >>= half
Just 8
|
Writer monad
想象一下我们要在一个函数返回结果的时候,同时返回一个log message,还是用half来举例子吧
1
2
|
half x = (x `div` 2, "half " ++ (show x) ++ "!")
half 8 -- (4,"half 8!")
|
我们修改了一下half的返回值: (res, log)
,和上边Maybe的例子一样,我们还是要设法实现:
1
|
half . half $ 8 -- half(half(8))
|
这时可以祭出Writer monad,每一个writer都由返回值和log组成:
1
|
data Writer w a = Writer { runWriter :: (a, w) }
|
我们需要为它实现Monad实例,还记得之前说的Functor,Applicative和Monad之间的关系吗?这三个都需要实现:
1
2
3
4
5
6
7
8
9
10
11
|
instance Functor (Writer w) where
fmap f (Writer (a, w)) = Writer (f a, w)
instance Monoid w => Applicative (Writer w) where
pure a = Writer (a, mempty)
Writer (f, w) <*> Writer (a, w') = Writer (f a, w <> w')
instance Monoid w => Monad (Writer w) where
return = pure
Writer (a, w) >>= f = let (b, w') = runWriter (f a)
in Writer (b, w <> w')
|
现在我们用Writer来重新定义一下half函数
1
2
3
4
|
half :: Int -> Writer String Int
half x = do
tell ("I just halved " ++ (show x) ++ "!")
return (x `div` 2)
|
这里我们用了do notation
,另外tell函数大概做了这样的事:
1
2
|
tell :: w -> Writer w ()
tell w = Writer ((), w)
|
现在half返回的结果是一个Writer,我们可以用runWriter得到结果和log:
1
2
|
half 8 -- Writer {runWriter = (4,"I just halved 8!")}
runWriter $ half 8 -- (4, "I just halved 8!")
|
于是我们可以优雅地:
1
2
|
runWriter $ half 16 >>= half >>= half
-- (2,"I just halved 16!I just halved 8!I just halved 4!")
|
这里举的几乎是最简单的例子,但是它可以让我们快速熟悉Writer monad,Writer monad里的w只要是Monoid
就可以。
当然了,有Writer就有Reader,但是这里就不多介绍了,有兴趣可以自己看一看。
Stream
我们一直写的都是Haskell代码,为了防止有人不太熟悉,我们来点Javascript,这一节的主角是Stream,而我们要用到baconjs
这个库:
1
2
3
4
5
6
7
8
9
10
|
const Bacon = require('baconjs')
const stream = new Bacon.Bus()
stream
.map(v => v.toUpperCase())
.onValue(v => console.log(v))
stream.push('Dog')
stream.push('Cat')
stream.push('Human')
|
上边这段代码很简单,输出如下:
你找到这里的Functor了吗?如果你不了解什么是Functor,可以看这篇文章
,注意那个map,它实现了下边这种类型:
1
|
(String -> String) -> Stream(String) -> Stream(String)
|
也就是把一个(String -> String)
的函数(在这里是toUpperCase
)作用在了整个stream上,也就是说这里的Stream是一个Functor。
假设现在我们的需求发生了变化:我们要用第三方服务处理这些words,比如说用百度翻译将这些单词翻译成中文,这里为了演示,我们用一个假的getChinese
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function getChinese(word) {
let chineseWord
switch (word) {
case 'Dog':
chineseWord = '狗'
break;
case 'Cat':
chineseWord = '猫'
break;
case 'Human':
chineseWord = '人'
break;
default:
chineseWord = '不知道';
break;
}
const promise = Promise.resolve(chineseWord)
const stream = Bacon.fromPromise(promise)
return stream
}
|
getChinese
函数的类型是 String -> Stream(String)
,这里map就不适用了,我们如果把这个函数传给map就相当于:
1
|
(String -> Stream(String)) -> Stream(String) -> Stream(Stream(String))
|
而我们最终想要的是Stream(String)
,那这里该用什么呢?没错,就是Monad,也就是说我们需要的是:
1
2
3
|
// m a -> (a -> m b) -> m b
// (a -> m b) -> m a -> m b
(String -> Stream(String)) -> Stream(String) -> Stream(String)
|
在baconjs中我们可以使用flatMap
:
1
2
3
4
5
6
7
8
9
10
|
const Bacon = require('baconjs')
const stream = new Bacon.Bus()
stream
.flatMap(w => getChinese(w))
.onValue(w => console.log(w))
stream.push('Dog')
stream.push('Cat')
stream.push('Human')
|
输出正如我们所料:
Ok,Javascript的例子就到这了。
Conclusion
Monad非常强大,有很多有用的Monad可以帮助我们使代码更加优雅和简洁,比如Writer monad, Reader monad, IO monad和State monad等。本文的例子基本都是最简单和最直观的,实际Monad的使用可能会更加复杂,这也是逐渐熟悉和学习的过程。
References
文章作者
杂毛小道
上次更新
2020-08-10
许可协议
署名 4.0 国际 (CC BY 4.0)