2010-11-16 100 views
53

我试图学习箭头的含义,但我不明白他们。什么是箭头,我该如何使用它们?

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

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

+6

相关:http://stackoverflow.com/questions/3154701/help-understanding-arrows-in-haskell – kennytm 2010-11-16 07:07:19

+0

@KennyTM:你的回答几乎解释了我想知道的大部分事情。现在我明白了一些原因。 – fuz 2010-11-17 06:35:57

+0

@FUZxxl我最近也想认真地搂住箭,并且遇到了同样的挫折感。你有没有一份你认为对你的任务最有帮助的清单? – kizzx2 2011-08-27 13:51:04

回答

60

我不知道教程,但我认为如果你看一些具体的例子,理解箭头是最容易的。我学习如何使用箭头的最大问题是没有任何教程或示例实际上展示如何使用箭头,以及如何组合它们。所以,考虑到这一点,这是我的迷你教程。我将检查两个不同的箭头:函数和用户定义的箭头类型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 -> c,b是输入,而c是输出。
对于MyArr b c,b是输入,而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返回INTS的列表为n。

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

4)上面的大部分代码都是针对每个Arrow实例的[1]。在Control.Arrow(和Control.Category)中的一切都是关于组成箭头来制作新箭头。如果我们假装类别是箭代替单独的类的一部分:

-- 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类为我们提供了使用几个原始函数(first,arr***,以及来自Control.Category的>>>id)将箭头化的计算组合成单个新箭头化计算的手段。与Monad类似,“箭头是做什么的?”这个问题。不能普遍回答。这取决于箭头。

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

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

+0

啊,看看那个,它又是流换能器!这对你个人来说可能不是新闻(如果你是约翰,我认为你是?),但是如果'MyArr'类型被扩展为包含最终状态和Kleisli箭头,就像'data MyArr mab = Done |步骤(a - > m(b,MyArr mab)',加上对“运行”函数和“折叠”构造函数的适当调整,结果是增量式左侧折叠流处理器的迭代精度非常接近。 “哦,枚举可以像一个”箭头“”可能对初学者没有帮助... – 2010-11-16 17:18:09

+0

我实际上决定使用iteratees进行我的工作,因为我需要一个流量传感器,它的功率稍大一点。似乎很合适,这就是我最终在Hackage上建立一个小型图书馆的过程。 – 2010-11-16 19:19:51

28

从你的历史记录堆栈溢出一眼,我会假设你舒服一些其他的标准类型类别,特别是FunctorMonoid,并从这些简短的比喻开始。

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

Monoid的操作是空列表和(++)的通用版本,它定义了“可以关联组合的事物,以及具有特定值的特定事物”。列表非常符合该描述的最简单的事情,并且Monoid代表列表的该方面的概括。

以相同的方式如上述两个,在Category型类的操作是的id(.)广义的版本,并将其定义“的东西在特定方向上连接两个类型,可连接头 - 尾”。所以,这代表了函数那方面的概括。值得注意的是不包含在泛化中的是咖喱或功能应用。

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

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

这里要注意的一个有用的事情是,即使Arrow s的有时被描述为“下一步”,从Monad s时,真的不是有一个非常有意义的关系。对于任何Monad,您可以使用Kleisli箭头,这些箭头只是a -> m b之类的函数。 Control.Monad中的(<=<)运算符是这些的箭头组合。另一方面,Arrow s不会给你一个Monad,除非你还包括ArrowApply类。所以没有直接的联系。

这里的关键区别在于,尽管Monad s可以用于序列计算并按部就班地进行,但在某种意义上,它与常规函数一样是“永恒的”。它们可以包含额外的机器和功能,这些机器和功能被(.)拼接,但更像是建立管道,而不是积累行动。

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


我最喜欢的一个Arrow的例子是状态流传感器,这是这个样子:

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

一个StreamTrans箭头的输入值转换为输出的“更新”版本身;考虑这与有状态Monad不同的方式。

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

我还写了一个similar answer previously,您可能会发现有帮助。

24

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

要了解这实际上是多么有用,请考虑一下您想要编写的一些 函数,其中一些是纯的,一些是单数的 。例如,f :: a -> b,g :: b -> m1 ch :: c -> m2 d

相知参与,我可以建立由手的组合物的类型,但 组合物的输出类型必须反映中间 单子类型(在上述情况下,m1 (m2 d))。如果我只想将 的功能看作是a -> bb -> cc -> d?也就是, 我想抽象出monads的存在,并且仅为 基础类型提供理由。我可以使用箭头来做到这一点。

这里是一个抽象了IO在 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 => a b c”的参数。没有一个库代码需要 意识到涉及monad。只有箭头 的创建者和最终用户需要知道。

泛化IOArrow的职能任何单子的工作被称为“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将中间一元值变回纯粹的值(因此永远丢失该上下文 )。例如:

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'不必 处理任何单子类型参数。

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),另一种可能retun null或者是可互换地分配给null,最后,第三个函数也可以返回null。现在给定值x,将x传递给func3,如果它不返回null,则将此值传递给func2,并且只有当此值不为null时,才会将此值传递给func1。它更具确定性,并且控制流允许您构建更复杂的异常处理。

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

相关问题