为什么需要 Applicative Functor
为啥需要 Applicative Functor
在学习 Applicative Functor 之前,首先是一个关键的问题,为什么有了 Functor 我们还需要 Applicative Functor?
Functor 的 fmap :: a -> b -> f a -> f b
不难理解,这里的 map 函数 a -> b
只有 1 个入参,那如果我们想要 map 包含多个入参的函数呢?比如 (+) :: a -> a -> a
需要 2 个参数,那用 fmap
会得到什么?看下面的代码
ghci> let a = fmap (+) [1,2,3]
ghci> :t a
a :: Num a => [a -> a]
可以看到,List Functor 里面的每一个值都变成了一个函数 a -> a
,也不难理解,Haskell Function 都是 Currying Functions,所以列表里面的元素相当于都成为了 (+)
左侧的入参,可以将其想象为
[\x -> 1 + x, \x -> 2 + x, \x -> 3 + x]
-- or, more simpler
[(1+), (2+), (3+)]
那么我们应该可以这么写代码(让 1
成为 (+)
右侧的入参)
ghci> fmap ($ 1) a
[2,3,4]
-- 2 = 1 + 1
-- 3 = 2 + 1
-- 4 = 3 + 1
fmap f x
的 map 函数 f
包含 2 个入参,那么 Functor x
里面的值会变成一个函数理解了上面你也就不难理解我们可以很轻松用 fmap
得到 Just (1+)
ghci> let justF = fmap (+) $ Just 1
ghci> :t justF
justF :: Num a => Maybe (a -> a)
现在设想,我们有一个值 Just 2
ghci> :t Just 2
Just 2 :: Num a => Maybe a
那么,我们能否以某种方式结合 Just (1+)
和 Just 2
得到 Just (1+2) = Just 3
呢?毕竟,这是一件很自然的事情
你会发现无法用 fmap
做到这一点,因为 fmap
能用来 map 的函数的类型是 a -> b
,是一个普通的一元函数,但现在函数在 Functor 里面(Maybe (a -> a)
而不是 a -> a
)。那么,如果存在这么一个操作符 magicOperator
,我们可以尝试推导它的类型签名,不难推出,应该是下面这样
magicOperator :: Maybe (a -> b) -> Maybe a -> Maybe b
虽然无法直接用 fmap
,但是我们可以通过 Pattern Matching 将 Just
里面的函数提取出来再用 fmap
apply Nothing _ = Nothing
apply (Just f) Nothing = Nothing
apply (Just f) (Just v) = fmap f (Just v)
但其他的 Functor 类型不一定支持你用 Pattern Matching 做提取 :(
如果将 Maybe
替换为一个抽象的 type constructor f
,那么我们就可以得到
magicOperator :: f (a -> b) -> f a -> f b
恭喜你,你刚刚自己推导出了 Applicative Functor 的 <*>
操作符
前面我们只考虑 map 函数入参为 1 个的场景,现在来考虑 n
个入参 的场景,用函数 g
表示 map 函数,它的类型签名是
g :: t1 -> t2 -> t3 -> ... -> tn -> t
假设我们有一堆的 Functor xi
,每一个 xi
的类型都是 f ti
,因为 Haskell Function 的函数都是 Currying Functions,可以先从 fmap g x1
开始推导
-- Note that
-- 1. fmap :: (a -> b) -> f a -> f b
-- 2. g :: t1 -> t2 -> t3 -> ... -> tn -> t
-- 3. x1 :: f t1
fmap g x1 :: f (t2 -> t3 -> ... -> tn -> t)
接下来可以尝试推导 fmap g x1 x2
,但很快会发现,类型对不上
fmap g x1 :: f (t2 -> t3 -> ... -> tn -> t)
x2 :: f t2
显然,这里应该用 Applicative Functor 的 <*>
,正确的形式应该是
fmap g x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
-- or, more simpler
g <$> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
<*>
,因为有的 Functor 里面是函数,有的 Functor 里面是值。<*>
可以将一个 Functor 里面的函数 apply 在另外一个 Functor 的值上,所以 <*>
起到了Function Application 的作用什么是 Applicative Functor
现在知道 Applicative Functor 的 <*>
是用来做 Function Application 的了,来看一下 Applicative 的定义
class (Functor f) => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
先看 pure
函数,看函数签名就很好理解,a -> f a
,只是把一个普通的值 a
放到了 Applicative Functor 里面,最直观的应用就是将一个函数放到 Applicative Functor 里面。在前面的例子中,我们有
fmap g x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
-- or, more simpler
g <$> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
如果用 pure
的话,也可以这么写
pure g <*> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
<*>
在前面我们自己推导过,含义已经清晰了,这里不再赘述
让我们来看 Applicative Functor 的一个例子,Maybe
类型其实就是一个 Applicative Functor
instance Applicative Maybe where
pure = Just
(Just f) <*> (Just x) = Just (f x)
_ <*> _ = Nothing
$, <$>, <*>
现在让我们来对比一下 3 个都表达了 Function Application 的操作符
($) :: (a -> b) -> a -> b
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
其中
($)
是普通的函数调用,将一个普通的一元函数应用在一个普通的值上,但通常我们不会写f $ x
而是直接写f x
<$>
是fmap
的中缀形式,将一个普通的一元函数应用在一个 Functor 上<*>
是 Applicative Functor 的操作符,将一个普通的一元函数 lift 到 Applicative Functor 里,然后应用在一个 Functor 上
总结
本文谈到了为什么有了 Functor,我们还需要 Applicative Functor。在我看来,Applicative Functor 最大的用处是我们可以方便地将任何一个普通函数 lift 到 Functor 里面,然后用 <*>
做 Function Application