什么是箭头,我怎样才能使用它们?
我试图学习箭的意思,但我不明白他们。
我使用了Wikibooks教程。 我认为维基教科书的问题主要在于它似乎是为那些已经了解这个主题的人写的。
有人可以解释什么是箭头,我怎样才能使用它们?
我不知道一个教程,但是如果你看一些具体的例子,我认为理解箭头是最容易的。 我学习如何使用箭头的最大问题是没有任何教程或例子实际上显示如何使用箭头,只是如何组成它们。 所以,考虑到这一点,这是我的迷你教程。 我将检查两个不同的箭头:函数和用户定义的箭头typesMyArr
。
-- type representing a computation data MyArr bc = MyArr (b -> (c,MyArr bc))
1)箭头是从指定types的input到指定types的输出的计算。 箭头typestypes有三个types参数:箭头types,inputtypes和输出types。 看着箭头实例的实例头,我们发现:
instance Arrow (->) bc where instance Arrow MyArr bc where
箭头(或(->)
或MyArr
)是计算的抽象。
对于函数b -> c
, b
是input, c
是输出。
对于MyArr bc
, b
是input, c
是输出。
2)要实际运行一个箭头计算,您可以使用特定于您的箭头types的函数。 对于函数,只需将该函数应用于参数。 对于其他箭头,需要有一个单独的函数(就像runIdentity
, runState
等monad)。
-- run a function arrow runF :: (b -> c) -> b -> c runF = id -- run a MyArr arrow, discarding the remaining computation runMyArr :: MyArr bc -> b -> c runMyArr (MyArr step) = fst . step
3)箭头经常用于处理input列表。 对于这些function可以并行执行,但对于某些步骤中的某些箭头输出,则取决于以前的input(例如保持input的总数)。
-- 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 bc -> [b] -> [c] runMyArrList _ [] = [] runMyArrList (MyArr step) (b:bs) = let (this, step') = step b in this : runMyArrList step' bs
这是箭头有用的原因之一。 他们提供了一个计算模型,可以隐式地使用状态,而不会将该状态暴露给程序员。 程序员可以使用箭头计算并将它们组合起来以创build复杂的系统。
这里是一个MyArr,它保持它收到的input数量:
-- 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作为input并返回从1到n的Ints列表。
请注意,我们仍然没有使用任何“箭头”函数,即Arrow类方法或用它们编写的函数。
4)上面的大多数代码都是针对每个Arrow实例的[1]。 Control.Arrow
(和Control.Category
)中的所有内容都是关于组合箭头来制作新箭头的。 如果我们假装类别是Arrow的一部分而不是一个单独的类:
-- combine two arrows in sequence >>> :: Arrow a => abc -> acd -> abd -- the function arrow instance -- >>> :: (b -> c) -> (c -> d) -> (b -> d) -- this is just flip (.) -- MyArr instance -- >>> :: MyArr bc -> MyArr cd -> MyArr bd
>>>
函数需要两个箭头,并使用第一个的输出作为第二个的input。
这是另一个操作员,通常称为“扇出”:
-- &&& applies two arrows to a single input in parallel &&& :: Arrow a => abc -> abc' -> ab (c,c') -- function instance type -- &&& :: (b -> c) -> (b -> c') -> (b -> (c,c')) -- MyArr instance type -- &&& :: MyArr bc -> MyArr bc' -> 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
types类为我们提供了一种方法,使用>>=
函数将monadic计算组合成一个新的monadic计算。 同样, Arrow
类为我们提供了一种方法,将箭头化的计算组合成一个使用几个原始函数( first
, arr
和***
,来自Control.Category的>>>
和id
)的新的箭头化的计算。 也和Monad类似,“箭是做什么的? 不能普遍回答。 这取决于箭头。
不幸的是,我不知道很多例子在野外的箭头实例。 function和玻璃钢似乎是最常见的应用。 HXT是唯一想到的其他重要用法。
[1]除数。 可以编写一个计数函数,对任何ArrowLoop
实例执行相同的ArrowLoop
。
看一下堆栈溢出的历史,我会假设你对其他一些标准类的类,特别是Functor
和Monoid
感到满意,并且从这些简单的类比开始。
Functor
上的单一操作是fmap
,它是列表中map
的一般化版本。 这几乎是types类的全部目的; 它定义了“可以映射的东西”。 所以,在某种意义上, Functor
代表了列表的特定方面的概括。
Monoid
的操作是空列表和(++)
一般化版本,它定义了“可以关联的事物,特定的事物是一个身份值”。 列表几乎是最符合该描述的东西, Monoid
表示列表的这个方面的一般化。
与上面两种方式相同,对Category
类的操作是id
和(.)
泛化版本,它定义了“在特定方向上连接两种types的东西,可以从头到尾连接”。 所以这代表了这个function方面的一个概括。 值得注意的是不包括在泛化或function应用中。
Arrow
types的类build立在Category
,但是其基本概念是相同的: Arrow
s是类似于函数的东西,并且具有为任何types定义的“标识箭头”。 在Arrow
类本身定义的额外操作只是定义了一个方法来提升一个任意函数的Arrow
和一个方法来组合两个箭头“并行”作为元组之间的单箭头。
所以,首先要记住的是, 构buildArrow
的expression式本质上是精心devise的函数组合 。 像(***)
和(>>>)
这样的组合器是用来写“无点”样式的,而proc
符号给出了一种给input和输出分配临时名字的方法,
这里需要注意的一点是,即使Arrow
有时被描述为Monad
的“下一步”,但是这里并没有一个非常有意义的关系。 对于任何Monad
您可以使用Kleisli箭头,这些箭头只是类似于a -> mb
函数。 Control.Monad
的(<=<)
运算符是这些的箭头组合。 另一方面,除非你还包括ArrowApply
类,否则Arrow
不会让你成为Monad
。 所以没有直接的联系。
这里的关键区别在于, Monad
可以用来对计算进行sorting并且一步一步做事,而Arrow
就像常规函数一样在某种意义上是“永恒的”。 它们可以包含额外的机器和function,通过(.)
拼接,但更像是build立一个pipe道,而不是积累行动。
其他相关types类为箭头添加附加function,例如能够将箭头与Either
以及(,)
。
我最喜欢Arrow
例子是有状态的stream传感器 ,它看起来像这样:
data StreamTrans ab = StreamTrans (a -> (b, StreamTrans ab))
StreamTrans
箭头将input值转换为输出和本身的“更新”版本; 考虑这与有状态的Monad
不同的方式。
为上述types编写Arrow
及其相关types类的实例可能是理解它们如何工作的很好的练习!
我以前也写过一个类似的答案 ,你可能会觉得有帮助。
我想补充一点,Haskell中的箭头要比根据文献显示的要简单得多。 它们只是function的抽象。
要看看这实际上是如何有用的,考虑你有一堆你想要编写的函数,其中一些是纯的,一些是一元的。 例如, f :: a -> b
, g :: b -> m1 c
和h :: c -> m2 d
。
知道所涉及的每种types,我都可以手动构build一个构图,但构图的输出types必须反映中间单元types(在上述情况下, m1 (m2 d)
)。 如果我只是想把这些function看作a -> b
, b -> c
和c -> d
也就是说,我想抽象出单子的存在,并仅仅关注基本types。 我可以用箭头来做到这一点。
这里是一个箭头,它抽象出IO monad中函数的IO的存在,这样我就可以用纯函数来编写它们, 而不需要组合代码知道IO是否涉及 。 我们首先定义一个IOArrow来包装IOfunction:
data IOArrow ab = 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 <- fa return (x, c)
然后我做一些简单的function,我想组成:
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,但是如果我在一个多态函数库中传递这些箭头,他们只需要接受types为“Arrow a => abc”的参数。 没有一个图书馆的代码需要知道涉及到monad。 只有箭头的创build者和最终用户需要知道。
将IOArrow推广到任何Monad中的函数被称为“Kleisli箭头”,并且已经有了一个内build的箭头来实现这个function:
main :: IO () main = do let g = arr (++ "!") . arr foo . Kleisli bar . arr id result <- runKleisli g "123" putStrLn result
你当然也可以使用箭头组合操作符和语法,使箭头更清晰一些:
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
地将unsafePerformIO
中间值变回纯粹的值(从而永远丢失该上下文)。 例如:
arrowUser' :: (String -> String) -> String -> String arrowUser' fx = fx main' :: IO () main' = do let h = (++ "!") . foo . unsafePerformIO . bar . id result = arrowUser' h "123" putStrLn result
尝试写没有unsafePerformIO
,没有arrowUser'
必须处理任何Monadtypes参数。
法新社(高级函数编程)研讨会上有John Hughes的讲稿。 请注意,它们是在基本库中的箭头类更改之前编写的:
当我开始探索箭头组合(本质上是Monad)时,我的方法是打破它最通常与之相关的function语法和组合,并开始使用更多的声明性方法来理解其原则。 考虑到这一点,我发现以下故障更直观:
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。 这是更确定的,控制stream程允许您构build更复杂的exception处理。
在这里,我们可以使用箭头组成: (func3 <=< func2 <=< func1) x
。