78

我试图学习箭头的含义,但我不明白它们。

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

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

4

5 回答 5

82

我不知道教程,但我认为如果你看一些具体的例子,最容易理解箭头。我在学习如何使用箭头时遇到的最大问题是,没有任何教程或示例真正展示了如何使用箭头,而只是展示了如何组合它们。因此,考虑到这一点,这是我的迷你教程。我将研究两个不同的箭头:函数和用户定义的箭头类型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是输出。
对于 a MyArr b cb是输入,c是输出。

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

-- 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 列表。

请注意,我们仍然没有使用任何“箭头”函数,即箭头类方法或根据它们编写的函数。

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单子计算组合成单个新单子计算的>>=方法。类似地,Arrow该类为我们提供了将箭头化计算组合成一个新的箭头化计算的方法,使用一些原始函数(firstarr、 和***>>>以及id来自 Control.Category 的)。也类似于 Monads,“箭头有什么作用?”的问题。一般无法回答。这取决于箭头。

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

[1] 除外count。可以编写一个计数函数,对ArrowLoop.

于 2010-11-16T16:48:22.443 回答
39

回顾一下您在 Stack Overflow 上的历史,我将假设您对其他一些标准类型类(尤其是Functorand )感到满意Monoid,并从这些类的简短类比开始。

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

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

和上面两个一样,对Category类型类的操作是 和 的泛化版本id(.)它定义了“在特定方向上连接两个类型的东西,可以头尾相连”。因此,这代表了功能方面的概括。值得注意的是,不包括在泛化中的是柯里化或函数应用。

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

因此,首先要记住的是,表达式 buildingArrow本质上是复杂的函数组合。组合器喜欢(***)并且(>>>)用于编写“无点”样式,而proc符号提供了一种在连接时为输入和输出分配临时名称的方法。

这里需要注意的一件有用的事情是,尽管Arrows 有时被描述为 s 的“下一步” Monad,但实际上并没有非常有意义的关系。对于任何人Monad,您都可以使用 Kleisli 箭头,它们只是类型为a -> m b. 中的(<=<)运算符Control.Monad是这些的箭头组合。另一方面,除非您还包括该课程,否则Arrows 不会为您提供 a 。所以没有直接的联系。MonadArrowApply

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

其他相关的类型类为箭头添加了额外的功能,例如能够将箭头与Either以及结合起来(,)


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

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

箭头将StreamTrans输入值转换为输出和自身的“更新”版本;考虑 this 与 stateful 的不同之处Monad

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

之前也写了一个类似的答案,您可能会觉得有帮助。

于 2010-11-16T14:59:42.740 回答
34

我想补充一点,Haskell 中的箭头远比文献中出现的要简单得多。它们只是功能的抽象。

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

知道所涉及的每种类型,我可以手动构建一个组合,但组合的输出类型必须反映中间单子类型(在上述情况下,m1 (m2 d))。如果我只想将函数视为只是a -> b,b -> cc -> d怎么办?也就是说,我想抽象出 monad 的存在,并且只对底层类型进行推理。我可以使用箭头来做到这一点。

这是一个箭头,它为 IO monad 中的函数抽象出 IO 的存在,这样我就可以用纯函数组合它们,而无需知道涉及 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 => ab c”类型的参数。不需要让任何库代码意识到涉及单子。只有箭头的创建者和最终用户需要知道。

将 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但不知道。没有箭头就没有办法“隐藏” IO arrowUser - 不能不求助于unsafePerformIO将中间一元值转回纯值(从而永远失去该上下文)。例如:

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 类型参数的情况下编写它。

于 2012-10-20T06:48:38.577 回答
2

有来自 AFP(高级函数式编程)研讨会的 John Hughes 的讲义。请注意,它们是在基础库中更改 Arrow 类之前编写的:

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

于 2010-11-16T08:13:59.837 回答
1

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

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

因此,本质上,对于某个 value x,首先调用一个我们假设可能返回的函数null(func1),另一个可能会重新调用null或分配给null可互换的函数,最后,第三个函数也可能返回null。现在给定值x,将 x 传递给 func3,然后,如果它不返回null,则将此值传递给 func2,并且仅当此值不为空时,将此值传递给 func1。它更具确定性,并且控制流允许您构建更复杂的异常处理。

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

于 2017-09-17T19:33:07.017 回答