单子和应用的联系是什么?

15

我正在阅读关于 applicative 的内容,并尝试理解 haskellbook 上的内容。

在书中,作者提到:

所以,使用 Applicative,我们可以为结构体创建一个 Monoid 并对值应用函数!

Monoid 如何与 applicative 相关联?


很好的问题!你可能会发现这个链接很有启发性。(如果不行的话,我很乐意写一篇更详细的答案。) - Benjamin Hodgson
1
另外:https://dev59.com/rmYr5IYBdhLWcg3wfaWd - leftaroundabout
您可能会发现typiclassopedia很有价值 https://wiki.haskell.org/Typeclassopedia#Definition_6 - LudvigH
3
一个可应用的 m a 具有结构 m 和值 apure :: a -> m a 使我们能够构造一个最小的结构(类似于 m 的恒等元),而 (<*>) :: m (a -> b) -> m a -> m b 则使我们能够组合两个 m(类似于幺半群的操作),同时对内部值进行函数应用(一个值是 a -> b,另一个是 a,你得到一个 b)。 - Alec
1
请注意,这里使用的 Monoid 是更一般的数学意义上的。Haskell 不会将其视为“Monoid”。 - Alec
2个回答

14

注意:我还没有拥有这本书,而且如果我没记错的话,至少一个作者在SO上很活跃,并且应该能够回答这个问题。 也就是说,单子(或者更确切地说是半群)背后的思想是,在该单子1中,您有一种方法可以从两个对象创建另一个对象:

mappend :: Monoid m => m -> m -> m

那么Applicative怎么成为一个单子呢?正如您引用的话所说,它在结构上是一个单子。也就是说,我们从一个f something开始,继续使用f anotherthing,最后得到了一个f resulthing,你已经猜到了:

amappend :: f (a -> b) -> f a -> f b

在我们继续之前,短暂的,非常短暂的时间内,让我们忘记f具有* -> * 类型。我们最终得到了什么?

amappend :: f          -> f   -> f

这就是所谓的“单调结构”部分。这也是 Haskell 中 ApplicativeFunctor 的区别,因为在 Functor 中我们没有该属性:

fmap     ::   (a -> b) -> f a -> f b
--          ^
--       no f here

这也是我们尝试仅使用fmap或其他函数时会遇到麻烦的原因:经过一次fmap后,除非我们能够“在那个新结构中应用我们的新函数”,否则我们将被困住。这就带我们来到了你问题的第二部分:

所以,使用Applicative,我们为值进行函数应用!

函数应用是($)。如果我们看一下<*>,我们可以立即看到它们很相似:

($)   ::   (a -> b) ->   a ->   b
(<*>) :: f (a -> b) -> f a -> f b

如果我们忘记在(<*>)中的f,那么我们最终只会得到($)。 因此,在我们的结构上下文中,(<*>)只是函数应用:

increase  :: Int -> Int
increase x = x + 1

five :: Int
five = 5

increaseA :: Applicative f => f (Int -> Int)
increaseA = pure increase

fiveA :: Applicative f => f Int
fiveA = pure 5

normalIncrease      = increase   $  five
applicativeIncrease = increaseA <*> fiveA

我想作者所谓的“函数应用”就是这个意思。我们突然可以将那些隐藏在我们结构中的函数应用于其他值。由于单调性,我们仍然呆在这个结构中。

话虽如此,我个人从不称之为单调的,因为 <*> 不作用于相同类型的两个参数,并且一个适用程序缺少空元素。

1对于真正的半群/幺半群,该操作应该是结合的,但这并不重要


我已经尝试了另一种表达方式:class Functor f => Cappl f where unit :: Category c => f (c a a); comp :: Category c => f (c y z) -> f (c x y) -> f (c x z)。这等同于Applicative,也等同于看似不那么强大的class Functor f => Fappl f where funit :: f (a -> a); fcomp :: f (y -> z) -> f (x -> y) -> f (x -> z)。我不知道是否有任何有趣的东西。 - dfeuer
@dfeuer 好奇:如果没有使用 Profunctor c,你是如何从 Fappl 转换到 Cappl 的? - Benjamin Hodgson
@BenjaminHodgson,我实际上是从FapplApplicative再到(显然的)Cappl,最后到(琐碎的)Fappl。如果你在这个过程中遇到了困难,请告诉我。我可以给你一个提示,但可能太大了。 - dfeuer
@Zeta,你能否公布IRC的链接? - softshipper
@dfeuer 我明白了。那个提示足够大了。 - Benjamin Hodgson
1
@zero_coding:IRC?“IIRC”意思是“如果我记得正确的话”,比如“如果我没错的话,至少一个作者有StackOverflow账户”。 - Zeta

1
尽管这个问题早已得到了一个很好的答案,但我想再补充一点。

看一下以下类

class Functor f => Monoidal f where
  unit :: f ()
  (**) :: f a -> f b -> f (a, b)

在解释为什么我们需要一些关于Applicative的问题的Monoidal类之前,让我们先看一下它的法则,遵守这些法则可以给我们提供一个单子:
  • f a (x) 等同于 f ((), a) (unit ** x),这给了我们左单位元(** unit) :: f a -> f ((), a)fmap snd :: f ((), a) -> f a
  • f a (x) 也等同于 f (a, ()) (x ** unit),这给了我们右单位元(unit **) :: f a -> f (a, ())fmap fst :: f (a, ()) -> f a
  • f ((a, b), c) ((x ** y) ** z) 等同于 f (a, (b, c)) (x ** (y ** z)),这给了我们结合律fmap assoc :: f ((a, b), c) -> f (a, (b, c))fmap assoc' :: f (a, (b, c)) -> f ((a, b), c)
正如你所猜测的那样,我们可以用Monoidal的方法来书写Applicative,反之亦然。
unit   = pure ()
f ** g = (,) <$> f <*> g = liftA2 (,) f g

pure x  = const x <$> unit
f <*> g = uncurry id <$> (f ** g)
liftA2 f x y = uncurry f <$> (x ** y)

此外,我们可以证明 MonoidalApplicative 法则告诉我们同样的事情。我之前问过这个问题

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接