我在Haskell邮件列表中遇到了这个discussion。从讨论中可以看出,添加liftA2作为Applicative方法可能会对性能产生影响。您能否提供具体示例,说明为什么有必要在应用方法中添加liftA2?

最佳答案

这封电子邮件写于2017年。那时 Applicative typeclass看起来像:

class Functor f => Applicative f where
    -- | Lift a value.
    pure :: a -> f a

    -- | Sequential application.
    (<*>) :: f (a -> b) -> f a -> f b

    -- | Sequence actions, discarding the value of the first argument.
    (*>) :: f a -> f b -> f b
    a1 *> a2 = (id <$ a1) <*> a2
    -- This is essentially the same as liftA2 (const id), but if the
    -- Functor instance has an optimized (<$), we want to use that instead.

    -- | Sequence actions, discarding the value of the second argument.
    (<*) :: f a -> f b -> f a
    (<*) = liftA2 const

因此,不将liftA2作为Applicative类型类的一部分。这是defined as [src]:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = fmap f a <*> b

因此无法在typeclass中进行特殊实现。这意味着有时liftA2可以更有效地实现,但无法对其进行定义。

例如,Maybe函子和Applicative被实现为:
instance Functor Maybe where
    fmap f (Just x) = Just (f x)
    fmap _ Nothing = Nothing

instance Applicative Maybe where
    pure = Just
    Just f <*> Just x = Just (f x)
    _ <*> _ = Nothing

因此,这意味着实现liftA2Maybe类似于:
liftA2Maybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
liftA2Maybe f x y = apMaybe (fmapMaybe f x) y
    where fmapMaybe f (Just x) = Just (f x)
          fmapMaybe _ Nothing = Nothing
          apMaybe (Just f) (Just x) = Just (f x)
          apMaybe _ _ = Nothing

但这不是最佳的。这意味着fmapMaybe将检查参数是否为Just xNothing,然后返回Just (f x)Nothing。但是不管怎样,apMaybe会再次检查它,而我们已经事先知道了。我们可以通过以下方式实现更有效的实施:
liftA2Maybe :: (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
liftA2Maybe f (Just x) (Just y) = Just (f x y)
liftA2Maybe _ _ _ = Nothing

在这里,我们避免了数据构造函数的额外拆包。但是,这并不是问题。对于某些数据结构(例如 ZipList ),开销将更为严重,因为对象数量更多。

2017年6月23日,发布了一个新的base库,其中liftA2函数作为方法添加到 Applicative type class中。

09-15 18:14