这篇文章是对原文 Functors, Applicatives, And Monads In Pictures 的翻译,由 Aditya Bhargava 撰写,翻译时已取得作者授权。
它是了解函数式编程非常棒的一篇文章,但它的两篇中文译文已不再可用(404、全部图片丢失),另外仅剩的一篇却是以 Kotlin 为导向的,因此打算自己再翻译一版。
有一个简单的值:
为其应用一个函数:
这很简单,让我们继续扩展它。我们说,任何一个值都可以放在一个上下文里,你可以认为上下文是一个盒子,然后把值放进去:
现在,当你将一个函数应用到该值时,你将得到不同的结果 —— 这取决于上下文是什么。这就是 Functors、Applicatives、Monads、Arrows 等存在的基础。有一个 Maybe
数据类型,它定义了两个相关的上下文:
data Maybe a = Nothing | Just a
马上我们就会看到,对 Just a
与 Nothing
应用一个函数,会有什么不同。现在我们先来看看 Functor。
当一个值被放在上下文里,你不能对其应用一个普通函数:
这时便需要 fmap
出场了,它对上下文了如指掌,知道如何为放在上下文里的值,应用一个函数。比如,你想为 Just 2
应用 (+3)
,使用 fmap
:
> fmap (+3) (Just 2)
Just 5
很快啊!fmap
就出色地完成了它的工作!但是 fmap
是咋知道如何应用函数的?
Functor
是一个 typeclass,这是它的定义:
对于任意类型,只要它定义了 fmap
的处理方式,它就是一个 Functor
。下面是 fmap
的工作原理:
因此我们可以:
> fmap (+3) (Just 2)
Just 5
然后 fmap
会魔法般地,应用该函数,因为 Maybe
就是一个 Functor
。它定义了 fmap
如何作用于 Just
s 和 Nothing
s:
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
时,你其实是在组合它们,使其成为一个新的函数!
Applicative
将 Functor
提高了一个层次。与 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
学习 Monad 的方法:
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
Functor
typeclass 的数据类型Applicative
typeclass 的数据类型Monad
typeclass 的数据类型Maybe
实现了所有它们三个,因此是一个 functor、applicative,和 monad它们三个的区别是什么?
fmap
或 <$>
,为上下文里的值,应用一个函数<*>
或 liftA
,为上下文里的值,应用一个上下文里的函数>>=
或 liftM
,为上下文里的值,应用一个接受普通值、返回上下文值的函数所以,亲爱的朋友(我们已经算朋友了),我觉得我们都同意,monads 是一个简单、高明的想法。现在你已经对着本指南痛饮一番了,何不拉上 Mel Gibson 一醉方休呢。查看 LYAH 网站的 Monads 部分。Miran 写了很多深入的,我所忽略的细节。