Haskell 为什么 Applicatives 需要在同一个 Context 中获取态射和数据?

In Haskell why Applicatives need to take morphisms and data in same Context?

我是 Haskell 的新手。这可能是个愚蠢的问题。

因为 Applicative 类型类具有 apply 函数,该函数在同一上下文中获取函数和数据。为什么它不能与众不同并且更通用。

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

为什么我们不能写这样的东西

class Functor f => Applicative f where
    (<*>) :: Functor g => g (a -> b) -> f a -> g (f b)
    (<*>) gab fa = fmap (\g -> fmap g fa) gab

    (<<*>>) :: Functor g => (g (f a) -> f a) -> g (a -> b) -> f a -> f b
    (<<*>>) peelOuter gab fa = peelOuter $ gab <*> fa

    (>>*<<) :: Functor g => (g (f a) -> g a) -> g (a -> b) -> f a -> g b
    (>>*<<) cleanInner gab fa = cleanInner $ gab <*> fa

可以如下使用

-- Extract List from maybe
elfm :: Maybe [a] -> [a]
elfm Nothing = []
elfm (Just xs) = xs

-- Fuse List elements in Maybe []
flem :: Monoid a => Maybe [a] -> Maybe a
flem Nothing = mempty
flem (Just xs) = Just $ foldl (<>) mempty xs

Just (*2) <*> [1,2,3,4]
-- Just [2,4,6,8]
(<<*>>) elfm (Just (*2)) [1,2,3,4]
-- [2,4,6,8]
(>>*<<) flem (Just (++ "Haskell")) ["Hello, "]
-- Just "Hello, Haskell"

而且我读到 Applicatives 的全部意义在于 Functors 提升多参数函数的缺点。这样对吗?

而且我觉得功能应用不尽如人意

add :: Num a => a -> a -> a
add a b = a + b

-- I want to apply [1,2,3] as First arguments and [4,5,6] as 2nd arguments.
-- Like add 1 4, add 2 4, add 3 6
-- But it is give all possibilities of combinations like a tree

--                          <*>
--      (+1)                (+2)            (+3)
-- (1+4)(1+5)(1+6)  (2+4)(2+5)(2+6)  (3+4)(3+5)(3+6)

并且还将它们与批处理进行了比较,但没有给出非常真实的例子。请为此提供示例。

Applicative 的每个实例都必须有自己的 <*> 实现。这就是为什么我们首先输入 classes 的原因。您的代码具有 class 本身定义的所有方法,没有留下任何实例。这意味着根本没有太多类型 class 。只有一堆通用函数。所有的肉都委托给('<<*>>)(>>*<<)的参数peelOutercleanInner。让我们更仔细地看看它们。它们或多或少是对称的,所以 (<<*>>) 应该足够了。

(<<*>>) :: Functor g => (g (f a) -> f a) -> g (a -> b) -> f a -> f b
(<<*>>) peelOuter gab fa = peelOuter $ gab <*> fa

其实peelOuter本来应该是class类型的方法,但是问题不止一个。

第一个问题涉及到两个仿函数,peelOuter每个仿函数需要单独实现。也就是说,我们这里有一个双参数类型 class ApplicativePair,我们需要为每一对创建一个单独的实例。

第二个问题是peelOuter不能对每一对真正的Applicative仿函数都实现。不能从 Maybe (Id a) 中提取 Id a,或者从 IO [a] 中提取 [a],或者...

更糟糕的是,当 fg 是同一个函子时,它是否总是可实现的还不清楚。显然,当 f 是一个 monad 时,它就只是一个 join。然而,并非所有的应用程序都是 monad,而 join 恰恰是应用程序缺乏成为 monad 的地方。所以 peelOuter,即使这样的类型是可实现的,也会违反一些 monad 法则。那是一件坏事?不一定,如果它仍然遵循适用的法律。但是,您没有提供任何法律,只提供了一堆功能。

任何两个仿函数都是 FunctorApplicative 仿函数。这用 newtype Compose 编码,参见 Data.Functor.Compose

所以,你的例子可以通过newtype Compose来解决。

-- Just (*2) <*> [1,2,3,4]
getCompose $ pure (*2) <*> Compose (Just [1,2,3,4])
-- or
getCompose $ (*2) <$> Compose (Just [1,2,3,4])
-- Just [2,4,6,8]

-- (<<*>>) elfm (Just (*2)) [1,2,3,4]
elfm . getCompose $ pure (*2) <*> Compose (Just [1,2,3,4])
-- or with toList (method of Foldable)
toList $ pure (*2) <*> Compose (Just [1,2,3,4])
-- or
toList $ (*2) <$> Compose (Just [1,2,3,4])
-- [2,4,6,8]

-- (>>*<<) flem (Just (++ "Haskell")) ["Hello, "]
flem . getCompose $ pure (++ "Haskell") <*> Compose (Just ["Hello, "])
-- or with toList and listToMaybe
listToMaybe . toList $ pure (++ "Haskell") <*> Compose (Just ["Hello, "])
-- or
listToMaybe . toList $ (++ "Haskell") <$> Compose (Just ["Hello, "])
-- or with head :: Foldable f => f a -> Maybe a
head $ (++ "Haskell") <$> Compose (Just ["Hello, "])
-- Just "Hello, Haskell"

关于最后一个问题。你在@Robin Zigmond 的评论中得到了答案。它写了关于 newtype ZipList。使用 ZipList 你可以:

getZipList $ (+) <$> ZipList [1,2,3] <*> ZipList [4,5,6]
-- [5,7,9]

因此,Haskell 中 newtype 的目的之一是能够为某种类型编写不同的实例。