What are Applicative Functors?

上一篇文章我们讲了函子(functor)是什么,这一篇文章讲一个基于函子的概念——可应用函子(applicative functor)

动机

我们知道,函子可以描述从元素类型的映射到容器类型之间的映射之间的转换。最常见的应用场景就是 Maybe,下面是 MaybeFunctor 实现。请读者熟悉一下,因为后面会用到。

instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap f (Just a) = Just (f a)

假设我们有二元运算,叫做 op。我们不管它做了什么,只需要知道它是一个在整数上的二元运算就可以了。

op :: Float -> Float -> Float

因为 Haskell 中的函数都是高阶函数,所以我们 Haskell 程序员常用的一个技巧是用函数只用一半,后半部分留着,需要用的时候再用。

newop = op 0.0

但很多时候我们拿到的值的类型并非是 Float,很有可能是被某个容器类型(container type)包装起来的 Float,比如 Maybe Float。此时我们想把 op 变得在 Maybe Float 上也是可用的,该怎么做呢?如果直接重写一个 opForMaybe,代码大概是这样子的。

opForMaybe :: Maybe Float -> Maybe Float -> Maybe Float
opForMaybe Nothing  _        = Nothing
opForMaybe _        Nothing  = Nothing
opForMaybe (Just x) (Just y) = Just (op x y)

但别忘了我们是 Haskell 程序员,我们要从类型的角度思考问题!很自然地,我们会想到上面的 Functor,毕竟它的作用就是「描述从元素类型的映射到容器类型之间的映射之间的转换」。我们打开 GHCi 来试试。

Prelude> :t fmap op
fmap op :: Functor f => f Float -> f (Float -> Float)

诶?这和我们想的不太一样啊,我们想要的是一个 Maybe Float -> Maybe Float -> Maybe Float 的函数,用 fmap 操作后得到的反而是一个 Maybe Float -> Maybe (Float -> Float) 函数。这就说明 Functor 在此刻肯定不能满足我们的需求——把一个二元的操作映射到对容器类型的二元操作,我们要构造一个至少可以用于二元函数的 Functor

构造

上面的小节展示了一种 Functor 不能满足需求的情况,下面我们就来想一下如何解决这个问题。如果我们仿照 Functor 的定义,去构建一个可以用于二元函数的 Functor,写出来的东西会是下面这个样子。

class BinaryFunctor f where
  bfmap :: (a -> b -> c) -> f a -> f b -> f c

二元还好,如果是三元、四元、甚至十元函数,那我们岂不是要一个一个写?作为喜欢抽象的 Haskell 程序员,肯定不能就此屈服,我们要实现更加通用的东西。为了看出哪里可以改进,我们随便把一个三元函数套用到上面的 BinaryFunctor 中。

Prelude> :t op3
op3 :: Float -> Float -> Float -> Float
Prelude> :t bfmap op3
bfmap op3 :: BinaryFunctor f => f Float -> f Float -> f (Float -> Float)

观察类型我们可以发现,和上一节 GHCi 的结果一样,得到的函数签名的最后一个类型都是 f (Float -> Float),所以与其给 nn 元函数设计不同的 Functor,我们不如直接设计一个把 f (a -> b) 转换为 f a -> f b 的抽象。写出来大概就是这个样子的。

class Functor' f where
  fmap' :: f (a -> b) -> f a -> f b

我们手动给 Maybe 实现一下。

instance Functor' Maybe where
  fmap' Nothing  _        = Nothing
  fmap' _        Nothing  = Nothing
  fmap' (Just f) (Just x) = f x

这样就完美了!不过还有个问题:我们对第一个参数 Maybe a 做了过多的解构(destruct),讨论了每一种情况,这样有点重复造轮子,通过重用 Functor 中的 fmap,我们可以实现得更加简洁。

class Functor f => Functor' f where
  fmap' :: f (a -> b) -> f a -> f b

instance Functor' Maybe where
  fmap' Nothing  _ = Nothing
  fmap' (Just f) x = fmap f x

登场

我们定义的 Functor' 其实就是 Applicative,来看看它的定义。

class Functor f => Applicative f where
    {-# MINIMAL pure, ((<*>) | liftA2) #-}

    pure :: a -> f a

    (<*>) :: f (a -> b) -> f a -> f b
    (<*>) = liftA2 id

    liftA2 :: (a -> b -> c) -> f a -> f b -> f c
    liftA2 f x = (<*>) (fmap f x)

    (*>) :: f a -> f b -> f b
    a1 *> a2 = (id <$ a1) <*> a2

    (<*) :: f a -> f b -> f a
    (<*) = liftA2 const

Applicative 中居然有五个成员,我们分别解读一下。

  • <*> 其实就是我们上面定义的 fmap' 函数,它们的类型签名一模一样。
  • 我们发现了一个熟悉的成员 liftA2,它的函数签名和我们第一版的 Functor' 的一模一样!其实这二者之间是可以相互转换的,只不过我们之前没有发现。
  • 除此之外,还有一个 pure 函数,它的作用我们现在暂且不提,因为要在引入新的概念后才能明白。
  • *><*,可以理解为是 Functor 中的 <$ 操作符,忽略其中的一个参数,直接给出定值。