Haskell中的x <*> y <$> z

5

我正在尝试理解一些Haskell源代码,并且遇到了这种结构:

x <*> y <$> z

e.g.

(+) <*> (+1) <$> a

有人能向我解释这个结构吗?我知道它可以翻译成fmap a (+ a + 1),但我无法建立联系。
2个回答

4

让我们开始吧:

x <*> y <$> z

加上括号后,变成:

(x <*> y) <$> z

鉴于 (<$>) :: Functor f => (a -> b) -> f a -> f b,我们有:

x <*> y :: a -> b
z :: Functor f => f a

鉴于 (<*>) :: Applicative g => g (c -> d) -> g c -> g d,我们有:

x :: Applicative g => g (c -> d)
y :: Applicative g => g c
x <*> y :: Applicative g => g d

结合前面的几个结果,我们得到:
g d ~ a -> b
g ~ (->) a
d ~ b

x :: a -> c -> b
y :: a -> c
x <*> y :: a -> b

因此:
(\x y z -> x <*> y <$> z) :: Functor f => (a -> c -> b) -> (a -> c) -> f a -> f b

现在我们知道函数实例中的(<*>)正在使用,因此我们也可以替换其定义:
x <*> y <$> z
(\r -> x r (y r)) <$> z

在你的例子中,x = (+)y = (+1),和 z = a,因此我们得到...
(\r -> r + (r + 1)) <$> a

...它将a中的每个值加上自身再加一:

GHCi> (+) <*> (+1) <$> [0..3]
[1,3,5,7]
GHCi> ((+) <*> (+1) <$> (*5)) 2
21

1
不用说,但实际上不要这么做。这是非常糟糕的风格,因为它看起来像一件事情,但实际上是另一回事。准则是永远不要隐式使用Reader实例。 - Lazersmoke
@Lazersmoke 同意。使用 <*> 作为 S 组合子,利用它的 (->) a 实例,可能会相当令人困惑。 - chi
2
@Lazersmoke,我认为Applicative ((->) r)有一些合法的用途(特别是,如果在上下文中fg是函数这一事实非常明显或不相关,则op<$>f<*>g对我来说并不麻烦)。无论如何,我们正在讨论的例子不是其中之一。(我同意chi的观点,使用独立的(<*>)作为S组合器往往会令人困惑。) - duplode

3
在 `x <*> y <$> z` 中,即 `fmap (x<*>y) z`,你将函数 x<*>y 应用于函子值 z。实际上,`<*>` 对 fmapping 一无所知 - 这两个运算符在完全不同的函子上工作!这是需要认识到的第一件重要的事情。
接下来,如果 `x<*>y` 的结果是一个函数,则 `<*>` 的 `Applicative` 实例实际上是函数函子。我希望人们不要那么频繁地使用它,因为它实际上是更令人困惑的实例之一,通常不是最好的选择。
具体而言,`f<*>g` 只是一种聪明的方式,将函数 `f` 和 `g` 组合起来,同时直接将初始输入传递给 `f`。它的工作原理如下:
(<*>) :: (f ~ (x->))
     => f (a -> b) -> f a -> f b

即,也就是。
(<*>) :: (x ->(a -> b)) -> (x -> a) -> (x -> b)
       ≡ (x -> a -> b)  -> (x -> a) ->  x -> b
(f <*> g) x = f x $ g x

就数据流而言,它是这个操作:
────┬─────▶ f ──▶
    │       │
    └─▶ g ──┘

我更愿意用箭头组合器来表达这个意思:(链接)
     ┌───id──┐
────&&&     uncurry f ──▶
     └─▶ g ──┘

"所以f<*>g ≡ id &&& g >>> uncurry f。当然,这并不像显式的lambda版本\x -> f x $ g x那样紧凑,实际上更冗长,说实话可能是最好的。然而,箭头版本是三个版本中最通用的版本,可以说最好地表达了正在进行的操作。它之所以如此冗长,主要是因为在这里柯里化不能起作用;我们可以定义一个运算符。"
(≻>>) :: (x->(a,b)) -> (a->b->c) -> x -> c
g≻>>h = uncurry h . g

然后

         x <*> y <$> z
≡ fmap (id &&& y ≻>> x) z
≡ fmap (\ξ -> x ξ $ y ξ) z

例如,我们有:
   (+) <*> (+1) <$> a
≡ fmap (id &&& (+1) ≻>> (+)) z
≡ fmap (\x -> 1 + x+1) z
≡ fmap (+2) z

我最初误读了你的问题。模式<$> <*>比你的<*> <$>更常见,下面是解释...或许对其他人有用。 f <$> y <*> z也可以写成liftA2 f y z,而liftA2在我看来比等效的<*>更容易理解。
liftA2 :: (a -> b -> c) -> f a -> f b -> f c

它的作用是,它接受一个值的组合函数,并从中产生一个容器的组合函数。它有点类似于 zipWith,不同之处在于对于列表实例,它不仅将 a 列表中的每个元素与 b 列表中的 相应元素 组合,而且将 a 列表中的每个元素 与所有元素b 列表中组合,并连接结果。
Prelude> Control.Applicative.liftA2 (+) [0,10..30] [0..3]
[0,1,2,3,10,11,12,13,20,21,22,23,30,31,32,33]

4
请注意,原帖中的表达式不是x <$> y <*> z(它等价于liftA2 x y z),而是x <*> y <$> z,这更加令人困惑。 - duplode
1
尽管答案是错误的,但我仍然觉得它非常有帮助。如果有一个编辑承认了它,我仍然会点赞。 - hgiesel
你可能会高兴地听到,在即将发布的基础库中,liftA2 将成为一个 Applicative 方法,并且 Traversable 派生将在适当的情况下使用它。不幸的是,ApplicativeDo 的解糖是另一个非常棘手的问题,所以我还没有尝试将其整合进去。 - dfeuer

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