理解Haskell中的箭头

59
我一直试图理解箭头,因为它们是大多数FRP实现的基础。我认为我理解了基本思想-它们与单子相关,但在每个绑定操作中存储静态信息,因此您可以遍历箭头链并查看静态信息,而无需评估整个箭头。
但是当我们开始讨论第一、第二和交换时,我就迷失了。2元组与箭头有什么关系?教程将元组内容呈现为显而易见的下一步,但我并没有真正看到联系。
同样,箭头语法从直觉上意味着什么?

函数式编程通常只允许返回单个返回值,如果您想要返回多个值,则需要使用元组。如果您想要编写 avg lst = sum lst / length lst,由于函数式编程的限制,您需要三次写入“lst”。如果您想要摆脱“lst”,则可以编写 avg = (sum &&& length) >>> uncurry (/),其中 {(>>>) = flip (.); f &&& g = \a -> (f a, g a)},uncurry 副词使您的函数接受 (f a, g a)。 - aoeu256
1个回答

50
请查看http://www.cs.yale.edu/homes/hudak/CS429F04/AFPLectureNotes.pdf,其中解释了箭头在FRP中的工作原理。

使用2元组定义箭头是因为需要表示接受2个参数的箭头化函数。

在FRP中,常量和变量通常被表示为忽略其“输入”的箭头。

twelve, eleven :: Arrow f => f p Int
twelve = arr (const 12)
eleven = arr (const 11)

函数应用然后转换为组合(>>>):

# (6-) 12

arr (6-) <<< twelve

现在我们如何将一个有两个参数的函数转换为箭头函数呢?例如

(+) :: Num a => a -> a -> a

由于柯里化,我们可以将其视为返回函数的函数。因此

arr (+) :: (Arrow f, Num a) => f a (a -> a)

现在让我们把它应用到一个常量上

arr (+)             -- # f     a (a -> a)
  <<< twelve        -- # f b Int
                      :: f b     (Int -> Int)

+----------+      +-----+      +--------------+
| const 12 |----> | (+) |  ==  | const (+ 12) |
+----------+      +-----+      +--------------+

嘿,等等,它不起作用。结果仍然是返回函数的箭头,但我们希望得到类似于f Int Int的东西。我们注意到在Arrow中柯里化失败,因为只允许组合。因此,我们必须首先取消柯里化该函数。

uncurry :: (a -> b -> c) -> ((a, b) -> c)

uncurry (+) :: Num a => (a, a) -> a

然后我们有箭头

(arr.uncurry) (+) :: (Num a, Arrow f) => f (a, a) a

这是因为有2-tuple出现了,接下来需要使用像&&&这样的bunch函数来处理这些2-tuples。

(&&&) :: f a b -> f a d -> f a (b, d)

那么加法就能正确执行。

(arr.uncurry) (+)        -- # f   (a,    a) a
  <<<     twelve         -- # f b  Int
      &&& eleven         -- # f b      Int
                           :: f b           a

+--------+
|const 12|-----.
+--------+     |       +-----+      +----------+
              &&&====> | (+) |  ==  | const 23 |
+--------+     |       +-----+      +----------+
|const 11|-----'
+--------+

(现在,为什么我们不需要像 &&&& 这样的东西来处理有 3 个参数的函数?因为我们可以使用 ((a,b),c) 代替。)

   add :: Monad m => m Int -> m Int -> m Int
   add x y = x >>= \u -> (y >>= \v -> return (u + v))

现在还无法用箭头形式表达。如果明确指定输入的依赖关系,那么类似的定义应该采用以下形式

   add :: Arrow a => a b Int -> a b Int -> a b Int
   add f g = ...

我们必须按顺序组合fg。唯一可用的顺序运算符是>>>,但fg的类型不正确,不能组合在一起。实际上,add函数需要在计算f保存类型为b的输入,以便能够将相同的输入提供给g。同样,f的结果必须在计算g期间保存下来,以便最终将这两个结果相加并返回。迄今为止介绍的箭头组合器没有办法在另一个计算中保存值,因此我们别无选择,只能引入另一个组合器。


1
支持元组的原语真的是显而易见的答案吗?为什么不支持Applicative样式的原语呢? - yairchu
@yairchu,当你尝试使用这些基元并尝试用它们来实现 (>>>)first 时,问题就出现了。如果我没记错的话,first 是棘手的一个。 - luqui
1
@luqui:我不知道,使用我的实验性FRP框架(http://github.com/yairchu/peakachu),我没有看到实现“first”会有任何问题。我只是不明白为什么你一开始就想要它。 - yairchu

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