为什么将liftA2作为一种方法添加到Applicative



我在Haskell邮件列表上看到了这个讨论。从讨论中可以看出,添加liftA2作为应用程序的方法似乎会对性能产生影响。你能提供具体的例子说明为什么有必要在应用方法中添加liftA2吗?

这封电子邮件写于2017年。当时Applicative类型类看起来像:

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类型类的一部分。它被定义为[src]:

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = fmap f a <*> b

因此无法在类型类中进行特殊实现。这意味着有时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

因此,这意味着MaybeliftA2类似于来实现

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 x还是Nothing,然后返回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类型类的方法。

相关内容

最新更新