应用函子(Applicative) 是函子的子类型,虽然可以使用应用函子来定义函子。应用函子是介于 Functor 和 Monad 之间的抽象。函子的 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]
应用函子应当满足的条件
- 单位律 (identity)
pure id <*> v = v
- 同态律 (homomorphism)
pure f <*> pure x = pure (f x)
在没有任何副作用的盒子 pure
的包装下,用盒子计算的结果应该和不用没有盒子的计算之后再放入盒子相同。
- 交换律 (interchange)
u <*> pure y = pure ($ y) <*> u
将一个有状态的函数应用到一个无状态的参数时,不管是先应用函数,还是先计算参数再应用函数,最终的结果应该相同。
- 结合律 (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
对于类型 []
我们有两种方式来实现其应用函子,取决于我们如何理解列表中的元素。具体的实现参考这里。
- 将列表看作是某种元素的集合
- 将列表看作一个包含了多个未确定的计算过程的环境(标准库中的实现采用的是这个理解)