为什么需要 Applicative Functor

信息
阅读本文之前,我假设你对 Functor 有很好的理解

在学习 Applicative Functor 之前,首先是一个关键的问题,为什么有了 Functor 我们还需要 Applicative Functor?

Functor 的 fmap :: a -> b -> f a -> f b 不难理解,这里的 map 函数 a -> b 只有 1 个入参,那如果我们想要 map 包含多个入参的函数呢?比如 (+) :: a -> a -> a 需要 2 个参数,那用 fmap 会得到什么?看下面的代码

haskell

ghci> let a = fmap (+) [1,2,3]
ghci> :t a
a :: Num a => [a -> a]

可以看到,List Functor 里面的每一个值都变成了一个函数 a -> a,也不难理解,Haskell Function 都是 Currying Functions,所以列表里面的元素相当于都成为了 (+) 左侧的入参,可以将其想象为

haskell

[\x -> 1 + x, \x -> 2 + x, \x -> 3 + x]
-- or, more simpler
[(1+), (2+), (3+)]

那么我们应该可以这么写代码(让 1 成为 (+) 右侧的入参)

haskell

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+)

haskell

ghci> let justF = fmap (+) $ Just 1
ghci> :t justF
justF :: Num a => Maybe (a -> a)

现在设想,我们有一个值 Just 2

haskell

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,我们可以尝试推导它的类型签名,不难推出,应该是下面这样

信息
一元函数(Unary Function),意思是只有 1 个入参的函数

haskell

magicOperator :: Maybe (a -> b) -> Maybe a -> Maybe b
技巧

虽然无法直接用 fmap,但是我们可以通过 Pattern Matching 将 Just 里面的函数提取出来再用 fmap

haskell

apply Nothing _ = Nothing
apply (Just f) Nothing = Nothing
apply (Just f) (Just v) = fmap f (Just v)

其他的 Functor 类型不一定支持你用 Pattern Matching 做提取 :(

如果将 Maybe 替换为一个抽象的 type constructor f,那么我们就可以得到

haskell

magicOperator :: f (a -> b) -> f a -> f b

恭喜你,你刚刚自己推导出了 Applicative Functor 的 <*> 操作符


前面我们只考虑 map 函数入参为 1 个的场景,现在来考虑 n 个入参 的场景,用函数 g 表示 map 函数,它的类型签名是

haskell

g :: t1 -> t2 -> t3 -> ... -> tn -> t

假设我们有一堆的 Functor xi,每一个 xi 的类型都是 f ti,因为 Haskell Function 的函数都是 Currying Functions,可以先从 fmap g x1 开始推导

haskell

-- 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,但很快会发现,类型对不上

haskell

fmap g x1 :: f (t2 -> t3 ->  ... -> tn -> t)
x2        :: f  t2

显然,这里应该用 Applicative Functor 的 <*>,正确的形式应该是

haskell

fmap g x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
-- or, more simpler
g <$> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
注意
所以结论是:我们需要 Applicative Functor 的 <*>,因为有的 Functor 里面是函数,有的 Functor 里面是值。<*> 可以将一个 Functor 里面的函数 apply 在另外一个 Functor 的值上,所以 <*> 起到了Function Application 的作用
警告
除了满足下面的定义之外,Applicative Functor 还应该满足 Haskell Applicative Laws

现在知道 Applicative Functor 的 <*> 是用来做 Function Application 的了,来看一下 Applicative 的定义

haskell

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 里面。在前面的例子中,我们有

haskell

fmap g x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn
-- or, more simpler
g <$> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn

如果用 pure 的话,也可以这么写

haskell

pure g <*> x1 <*> x2 <*> x3 <*> x4 <*> ... <*> xn

<*> 在前面我们自己推导过,含义已经清晰了,这里不再赘述

让我们来看 Applicative Functor 的一个例子,Maybe 类型其实就是一个 Applicative Functor

haskell

instance Applicative Maybe where
    pure                  = Just
    (Just f) <*> (Just x) = Just (f x)
    _        <*> _        = Nothing

现在让我们来对比一下 3 个都表达了 Function Application 的操作符

haskell

($)   ::                    (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