原文是站在
Haskell
方面写的,其中涉及到一些Haskell
中的方法。这些方法对于JavaScript
开发者可能不太容易理解,所以可以去看上面的链接都需要梯子,想要看国内的文章,可以去
下面是一个值:
我们都知道怎么运用一个函数到这个值上面:
备注:图中
(+3)
表示的是一个进行加3
操作的函数。
很简单。让我们扩展一下,任何值都可以被放入一个上下文中。所以现在你可以将上下文想象成一个盒子,你可以将一个值放进这个盒子中。
现在当你在这个值上使用函数时,根据不同的上下文,你会得到不同的结果,这就是Functor
、Applicative
、Monad
和Arrow
等的基础概念。如图:数据类型 Maybe
定义了两个相关的上下文。
data Maybe a = Nothing | Just a复制代码
很快我们会看到对一个Just a
和一个Nothing
来说函数应用有什么不同. 首先我们来说Functor
(函子)
Functor
当一个值被封装在一个上下文里, 你就不能拿普通函数来应用:
这时,fmap
出现了,fmap
知道怎样将一个函数应用到一个带有上下文的值,举个例子,也许你想将(+3)
运用到Just 2
上面,使用fmap
:
> fmap (+3) (Just 2)Just 5复制代码
fmap
让我们知道它怎么做到的,但是fmap
怎么知道怎么将函数运用到这个值上?
Functor
是什么?
Functor
是一种,下面是它的定义:
typeclass
是一类定义了一些行为的接口。如果一种数据类型是typeclass
,那么这种数据类型就支持和执行在typeclass
中描述的行为。
一个Functor
是定义了fmap
的工作原理的任何数据类型,下面就描述了fmap
怎么工作的:
所以我们可以这样做:
> fmap (+3) (Just 2)Just 5复制代码
fmap
神奇地运用了这个函数。因为Maybe
是一个 Functor
,它申明了fmap
怎么运用到Just
和Nothing
:
instance Functor Maybe where fmap func (Just val) = Just (func val) fmap func Nothing = Nothing复制代码
当我们写下fmap (+3) (Just 2)
时,它正发生着下面的事情:
所以接下来你很喜欢已有的fmap
,但是当你运用(+3)
到Nothing
上呢?
> fmap (+3) NothingNothing复制代码
另一个例子,当你运用一个函数到一个list
上面会发生什么呢?
Lists
也是functor
,下面是它的定义:
instance Functor [] where fmap = map复制代码
最后一个例子:当你将一个函数运用到另一个函数上回发生什么呢?
fmap (+3) (+1)复制代码
下面是一个函数:
下面是一个函数应用到另一个函数上面:
结果也是另外一个函数:
> import Control.Applicative> let foo = fmap (+3) (+2)> foo 1015复制代码
所以函数也是Functor
。
instance Functor ((->) r) where fmap f g = f . g复制代码
当你在一个函数上使用fmap
时,其实你就是在做函数合成
注意: 目前为止我们做的是将上下文当作是一个容纳值的盒子. 特别要记住: 盒子是有效的记忆图像, 然而有时你并没有盒子. 有时你的 “
盒子
” 是个函数.
Applicative
Applicative
把它带到了一个新的层次。使用Applicative
,我们的值放在上下文中,就像Functor
:
而且我们的函数也可以放入上下文中:
完全理解Applicative
并不是开玩笑,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复制代码
Applicatives
:
> (+) <$> (Just 5)Just (+5)> Just (+5) <*> (Just 3)Just 8复制代码
Applicative
把Functor
推到了一边. “大腕儿用得起任意个参数的函数,” 他说. “用<$>
和<*>
武装之后, 我可以接受需要任意个未封装的值的函数. 然后我传进一些封装过的值, 再我就得到一个封装的值的输出!”
> (*) <$> Just 5 <*> Just 3Just 15复制代码
一个叫做liftA2
的函数也做一样的事:
> liftA2 (*) (Just 5) (Just 3)Just 15复制代码
Monad
怎么学习Monad
:
- 获得计算机科学的
PhD
- 把它扔在一边,因为在这个章节里,你不会需要它
Monad
更加麻烦了
Functor
应用函数到封装过的值:
Applicative
运用封装过的函数到封装过的值上面:
Monad
运用一个返回封装过的值的函数到一个封装过的值上,Monad
有一个函数>>=
(发音"bind
")来做这件事。
让我们来看一个例子,熟悉的Maybe
是一个monad
:
假设half
是仅仅对偶数才可用的函数:
half x = if even x then Just (x div 2) else Nothing复制代码
如果我们传给它一个封装过的值会发生什么?
我们需要使用>>=
将封装过的值挤进这个函数,下面是>>=
的图片:
它怎么起作用的:
> Just 3 >>= halfNothing> Just 4 >>= halfJust 2> Nothing >>= halfNothing复制代码
在内部发生了什么?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复制代码
下图就是Monad
对Just 3
发生了什么:
如果你传给它一个Nothing
,那就更简单了:
你也可以级联这个函数:
> Just 20 >>= half >>= half >>= halfNothing复制代码
所以现在我们知道了Maybe
同时是一个Functor
、Applicative
和Monad
。现在让我们开始另一个示例: the IO monad
3
个特别的函数。getLine
获取用户输入而不接收参数:
getLine :: IO String复制代码
readFile
需要一个字符串(文件名)参数,返回这个文件的内容:
readFile :: FilePath -> IO String复制代码
putStrLn
需要一个字符串参数,并且将它打印出来
putStrLn :: String -> IO ()复制代码
这三个函数都接收一个常规的值 (或者不接收值) 返回一个封装过的值. 我们可以用>>=
把一切串联起来!
getLine >>= readFile >>= putStrLn复制代码
总结:
- 一个
functor
是一种数据类型,它需要执行Functor typecla
描述的行为 - 一个
applicative
是一种数据类型,它需要执行Applicative typeclass
描述的行为 - 一个
monad
是一种数据类型,它需要执行Monad typecla
描述的行为 - 一个
Maybe
遵守这个,所以它既是functor、applicative
,也是monad
functor
、applicative
和monad
之间有什么不同?
functor
:你可以使用fmap
或<$>
将一个函数运用到一个封装的值上applicative
:你可以使用<*>
或liftA
将一个封装过的函数运用到一个封装的值上monad
:你可以使用>>=
或liftM
将一个返回封装值的函数运用到一个封装的值上