什么是箭头,我如何使用它们?

我试图学习 的含义,但我不明白他们。

我用了维基教程。我认为 Wikibook 的问题主要在于它似乎是为那些已经理解这个主题的人而写的。

谁能解释一下什么是箭,我怎么用它们?

14885 次浏览

这里有 John Hughes 在 AFP (高级函数式编程)研讨会上的演讲笔记。注意,它们是在 Base 库中的 Arrow 类被更改之前编写的:

Http://www.cse.chalmers.se/~rjmh/afp-arrows.pdf

回顾一下 Stack Overflow 的历史,我将假设您熟悉其他一些标准类型类,特别是 FunctorMonoid,并从这些类型开始做一个简短的类比。

Functor上的单个操作是 fmap,它作为列表上 map的通用版本。这基本上就是类型类的全部用途; 它定义了“您可以映射的内容”。因此,在某种意义上,Functor代表了列表特定方面的泛化。

Monoid的操作是空列表和 (++)的通用版本,它定义了“可以联合组合的事物,以及特定的标识值”。列表几乎是最简单的符合这一描述的东西,而 Monoid代表了列表这一方面的一般化。

In the same way as the above two, the operations on the Category type class are generalized versions of id and (.), and it defines "things connecting two types in a particular direction, that can be connected head-to-tail". So, this represents a generalization of that aspect of 功能. Notably not included in the generalization are currying or function application.

The Arrow type class builds off of Category, but the underlying concept is the same: Arrows are things that compose like functions and have an "identity arrow" defined for any type. The additional operations defined on the Arrow class itself just define a way to lift an arbitrary function to an Arrow and a way to combine two arrows "in parallel" as a single arrow between tuples.

首先要记住的是 构建 Arrow的表达式本质上是复杂的复合函数。像 (***)(>>>)这样的组合器是用来编写“无点”样式的,而 proc符号则提供了一种方法来为输入和输出分配临时名称,同时将事情连接起来。

这里需要注意的一点是,尽管 Arrow有时被描述为 Monad的“下一步”,但它们之间并没有什么特别有意义的关系。对于任何 Monad,您都可以使用 Kleisli 箭头,它只是具有类似于 a -> m b的类型的函数。Control.Monad中的 (<=<)操作符是这些操作符的箭头组合。另一方面,除非你也包括 ArrowApply类,否则 Arrow不会给你一个 Monad。所以没有直接联系。

The key difference here is that whereas Monads can be used to sequence computations and do things step-by-step, Arrows are in some sense "timeless" just like regular functions. They can include extra machinery and functionality that gets spliced in by (.), but it's more like building a pipeline, not accumulating actions.

The other related type classes add additional functionality to an arrow, such as being able to combine arrows with Either as well as (,).


我最喜欢的 Arrow的例子是 有状态流传感器有状态流传感器,它看起来像这样:

data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))

StreamTrans箭头将输入值转换为输出和“更新”版本本身; 请考虑这与有状态 Monad的不同之处。

Arrow及其相关类型类编写实例可能是理解它们如何工作的一个很好的练习!

我还写了一个 similar answer previously,你可能会觉得有帮助。

I don't know a tutorial, but I think it's easiest to understand arrows if you look at some concrete examples. The biggest problem I had learning how to use arrows was that none of the tutorials or examples actually show how to 使用 arrows, just how to compose them. So, with that in mind, here's my mini-tutorial. I'll examine two different arrows: functions and a user-defined arrow type MyArr.

-- type representing a computation
data MyArr b c = MyArr (b -> (c,MyArr b c))

1) An Arrow is a calculation from input of a specified type to output of a specified type. The arrow typeclass takes three type arguments: the arrow type, the input type, and the output type. Looking at the instance head for arrow instances we find:

instance Arrow (->) b c where
instance Arrow MyArr b c where

箭头((->)MyArr)是计算的抽象。

对于函数 b -> cb是输入,c是输出。
For a MyArr b c, b is the input and c is the output.

