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

时间:2010-11-16 05:20:20

标签: haskell arrows

我试图了解箭头的含义,但我不理解它们。

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

有人可以解释箭头是什么以及如何使用它们吗?

5 个答案:

答案 0 :(得分:71)

我不知道教程,但我认为如果你看一些具体的例子,最容易理解箭头。我学习如何使用箭头的最大问题是,没有任何教程或示例实际显示如何使用箭头,以及如何组合它们。所以,考虑到这一点,这是我的迷你教程。我将检查两个不同的箭头:函数和用户定义的箭头类型MyArr

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

1)箭头是从指定类型的输入到指定类型的输出的计算。箭头类型类有三种类型参数:箭头类型,输入类型和输出类型。查看箭头实例的实例头,我们发现:

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

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

对于函数b -> cb是输入,c是输出。
对于MyArr b cb是输入,c是输出。

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

-- 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

这是箭头有用的一个原因。它们提供了一种计算模型,可以隐式地利用状态,而不会将该状态暴露给程序员。程序员可以使用箭头化计算并将它们组合起来以创建复杂的系统。

这是一个MyArr,它记录了它收到的输入数量:

-- 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类方法或根据它们编写的函数。

4)上面的大多数代码都是特定于每个Arrow实例[1]。 Control.Arrow(和Control.Category)中的所有内容都是关于组成箭头以制作新箭头。如果我们假装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

>>>函数需要两个箭头,并使用第一个输出作为第二个输入。

这是另一个操作员,通常称为“扇出”:

-- &&& 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类型类为我们提供了一种使用>>=函数将monadic计算组合成单个新monadic计算的方法。类似地,Arrow类为我们提供了使用一些原始函数(firstarr***将箭头化计算组合成单个新箭头化计算的方法,{来自Control.Category的{1}}和>>>。也像Monads一样,问题是“箭头做什么?”不能普遍回答。这取决于箭头。

不幸的是,我不知道野外箭头实例的很多例子。功能和FRP似乎是最常见的应用程序。 HXT是唯一可以想到的其他重要用法。

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

答案 1 :(得分:34)

通过浏览Stack Overflow上的历史记录,我将假设您对某些其他标准类型类感到满意,特别是FunctorMonoid,并从这些简短的比喻。

Functor上的单个操作是fmap,它在列表中充当map的通用版本。这几乎是类型类的全部目的;它定义了“你可以映射的东西”。因此,从某种意义上说,Functor代表了列表特定方面的概括。

Monoid的操作是空列表和(++)的通用版本,它定义了“可以关联地组合的事物,特定的事物是身份值”。列表几乎是符合该描述的最简单的东西,Monoid表示列表的这一方面的概括。

与上述两种方法相同,Category类型类的操作是id(.)的通用版本,它定义了“在特定类型中连接两种类型的内容”方向,可以头尾相连“。因此,这代表了函数的这一方面的概括。值得注意的是,概括不包括currying或函数应用。

Arrow类型类建立在Category之上,但基本概念是相同的:Arrow s是组成类似函数的东西,并且具有为任何函数定义的“标识箭头”类型。在Arrow类上定义的附加操作本身只定义了一种将任意函数提升为Arrow的方法,以及将两个“并行”箭头组合为元组之间的单箭头的方法。

因此,首先要记住的是构建Arrow表达式本质上是精心设计的函数组合。像(***)(>>>)这样的组合器用于编写“无点”样式,而proc符号提供了一种在连接时为输入和输出分配临时名称的方法。

这里要注意的一件有用的事情是,尽管Arrow有时被描述为Monad的“下一步”,但那里确实没有非常有意义的关系。对于任何Monad,您可以使用Kleisli箭头,它们只是类型为a -> m b的函数。 (<=<)中的Control.Monad运算符是这些运算符的箭头组合。另一方面,除非您还包含Arrow类,否则Monad不会为您ArrowApply。因此,没有直接的联系。

这里的关键区别在于,Monad可用于对计算进行排序并逐步执行,Arrow在某种意义上就像常规函数一样“永恒”。它们可以包括由(.)拼接的额外机械和功能,但它更像是构建管道而不是累积动作。

其他相关类型类为箭头添加了其他功能,例如能够将箭头与Either以及(,)组合在一起。


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

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

StreamTrans箭头将输入值转换为输出和自身的“更新”版本;考虑这与有状态Monad的区别。

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

我还写了一篇你可能会觉得有帮助的similar answer previously

答案 2 :(得分:30)

我想补充说,Haskell中的箭头比它们看起来要简单得多 根据文献。它们只是函数的抽象。

要了解这实际上是如何有用的,请考虑您有一堆 你想要写的函数,其中一些是纯粹的,有些是纯粹的 一元。例如,f :: a -> bg :: b -> m1 ch :: c -> m2 d

了解所涉及的每种类型,我可以手工构建作品,但是 组合物的输出类型必须反映中间体 monad类型(在上面的例子中,m1 (m2 d))。如果我只想治疗怎么办? 这些函数好像只是a -> bb -> cc -> d?那是, 我想抽象出monad的存在,并仅仅推断出 基础类型。我可以用箭头做到这一点。

这是一个箭头,用于抽象出IO中函数的存在 IO monad,这样我可以用纯函数组合它们而不用 编写需要知道涉及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 =&gt; a b c”类型的参数。库代码都不需要 要知道有一个monad参与其中。只有创作者和最终用户 箭头需要知道。

将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 monad涉及, arrowUser没有。没有办法从arrowUser“隐藏”IO 没有箭头 - 不是没有诉诸unsafePerformIO转向 中间monadic值回到纯粹的一个(从而失去了那个上下文 永远)。例如:

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

尝试在没有unsafePerformIO且没有arrowUser'的情况下撰写该文章 处理任何Monad类型的论点。

答案 3 :(得分:2)

John Hughes的AFP(高级功能编程)研讨会讲义。请注意,它们是在Base库中更改Arrow类之前编写的:

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

答案 4 :(得分:0)

当我开始探索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(func1),另一个函数可以重新调用null或者被赋值给{{1}最终,第三个函数也可以返回null。现在给定值null,将x传递给func3,然后,如果它不返回x,则将此值传递给func2,并且仅当此值不为null时,将此值传递给func1。它更具确定性,控制流程允许您构建更复杂的异常处理。

在这里,我们可以使用箭头组合:null