我试图了解箭头的含义,但我不理解它们。
我使用了Wikibooks教程。我认为Wikibook的问题主要在于它似乎是为那些已经了解这个主题的人写的。
有人可以解释箭头是什么以及如何使用它们吗?
答案 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 -> c
,b
是输入,c
是输出。
对于MyArr b c
,b
是输入,c
是输出。
2)要实际运行箭头计算,请使用特定于箭头类型的函数。对于函数,您只需将函数应用于参数。对于其他箭头,需要有一个单独的函数(就像runIdentity
,runState
等对于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
类为我们提供了使用一些原始函数(first
,arr
和***
将箭头化计算组合成单个新箭头化计算的方法,{来自Control.Category的{1}}和>>>
。也像Monads一样,问题是“箭头做什么?”不能普遍回答。这取决于箭头。
不幸的是,我不知道野外箭头实例的很多例子。功能和FRP似乎是最常见的应用程序。 HXT是唯一可以想到的其他重要用法。
[1] id
除外。可以编写一个count函数,为count
的任何实例执行相同的操作。
答案 1 :(得分:34)
通过浏览Stack Overflow上的历史记录,我将假设您对某些其他标准类型类感到满意,特别是Functor
和Monoid
,并从这些简短的比喻。
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 -> b
,g :: b -> m1 c
和h :: c -> m2 d
。
了解所涉及的每种类型,我可以手工构建作品,但是
组合物的输出类型必须反映中间体
monad类型(在上面的例子中,m1 (m2 d)
)。如果我只想治疗怎么办?
这些函数好像只是a -> b
,b -> c
和c -> 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类之前编写的:
答案 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
。