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


70

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

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

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



@KennyTM:您的答案几乎可以解释我想知道的大多数事情。现在我明白了一些原因。
2010年

@FUZxxl我最近也想认真地绘制箭头,遇到了同样的挫败感。您是否有一份自己认为对您的搜寻最有帮助的清单?
kizzx2 2011年

Answers:


78

我不知道教程,但是如果您看一些具体示例,我认为最容易理解箭头。我学习如何使用箭头的最大问题是,这些教程或示例都没有实际显示如何使用箭头,而是如何构成箭头。因此,考虑到这一点,这是我的迷你教程。我将研究两个不同的箭头:函数和用户定义的箭头类型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等,为单子)。

-- 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的整数列表。

请注意,我们仍未使用任何“箭头”函数,即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类型的类为我们提供了一种手段,一元计算,合并成使用一个新单子的计算>>=功能。类似地,Arrow类为我们提供了手段arrowized计算合并到单个新arrowized计算使用一些基本函数(firstarr,和***,具有>>>id从Control.Category)。同样类似于Monads的问题是“箭的作用是什么?” 无法普遍回答。这取决于箭头。

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

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


啊,看,又是流传感器!对于您个人而言,这可能不是什么新闻(如果您是约翰,我想是吗?),但是如果将MyArr类型扩展为包括最终状态和Kleisli箭头,则类似data MyArr m a b = Done | Step (a -> m (b, MyArr m a b),并对“运行”进行适当的调整函数和“折叠”构造函数,其结果是在本质上与迭代器非常接近的增量左折叠流处理器。当然说“哦,枚举者可以像一个Arrow”对初学者来说可能没有帮助...
CA McCann 2010年

实际上,我决定在工作中使用迭代器,因为我需要一个功率更大的流传感器。奥列格(Oleg)有一些方便的东西,似乎很适合,这就是我最终得到一个关于Hackage的小图书馆的原因。
约翰L

我喜欢这个答案,因为它给了我思考“箭”的起点。首先,我尝试通过这篇文章,尽管这似乎是一篇不错的文章,但是由于其学究/理论的方法,因此对初学者不友好。我发现此答案非常有帮助。
Nawaz

36

从您关于Stack Overflow的历史记录看,我将假定您对其他一些标准类型类(特别是Functorand)感到满意Monoid,并从其中的简要类比入手。

上的单个操作Functorfmap,它是mapon列表的通用版本。这几乎是类型类的全部目的。它定义了“可以映射的事物”。因此,从某种意义上讲,它Functor表示列表的特定方面的概括。

的操作Monoid是空列表和的通用版本(++),它定义了“可以关联在一起的事物,与特定事物即身份值”。列表几乎是最适合该描述的最简单的事情,它Monoid代表了列表该方面的概括。

以相同的方式如上述两个,在所述操作Category类型类中广义的版本id(.),并将其定义“的东西在特定方向上连接两个类型,即可以是连接头-尾”。因此,这代表了该功能方面的概括。概括地说,不包括在currying或function应用程序中。

Arrow类型的类建立关的Category,但基本概念是相同的:Arrows为东西撰写类似功能,并有一个“同一性箭头”为任何类型的定义。在Arrow类本身上定义的其他操作仅定义了将任意函数提升为Arrow的方法,以及将两个“并行”的箭头组合为元组之间的单个箭头的方法。

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

这里要注意的一个有用的事情是,即使Arrow有时将s描述为s的“下一步” Monad,但实际上并没有非常有意义的关系。对于任何人,Monad您都可以使用Kleisli箭头,这些箭头只是类型为的函数a -> m b。这些中的(<=<)运算符Control.Monad是箭头组成。另一方面,除非您也包括该课程,否则您Arrow不会得到一个。因此没有这样的直接连接。MonadArrowApply

此处的主要区别在于,尽管Monads可用于对计算进行排序和逐步执行操作,但Arrows在某种意义上是“永恒的”,就像常规函数一样。它们可以包括由所拼接的额外机器和功能(.),但更像是建立管道,而不是累积动作。

其它相关的类型的类添加额外的功能,以一个箭头,诸如能够与箭头结合Either以及(,)


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

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

StreamTrans箭头转换的输入值的输出和的本身就是一个“已更新”版本; 考虑一下这与有状态的区别的方法Monad

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

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


31

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

要了解这在实际中的用处,请考虑您要构成的函数,其中有些是纯函数,有些是单子函数。例如,f :: a -> bg :: b -> m1 c,和h :: c -> m2 d

了解了所涉及的每种类型后,我可以手工构建一个合成,但是合成的输出类型必须反映中间的monad类型(在上述情况下为m1 (m2 d))。如果我只是想,如果他们只是治疗的功能a -> bb -> 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”类型的参数。不需要使任何库代码都知道一个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措施将中间单价转换回纯值(从而永远失去上下文)。例如:

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


顺便说一句,是不是bar = return . read一样的bar = read?:-/(想知道,我是一个初学者)。
Nawaz

@Nawaz不,因为read要输入类型String -> Int,而return . read要输入类型String -> IO Int
4castle '19


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

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

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


这不是更必要的方法吗?
galagora
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.