图解 Functor、Applicative、Monad

functors-applicatives-and-monads-in-pictures

序言

这篇文章是对原文 Functors, Applicatives, And Monads In Pictures 的翻译,由 Aditya Bhargava 撰写,翻译时已取得作者授权。

它是了解函数式编程非常棒的一篇文章,但它的两篇中文译文已不再可用(404、全部图片丢失),另外仅剩的一篇却是以 Kotlin 为导向的,因此打算自己再翻译一版。

正文

有一个简单的值:

为其应用一个函数:

这很简单,让我们继续扩展它。我们说,任何一个值都可以放在一个上下文里,你可以认为上下文是一个盒子,然后把值放进去:

现在,当你将一个函数应用到该值时,你将得到不同的结果 —— 这取决于上下文是什么。这就是 Functors、Applicatives、Monads、Arrows 等存在的基础。有一个 Maybe 数据类型,它定义了两个相关的上下文:

data Maybe a = Nothing | Just a

马上我们就会看到,对 Just aNothing 应用一个函数,会有什么不同。现在我们先来看看 Functor。

Functors

当一个值被放在上下文里,你不能对其应用一个普通函数:

这时便需要 fmap 出场了,它对上下文了如指掌,知道如何为放在上下文里的值,应用一个函数。比如,你想为 Just 2 应用 (+3),使用 fmap

> fmap (+3) (Just 2)
Just 5

很快啊!fmap 就出色地完成了它的工作!但是 fmap 是咋知道如何应用函数的?

Functor 究竟是啥

Functor 是一个 typeclass,这是它的定义:

对于任意类型,只要它定义了 fmap 的处理方式,它就是一个 Functor。下面是 fmap 的工作原理:

因此我们可以:

> fmap (+3) (Just 2)
Just 5

然后 fmap 会魔法般地,应用该函数,因为 Maybe 就是一个 Functor。它定义了 fmap 如何作用于 Justs 和 Nothings:

instance Functor Maybe where
    fmap func (Just val) = Just (func val)
    fmap func Nothing = Nothing

这就是当我们写 fmap (+3) (Just 2) 时,幕后所发生的事:

然后,你是不是会想,让我试试把 (+3) 应用到 Nothing 上?

> fmap (+3) Nothing
Nothing

像黑客帝国中的 Morpheus,fmap 知道要做什么;你投给它一个 Nothing,它也会回给你一个 Nothing!现在就能理解 Maybe 存在的意义了。下面是在一个没有 Maybe 的语言中,操作数据库记录的例子:

post = Post.find_by_id(1)
if post
  return post.title
else
  return nil
end

但在 Haskell,只需:

fmap (getPostTitle) (findPost 1)

如果 findPost 返回了一篇文章,我们将使用 getPostTitle 得到它的标题。同时,如果返回的是 Nothing,我们也仅会得到一个 Nothing!很整洁对吧?<$>fmap 的中缀版本,因此你会经常看到:

getPostTitle <$> (findPost 1)

还有另外一个例子:当将一个函数,应用到一个列表,会发生什么?

列表也是一个函数!这是它的定义:

instance Functor [] where
    fmap = map

好吧,最后一个例子:当将一个函数,应用到另外一个函数,会发生什么?

fmap (+3) (+1)

首先,这是一个函数:

当我们将其应用到另一个函数时:

结果是另一个函数:

> import Control.Applicative
> let foo = fmap (+3) (+2)
> foo 10
15

所以说,函数也是 Functor

instance Functor ((->) r) where
    fmap f g = f . g

当你在一个函数上使用 fmap 时,你其实是在组合它们,使其成为一个新的函数!

Applicatives

ApplicativeFunctor 提高了一个层次。与 Functor 类似,我们的值,被放在一个上下文里:

但是,我们的函数,现在也被放在了上下文里!

让我们慢慢理解它。Control.Applicative 定义了 <*>,它知道如何,为一个放在上下文里的值,应用一个放在上下文里的函数。如 Just (+3) <*> Just 2 == Just 5

使用 <*> 会产生许多有趣的现象,如:

> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]

