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