我试图学习箭头的含义,但我不明白它们。
我使用了 Wikibooks 教程。我认为 Wikibook 的问题主要在于它似乎是为已经了解该主题的人编写的。
有人可以解释什么是箭头以及如何使用它们吗?
我不知道教程,但我认为如果你看一些具体的例子,最容易理解箭头。我在学习如何使用箭头时遇到的最大问题是,没有任何教程或示例真正展示了如何使用箭头,而只是展示了如何组合它们。因此,考虑到这一点,这是我的迷你教程。我将研究两个不同的箭头:函数和用户定义的箭头类型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
是输出。
对于 a 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 到 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
该类为我们提供了将箭头化计算组合成一个新的箭头化计算的方法,使用一些原始函数(first
、arr
、 和***
,>>>
以及id
来自 Control.Category 的)。也类似于 Monads,“箭头有什么作用?”的问题。一般无法回答。这取决于箭头。
不幸的是,我不知道很多野外箭头实例的例子。函数和 FRP 似乎是最常见的应用。HXT 是唯一想到的其他重要用途。
[1] 除外count
。可以编写一个计数函数,对ArrowLoop
.
回顾一下您在 Stack Overflow 上的历史,我将假设您对其他一些标准类型类(尤其是Functor
and )感到满意Monoid
,并从这些类的简短类比开始。
上的单个操作Functor
is fmap
,它作为map
on 列表的通用版本。这几乎就是类型类的全部目的。它定义了“你可以映射的东西”。因此,从某种意义上说,Functor
它代表了列表特定方面的概括。
的操作Monoid
是空列表 和 的通用版本(++)
,它定义了“可以关联组合的事物,以及作为标识值的特定事物”。列表几乎是符合该描述的最简单的东西,并且Monoid
代表了列表这一方面的概括。
和上面两个一样,对Category
类型类的操作是 和 的泛化版本id
,(.)
它定义了“在特定方向上连接两个类型的东西,可以头尾相连”。因此,这代表了功能方面的概括。值得注意的是,不包括在泛化中的是柯里化或函数应用。
类型类基于Arrow
构建Category
,但基本概念是相同的:Arrow
s 是组成类似函数的事物,并且为任何类型定义了一个“标识箭头”。在类本身上定义的附加操作Arrow
只是定义了一种将任意函数提升为 anArrow
的方法,以及一种将两个箭头“并行”组合为元组之间的单个箭头的方法。
因此,首先要记住的是,表达式 buildingArrow
本质上是复杂的函数组合。组合器喜欢(***)
并且(>>>)
用于编写“无点”样式,而proc
符号提供了一种在连接时为输入和输出分配临时名称的方法。
这里需要注意的一件有用的事情是,尽管Arrow
s 有时被描述为 s 的“下一步” Monad
,但实际上并没有非常有意义的关系。对于任何人Monad
,您都可以使用 Kleisli 箭头,它们只是类型为a -> m b
. 中的(<=<)
运算符Control.Monad
是这些的箭头组合。另一方面,除非您还包括该课程,否则Arrow
s 不会为您提供 a 。所以没有直接的联系。Monad
ArrowApply
这里的关键区别在于,虽然Monad
s 可用于对计算进行排序并逐步执行操作,但Arrow
s 在某种意义上是“永恒的”,就像常规函数一样。它们可以包含由 拼接的额外机制和功能(.)
,但它更像是构建管道,而不是累积操作。
其他相关的类型类为箭头添加了额外的功能,例如能够将箭头与Either
以及结合起来(,)
。
我最喜欢的例子Arrow
是有状态的流传感器,它看起来像这样:
data StreamTrans a b = StreamTrans (a -> (b, StreamTrans a b))
箭头将StreamTrans
输入值转换为输出和自身的“更新”版本;考虑 this 与 stateful 的不同之处Monad
。
为上述类型编写实例Arrow
及其相关类型类可能是理解它们如何工作的一个很好的练习!
我之前也写了一个类似的答案,您可能会觉得有帮助。
我想补充一点,Haskell 中的箭头远比文献中出现的要简单得多。它们只是功能的抽象。
要了解这实际上是如何有用的,请考虑您有一堆要组合的函数,其中一些是纯函数,一些是单子函数。例如,f :: a -> b
、g :: b -> m1 c
和h :: c -> m2 d
。
知道所涉及的每种类型,我可以手动构建一个组合,但组合的输出类型必须反映中间单子类型(在上述情况下,m1 (m2 d)
)。如果我只想将函数视为只是a -> b
,b -> c
和c -> 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 类型参数的情况下编写它。
有来自 AFP(高级函数式编程)研讨会的 John Hughes 的讲义。请注意,它们是在基础库中更改 Arrow 类之前编写的:
当我开始探索 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
.