应用函子(Applicative)函子的子类型,虽然可以使用应用函子来定义函子。应用函子是介于 FunctorMonad 之间的抽象。函子的 fmap 提供的功能是将 a -> b 的函数 f 升格为 f a -> f b ,而应用函子的功能是将包含在盒子中的函数 f (a -> b) 应用到同样装在盒子中的参数之上 f a

应用函子的 Haskell 定义

class Functor f => Applicative f where
pure :: a -> f a
 
infixl 4 <*>, *>, <*
 
(<*>) :: f (a -> b) -> f a -> f b
 
(*>) :: f a -> f b -> f b
a1 *> a2 = (id <$ a1) <*> a2
 
(<*) :: f a -> f b -> f a
(<*) = liftA2 const
  • pure 函数表示接受一个参数,并把它放入到一个不影响计算语义的盒子中去,可以理解为为参数添加最小上文 (minimum context) ,虽然看起来添加最小上下文的解释有点模糊,但因为本身 Applicative 需要满足四个条件,一般的实现中几乎都只有一种实现可以满足所有限制。

  • (<*>) 是 Applicative 的核心函数,它接受一个已经升格的函数,将函数应用到同样升格的参数上,并且返回升格之后的计算结果。

  • 值得注意的是,函数 (*>), (<*) 看起来像是将右边或者左边的参数直接扔掉了,但实际这样的理解是不对的,实际上,函数将尖括号指向的盒子的内容重新打包到了另一个参数的盒子中,所以盒子的形状(上下文)由一个参数决定,内容由另一个参数决定。举个例子:

 Prelude> [1, 2] \*> [1, 2, 3]
 [1, 2, 3, 1, 2, 3]

应用函子应当满足的条件

  1. 单位律 (identity)
pure id <*> v = v
  1. 同态律 (homomorphism)
pure f <*> pure x = pure (f x)

在没有任何副作用的盒子 pure 的包装下,用盒子计算的结果应该和不用没有盒子的计算之后再放入盒子相同。

  1. 交换律 (interchange)
u <*> pure y = pure ($ y) <*> u

将一个有状态的函数应用到一个无状态的参数时,不管是先应用函数,还是先计算参数再应用函数,最终的结果应该相同。

  1. 结合律 (composition)
u <*> (v <*> w) = pure (.) <*> u <*> v <*> w

使用应用函子来定义函子

fmap g x = pure g <*> x
g <$> x = pure g <*> x

从定义可以看出来函子的 fmap 其实做了两步事情,第一步将函数 g 放入一个没有状态的盒子中,第二步将得到的带盒子的函数应用到带盒子的参数之上。

应用函子的实例

基本上标准库中所有的函子(Functor) 都是应用函子,以 Maybe 函子为例我们来实现它的应用函子。

instance Applicative Maybe where
pure :: a -> Maybe a
pure = Just
 
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
(Just f) <*> (Just x) = Just $ f x
_ <*> _ = Nothing

对于类型 [] 我们有两种方式来实现其应用函子,取决于我们如何理解列表中的元素。具体的实现参考这里

  1. 将列表看作是某种元素的集合
  2. 将列表看作一个包含了多个未确定的计算过程的环境(标准库中的实现采用的是这个理解)