2)要实际运行一个箭头计算,你需要使用一个特定于你的箭头类型的函数。对于函数,只需将函数应用于参数。对于其他箭头,需要一个单独的函数(就像单子的 runIdentityrunState等)。

-- run a function arrow
runF :: (b -> c) -> b -> c
runF = id


-- run a MyArr arrow, discarding the remaining computation
runMyArr :: MyArr b c -> b -> c
runMyArr (MyArr step) = fst . step

3)箭头经常用于处理输入列表。对于函数,这些可以并行执行,但对于某些箭头,在任何给定步骤中的输出取决于以前的输入(例如,保持输入的运行总量)。

-- run a function arrow over multiple inputs
runFList :: (b -> c) -> [b] -> [c]
runFList f = map f


-- run a MyArr over multiple inputs.
-- Each step of the computation gives the next step to use
runMyArrList :: MyArr b c -> [b] -> [c]
runMyArrList _ [] = []
runMyArrList (MyArr step) (b:bs) = let (this, step') = step b
in this : runMyArrList step' bs

这就是箭头有用的原因之一。它们提供了一个计算模型,该模型可以隐式地利用状态,而无需向程序员公开该状态。程序员可以使用箭头计算并结合它们来创建复杂的系统。

Here's a MyArr that keeps count of the number of inputs it has received:

-- count the number of inputs received:
count :: MyArr b Int
count = count' 0
where
count' n = MyArr (\_ -> (n+1, count' (n+1)))

现在函数 runMyArrList count将接受一个列表长度 n 作为输入,并返回一个从1到 n 的 Ints 列表。

注意,我们还没有使用任何“ Arrow”函数,即 Arrow 类方法或用它们编写的函数。

4)上面的大部分代码是特定于每个 Arrow 实例的[1]。Control.Arrow(和 Control.Category)中的所有内容都是关于组合箭头来生成新的箭头。如果我们假设类别是 Arrow 的一部分,而不是一个单独的类:

-- combine two arrows in sequence
>>> :: Arrow a => a b c -> a c d -> a b d


-- the function arrow instance
-- >>> :: (b -> c) -> (c -> d) -> (b -> d)
-- this is just flip (.)


-- MyArr instance
-- >>> :: MyArr b c -> MyArr c d -> MyArr b d

>>>函数接受两个箭头,并将第一个箭头的输出作为第二个箭头的输入。

Here's another operator, commonly called "fanout":

-- &&& applies two arrows to a single input in parallel
&&& :: Arrow a => a b c -> a b c' -> a b (c,c')


-- function instance type
-- &&& :: (b -> c) -> (b -> c') -> (b -> (c,c'))


-- MyArr instance type
-- &&& :: MyArr b c -> MyArr b c' -> MyArr b (c,c')


-- first and second omitted for brevity, see the accepted answer from KennyTM's link
-- for further details.

由于 Control.Arrow提供了一种组合计算的方法,这里有一个例子:

-- function that, given an input n, returns "n+1" and "n*2"
calc1 :: Int -> (Int,Int)
calc1 = (+1) &&& (*2)

我经常发现像 calc1这样的函数在复杂的折叠中很有用,或者像操作指针这样的函数。

Monad类型类为我们提供了一种使用 >>=函数将一元计算合并为一个新的一元计算的方法。类似地,Arrow类为我们提供了使用几个基本函数(firstarr***,以及 Control 的 >>>id)将箭头化计算组合成一个新的箭头化计算的方法。类别)。与 Monads 类似的问题还有“箭是干什么的?”不能一概而论。这取决于箭头。

不幸的是,我不知道很多野外箭头实例的例子。函数和 FRP 似乎是最常见的应用。HXT 是我想到的唯一另一个重要用法。

[1]除了 count。可以编写一个 count 函数,对 ArrowLoop的任何实例执行相同的操作。

我想补充的是,哈斯克尔的箭头远比看上去要简单 它们仅仅是函数的抽象。

