我一直试图抓住箭头,因为它们是大多数FRP实施的基础。我想我理解了它的基本思想 - 它们与monad有关但在每个绑定操作符中存储静态信息,因此您可以浏览一系列箭头并查看静态信息,而无需评估整个箭头。
但是我在开始讨论第一,第二和交换的时候迷路了。 2元组与箭头有什么关系?教程提供了元组的东西,好像它是明显的下一步,但我并没有真正看到连接。
就此而言,箭头语法直观地意味着什么?
答案 0 :(得分:46)
请查看http://www.cs.yale.edu/homes/hudak/CS429F04/AFPLectureNotes.pdf,其中解释了Arrows如何在FRP中工作。
2元组用于定义箭头,因为它需要表示带有2个参数的箭头化函数。
在FRP中,常量和变量通常表示为忽略其“输入”的箭头,例如
twelve, eleven :: Arrow f => f p Int
twelve = arr (const 12)
eleven = arr (const 11)
然后将函数应用程序转换为合成(>>>
):
# (6-) 12
arr (6-) <<< twelve
现在我们如何将2参数函数转换为箭头?例如
(+) :: Num a => a -> a -> a
由于currying,我们可能将此视为返回函数的函数。所以
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
。 我们注意到,因为只允许合成,因此cur中的currying失败。因此我们必须首先 uncurry 该函数
uncurry :: (a -> b -> c) -> ((a, b) -> c)
uncurry (+) :: Num a => (a, a) -> a
然后我们有箭头
(arr.uncurry) (+) :: (Num a, Arrow f) => f (a, a) a
由于这个原因,出现了2元组。然后需要像&&&
这样的束函数来处理这些2元组。
(&&&) :: 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个参数的函数,我们不需要像&&&&
那样的3元组?因为可以使用((a,b),c)
代替。)
编辑:从John Hughes的原始论文 Generalising Monads到Arrows ,它说明了原因
4.1箭头和对
然而,即使在monad的情况下,操作符
return
和>>=
也是我们开始编写有用代码所需要的,对于箭头类似的运算符arr
和>>>
还不够。甚至我们之前看到的简单的monadic加法函数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 = ...
我们必须按顺序组合
f
和g
。唯一可用的排序运算符是>>>
,但f
和g
没有合适的类型。实际上,add
函数需要在b
的计算中保存f
类型的输入,以便能够向{提供相同的输入{1}}。同样,必须在g
的计算中保存f
的结果,以便最终可以将两个结果相加并返回。到目前为止引入的箭头组合器使我们无法在另一个计算中保存值,因此我们别无选择,只能引入另一个组合器。