上一篇文章我们讲了函子是什么,这一篇文章讲一个基于函子的概念——可应用函子。
动机
我们知道,函子可以描述从元素类型的映射到容器类型之间的映射之间的转换。最常见的应用场景就是 Maybe,下面是 Maybe 的 Functor 实现。请读者熟悉一下,因为后面会用到。
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,很有可能是被某个容器类型包装起来的 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),所以与其给 元函数设计不同的 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 做了过多的解构,讨论了每一种情况,这样有点重复造轮子,通过重用 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 constApplicative 中居然有五个成员,我们分别解读一下。
<*>其实就是我们上面定义的fmap'函数,它们的类型签名一模一样。- 我们发现了一个熟悉的成员
liftA2,它的函数签名和我们第一版的Functor'的一模一样!其实这二者之间是可以相互转换的,只不过我们之前没有发现。 - 除此之外,还有一个
pure函数,它的作用我们现在暂且不提,因为要在引入新的概念后才能明白。 - 而
*>和<*,可以理解为是Functor中的<$操作符,忽略其中的一个参数,直接给出定值。