为了了解这是如何实际有用的,请考虑您有一些 functions you want to compose, where some of them are pure and some are 例如,f :: a -> bg :: b -> m1 ch :: c -> m2 d

了解所涉及的每种类型,我可以手工构建一个作文,但 该组合物的输出类型必须反映中间体 单子类型(在上面的例子中,m1 (m2 d))。如果我只是想治疗 就像它们只是 a -> bb -> c,和 c -> d一样? 就是说, 我想抽象出单子的存在和理由,只有关于 underlying types. I can use arrows to do exactly this.

中的函数的 IO 的存在 IO monad,这样我就可以用纯函数 < em > 组合它们,而不需要 编写需要知道 IO 涉及到 的代码 IOArrow 包装 IO 函数:

data IOArrow a b = IOArrow { runIOArrow :: a -> IO b }


instance Category IOArrow where
id = IOArrow return
IOArrow f . IOArrow g = IOArrow $ f <=< g


instance Arrow IOArrow where
arr f = IOArrow $ return . f
first (IOArrow f) = IOArrow $ \(a, c) -> do
x <- f a
return (x, c)

然后我创建一些简单的函数:

foo :: Int -> String
foo = show


bar :: String -> IO Int
bar = return . read

使用它们:

main :: IO ()
main = do
let f = arr (++ "!") . arr foo . IOArrow bar . arr id
result <- runIOArrow f "123"
putStrLn result

这里我调用 IOArrow 和 runIOArrow,但是如果我传递这些箭头 在一个多态函数库中,他们只需要接受 类型为“ Arrow a = > a b c”的参数 只有单子的创造者和最终用户才能意识到单子的存在 Arrow 需要知道。

将 IOArrow 推广到任何 Monad 中的函数,称为“ Kleisli” 而且已经有一个内置的箭头可以做到这一点:

main :: IO ()
main = do
let g = arr (++ "!") . arr foo . Kleisli bar . arr id
result <- runKleisli g "123"
putStrLn result

当然,您也可以使用箭头组合运算符和 proc 语法来 让我们更清楚地了解一下箭头的含义:

arrowUser :: Arrow a => a String String -> a String String
arrowUser f = proc x -> do
y <- f -< x
returnA -< y


main :: IO ()
main = do
let h =     arr (++ "!")
<<< arr foo
<<< Kleisli bar
<<< arr id
result <- runKleisli (arrowUser h) "123"
putStrLn result

在这里应该清楚的是,虽然 main知道 IO 单子参与, arrowUser没有。没有办法“隐藏”对 arrowUser的 IO 如果没有箭头,就不能使用 unsafePerformIO 中间一元值返回到纯值(因此失去了上下文) forever). For example:

arrowUser' :: (String -> String) -> String -> String
arrowUser' f x = f x


main' :: IO ()
main' = do
let h      = (++ "!") . foo . unsafePerformIO . bar . id
result = arrowUser' h "123"
putStrLn result

试着在不使用 unsafePerformIOarrowUser'的情况下编写它 处理任何 Monad 类型的参数。

当我开始探索 Arrow 组合(本质上是 Monads)时,我的方法是打破函数式语法和组合,它是最常见的关联,并且开始使用更具声明性的方法来理解它的原理。考虑到这一点,我发现以下分解更为直观:

function(x) {
func1result = func1(x)
if(func1result == null) {
return null
} else {
func2result = func2(func1result)
if(func2result == null) {
return null
} else {
func3(func2result)
}

因此,本质上,对于某个值 x,首先调用一个函数,我们假设它可能返回 null(function 1) ,另一个函数可能返回 null,或者可以互换地被分配给 null,最后,第三个函数也可能返回 null。现在给定值 x,将 x 传递给 function 3,只有在这样的情况下,如果它不返回 null,将这个值传递给 function 2,并且只有当这个值不为 null 时,才将这个值传递给 function 1。它更具有确定性,并且控制流允许您构造更复杂的异常处理。

在这里我们可以利用箭头组成: (func3 <=< func2 <=< func1) x