Typeclassopedia中Applicative的构成规律

The composition law of Applicative in the Typeclassopedia

我正在阅读 Typeclassopedia,但我在 Applicatives 部分遇到了问题。我想我(有点)明白了,但我想看看我的理解是否正确。

在组合法出现之前,应用法则都是有意义的。我只是无法解析它的右侧:

u <*> (v <*> w) = pure (.) <*> u <*> v <*> w

因此,我启动了 GHCI 并 运行 进行了一些实验。

Prelude> pure (.) <*> Just (+1) <*> Just (+2) <*> Just 34
Just 37

所以这验证了规律,但我还是没看懂。我尝试了一些变体,看看是否能获得一些见解:

Prelude> pure (.) <*> Just (+1) <*> Just (+2) <*> Just (+3) <*> Just 34

<interactive>:26:1: error:
    * Non type-variable argument in the constraint: Num (b -> b)
      (Use FlexibleContexts to permit this)
    * When checking the inferred type
        it :: forall b. (Num (b -> b), Num b) => Maybe b

那么,组合适用于两个功能而不适用于三个功能?我不明白为什么。

然后我验证了 <*> 是否像我想的那样对一个简单的表达式有效:

Prelude> Just (+1) <*> Just 1
Just 2

但是,以下方法不起作用:

Prelude>  Just (+1) <*> Just (+2) <*> Just 34

<interactive>:15:2: error:
    * Non type-variable argument in the constraint: Num (b -> b)
      (Use FlexibleContexts to permit this)
    * When checking the inferred type
        it :: forall b. (Num (b -> b), Num b) => Maybe b

因此,应用与组合不同。我原以为 Just (+2) <*> Just 34 会导致 Just 36Just (+1) <*> Just 36 会导致 Just 37。组合运算符 (.) 是必需的,但它仅适用于两个函数。我只是不明白为什么需要它或它是如何工作的。

而且,为了向水中投入更多的污垢,我尝试了以下方法:

Prelude> Just (,) <*> Just 2

失败,因为结果没有 Show 的实例。所以我尝试了:

Prelude> :t Just (,) <*> Just 2
Just (,) <*> Just 2 :: Num a => Maybe (b -> (a, b))

这让我有点见识。我需要传递第二个值才能返回 tuple。我试过了:

Prelude> Just (,) <*> Just 2 Just 34

但是失败了,错误消息确实没有帮助我找出错误所在。因此,查看上面代码的类型后,我意识到它与输入 Just (2, )(或类似的东西)一样。所以,我尝试了:

Prelude> Just (,) <*> Just (2) <*> Just 34
Just (2,34)

我真的很惊讶这能奏效。但在那惊喜中萌生了理解的萌芽(我认为)。

我回到Typeclassopedia中(<*>)的定义,发现它被定义为left associative。在此之前我什至没有考虑过关联性。所以上面的表达式实际上会这样计算:

Prelude> Just (,) <*> Just (2) <*> Just 34
-- Apply the 2 as the first parameter of (,) leaving a function
-- that takes the second parameter
Just (2,) <*> Just 34
-- Now apply the 34 as the parameter to the resulting function
Just (2,34)

如果这是正确的,那么有效的组合法示例的评估如下:

Prelude> pure (.) <*> Just (+1) <*> Just (+2) <*> Just 34
pure (+1 . ) <*> Just (+2) <*> Just 34
pure (+1 . +2) <*> Just 34
Just 37

这也解释了为什么三个函数不能这样组合:

Prelude> pure (.) <*> Just (+1) <*> Just (+2) <*> Just (+3) <*> Just 34
pure (+1 . ) <*> Just (+2) <*> Just (+3) <*> Just 34
pure (+1 . +2) <*> Just (+3) <*> Just 34

而且,在这一点上,我们有一个组合函数,它需要一个 Num 类型类的实例,但相反,它传递给它一个函数,但失败了。

以类似的方式:

Prelude>  Just (+1) <*> Just (+2) <*> Just 34

失败,因为 <*> 不是组合,并且正在尝试将一个函数 (+2) 应用于另一个函数, (+1) 并将 运行 应用于类型约束。

所以,我的直觉在这里是正确的吗?我终于明白了吗?或者我的(有点)受过教育的猜测仍然是错误的?

这只是一个没有正确应用替换规则的情况,忘记了括号在这些替换中的重要性(尽管其中一些可以稍后删除)。规则应表示为

u <*> (v <*> w) == (pure (.) <*> u <*> v) <*> w

然后

Just (+1) <*> (Just (+2) <*> Just 34)
    -- u = Just (+1)
    -- v = Just (+2)
    -- w = Just 34
 => (pure (.) <*> u <*> v) <*> w
 => (pure (.) <*> Just (+1) <*> Just (+2)) <*> Just 34

要在此添加另一层,您必须使用另一个 pure (.):

Just (+3) <*> (Just (+1) <*> (Just (+2) <*> Just 34))
    -- u = Just (+3)
    -- v = Just (+1)
    -- w = Just (+2) <*> Just 34
 => (pure (.) <*> u <*> v) <*> w
 => pure (.) <*> Just (+3) <*> Just (+1) <*> (Just (+2) <*> Just 34)

如果您想从第一阶段开始使用 pure 表格,您需要

Just (+3) <*> ((pure (.) <*> Just (+1) <*> Just (+2)) <*> Just 34)
    -- u = Just (+3)
    -- v = pure (.) <*> Just (+1) <*> Just (+2)
    -- w = Just 34
 => (pure (.) <*> u <*> v) <*> w
 => pure (.) <*> Just (+3) <*> (pure (.) <*> Just (+1) <*> Just (+2)) <*> Just 34

恐怕没有人们希望的那么漂亮。

正如 chi 指出的那样,您的直觉现在是正确的。引用 :

Since (.) only composes two functions, it can't work on three.

这种形式的合成法则(即 (<*>) 的形式)相当令人头疼,特别是如果你想使用它来计算出更多的结果(举个小例子,参见后面的部分) )。如果我们对其应用 (pure f <*> x = f <$> x),它会变得更容易扫描:

u <*> (v <*> w) = ((.) <$> u <*> v) <*> w

现在所讨论的关联性更加明显:通过函子应用 v 然后 uw 与组合 uv 通过函子,然后应用到 w。但是,为了进行重大改进,我们必须切换到所谓的 static arrow 表示:

idA :: Applicative f => f (a -> a)
idA = pure id

(.*) :: Applicative f => f (b -> c) -> f (a -> b) -> f (a -> c)
u .* v = (.) <$> u <*> v -- This looks familiar...

-- Conversely:
-- pure x = ($ x) <$> idA -- Alternatively: const x <$> idA
-- u <*> v = ($ ()) <$> (u .* (const <$> v))

根据(.*),合成法则变为...

u .* (v .* w) = (u .* v) .* w

...这显然是一个结合律。