以下是一些 Applicative 能做,而 Functor 不能做的事。如果为两个上下文里的值,应用一个接收两个参数的函数呢?

> (+) <$> (Just 5)
Just (+5)
> Just (+5) <$> (Just 4)
ERROR ??? WHAT DOES THIS EVEN MEAN WHY IS THE FUNCTION WRAPPED IN A JUST

使用 Applicative

> (+) <$> (Just 5)
Just (+5)
> Just (+5) <*> (Just 3)
Just 8

Applicative 击败了 Functor。“大人可以使用任意数量参数的函数,”它说。“装备了 <$><*>,我可以接受任何函数,它们接受任意数量上下文里的值。然后,我将值传入,并得到一个传出的值!哈哈哈哈!”

> (*) <$> Just 5 <*> Just 3
Just 15

也可以通过 liftA2 达到与上面相同的效果:

> liftA2 (*) (Just 5) (Just 3)
Just 15

Monads

学习 Monad 的方法:

  1. 读一个计算机科学的 PhD。
  2. 抛之脑后,不要想它。因为在本节你不需要它!

Monad 添加了一个新的转变。

Functor 为上下文里的值,应用一个函数:

Applicative 为上下文里的值,应用一个上下文里的函数:

Monad 为上下文里的值,应用一个接受普通值、返回上下文值的函数。它有一个 >>=(读作 “bind”)函数做到这点。 让我们看一个例子。我们熟悉的 Maybe,它也是个 Monad:

假设 half 是一个仅对偶数有效的函数:

half x = if even x
           then Just (x `div` 2)
           else Nothing

我们喂给它一个上下文里的值,会发生什么?

我们需要用 >>= 将上下文里的值,推到该函数中。这是它的照片:

它是这样运行的:

> Just 3 >>= half
Nothing
> Just 4 >>= half
Just 2
> Nothing >>= half
Nothing

那么,内部到底发生了什么?Monad 是一个 typeclass,这是它的部分定义:

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b

其中 >>= 如图所示:

所以 Maybe 是一个 Monad:

instance Monad Maybe where
    Nothing >>= func = Nothing
    Just val >>= func  = func val

这是它和 Just 3 的运作过程!

并且,当你传入一个 Nothing,它会更简单:

你还可以将它们链式调用:

> Just 20 >>= half >>= half >>= half
Nothing

Cool!目前为止,我们知道 Maybe 是一个 Functor、一个 Applicative,还是个 Monad。 现在,让我们转向另一个例子:IO monad:

具体只看三个函数。getLine 不接收任何参数,并获取用户的输入:

getLine :: IO String

readFile 接收一个 string 类型的文件名,并返回文件的内容:

readFile :: FilePath -> IO String

putStrLn 接收一个 string,并将其打印:

putStrLn :: String -> IO ()

这三个函数都接收一个常规的值(或不接收),并返回上下文里的值。我们可以使用 >>= 将它们链起来!

getLine >>= readFile >>= putStrLn

好耶!前排围观 Monad 表演! Haskell 还为我们提供了 do,它是 Monad 的语法糖:

foo = do
    filename <- getLine
    contents <- readFile filename
    putStrLn contents

结论

  1. functor 是实现 Functor typeclass 的数据类型
  2. applicative 是实现 Applicative typeclass 的数据类型
  3. monad 是实现 Monad typeclass 的数据类型
  4. Maybe 实现了所有它们三个,因此是一个 functor、applicative,和 monad

它们三个的区别是什么?

  • functors:使用 fmap<$>,为上下文里的值,应用一个函数
  • applicatives:使用 <*>liftA,为上下文里的值,应用一个上下文里的函数
  • monads:使用 >>=liftM,为上下文里的值,应用一个接受普通值、返回上下文值的函数

所以,亲爱的朋友(我们已经算朋友了),我觉得我们都同意,monads 是一个简单、高明的想法。现在你已经对着本指南痛饮一番了,何不拉上 Mel Gibson 一醉方休呢。查看 LYAH 网站的 Monads 部分。Miran 写了很多深入的,我所忽略的细节。