terminal

现在开始学习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

也就是说half函数是上边这样的,如果我们传一个3给它,他会返回一个被Maybe值–Nothing,但是我们不能传一个Maybe值给half函数:

half_ouch

这时候如果我们想要把Maybe值硬塞给half函数该怎么办呢?我们需要一个东西把Maybe容器里封装的值抽出来,这就是Monad的作用,据说它长成下边的样子:

plunger

来看看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,所以fmappure(<*>)都可以和Monad一起使用
  • 实现Monad实例也需要实现Functor和Applicative实例

monad-proposal

几个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组成:

writer_monad
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 这个库:

logo
 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')

上边这段代码很简单,输出如下:

1
2
3
DOG
CAT
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')

输出正如我们所料:

1
2
3



Ok,Javascript的例子就到这了。

Conclusion

Monad非常强大,有很多有用的Monad可以帮助我们使代码更加优雅和简洁,比如Writer monad, Reader monad, IO monad和State monad等。本文的例子基本都是最简单和最直观的,实际Monad的使用可能会更加复杂,这也是逐渐熟悉和学习的过程。

References