Arrows似乎在Haskell社区越来越受欢迎,但在我看来,Monads更强大。使用箭头获得了什么?为什么不能使用Monads呢?
答案 0 :(得分:12)
每个monad都会出现一个箭头
newtype Kleisli m a b = Kleisli (a -> m b)
instance Monad m => Category (Kleisli m) where
id = Kleisli return
(Kleisli f) . (Kleisli g) = Kleisli (\x -> (g x) >>= f)
instance Monad m => Arrow (Kleisli m) where
arr f = Kleisli (return . f)
first (Kleisli f) = Kleisli (\(a,b) -> (f a) >>= \fa -> return (fa,b))
但是,有些箭不是单子。因此,有些箭头可以做你不能用monad做的事情。一个很好的例子是箭头变换器添加一些静态信息
data StaticT m c a b = StaticT m (c a b)
instance (Category c, Monoid m) => Category (StaticT m c) where
id = StaticT mempty id
(StaticT m1 f) . (StaticT m2 g) = StaticT (m1 <> m2) (f . g)
instance (Arrow c, Monoid m) => Arrow (StaticT m c) where
arr f = StaticT mempty (arr f)
first (StaticT m f) = StaticT m (first f)
此箭头转换器非常有用,因为它可用于跟踪程序的静态属性。例如,您可以使用它来检测API以静态测量您正在进行的呼叫数量。
答案 1 :(得分:10)
我总是发现很难用这些术语来思考这个问题:使用箭头可以获得什么。正如其他评论者提到的那样,每个单子都可以简单地变成箭头。所以monad可以做所有的箭头事情。但是,我们可以制作不是monad的箭头。也就是说,我们可以制作可以执行这些箭头的东西而不会使它们支持monadic绑定的类型。它可能看起来不是这样,但monadic绑定函数实际上是一个相当严格(因此很强大)的操作,它取消了许多类型的资格。
请参阅,为了支持bind,你必须能够断言无论输入类型如何,将要发布的内容将包含在monad中。
(>>=) :: forall a b. m a -> (a -> m b) -> m b
但是,我们如何为类似data Foo a = F Bool a
的类型定义绑定当然,我们可以将一个Foo与另一个Foo组合,但我们如何组合Bools。想象一下,Bool标记了其他参数的值是否发生了变化。如果我有a = Foo False whatever
并将其绑定到函数中,我不知道该函数是否会更改whatever
。我无法编写正确设置Bool的绑定。这通常被称为静态元信息的问题。我无法检查被绑定的函数以确定它是否会改变whatever
。
还有其他几种情况:代表变异函数的类型,可以提前退出的解析器等等。但基本思路是这样的:monads设置了一个不是所有类型都能清除的高标准。箭头允许您以强大的方式组合类型(可能或可能不支持这种高的绑定标准),而不必满足绑定。当然,你确实失去了monad的一些力量。
故事的道德:没有箭可以做monad不能做的事,因为monad总是可以做成箭。但是,有时你不能将你的类型变成monad,但你仍然希望它们具有monad的大部分组合灵活性和强大功能。
其中许多想法都受到精湛的Understanding Haskell Arrows
的启发答案 2 :(得分:3)
好吧,我会通过将问题从Arrow
更改为Applicative
来稍微作弊。很多相同的动机都适用,我比箭头更了解应用。 (事实上,every Arrow
is also an Applicative
只有not vice-versa,所以我只是将它向下延伸到Functor
的斜率。)
就像每个Monad
都是Arrow
一样,每个Monad
也是Applicative
。有Applicatives
个Monad
s(例如ZipList
),这是一个可能的答案。
但假设我们正在处理一个承认Monad
实例以及Applicative
的类型。为什么我们有时会使用Applicative
实例而不是Monad
?因为Applicative
功能较弱,而且带来了好处:
Monad
可以做Applicative
无法做到的事情。例如,如果我们使用Applicative
IO
实例来组合更简单的复合操作,我们编写的任何操作都不会使用任何其他操作的结果。应用程序IO
所能做的就是执行组件操作并将其结果与纯函数相结合。Applicative
类型,以便我们可以在执行之前对操作进行强大的静态分析。因此,您可以编写一个程序,在执行它之前检查Applicative
操作,找出它将要执行的操作,并使用它来提高性能,告诉用户将要执行的操作等等。作为第一个例子,我一直在使用Applicative
设计一种OLAP计算语言。类型允许Monad
实例,但我故意避免这样做,因为我希望查询 比Monad
允许的更强大。 Applicative
表示每次计算都将以可预测的查询数量为基础。
作为后者的一个例子,我将使用my still-under-development operational Applicative
library中的玩具示例。如果您将Reader
monad写为操作Applicative
程序,则可以检查生成的Reader
以计算他们使用ask
操作的次数:
{-# LANGUAGE GADTs, RankNTypes, ScopedTypeVariables #-}
import Control.Applicative.Operational
-- | A 'Reader' is an 'Applicative' program that uses the 'ReaderI'
-- instruction set.
type Reader r a = ProgramAp (ReaderI r) a
-- | The only 'Reader' instruction is 'Ask', which requires both the
-- environment and result type to be @r@.
data ReaderI r a where
Ask :: ReaderI r r
ask :: Reader r r
ask = singleton Ask
-- | We run a 'Reader' by translating each instruction in the instruction set
-- into an @r -> a@ function. In the case of 'Ask' the translation is 'id'.
runReader :: forall r a. Reader r a -> r -> a
runReader = interpretAp evalI
where evalI :: forall x. ReaderI r x -> r -> x
evalI Ask = id
-- | Count how many times a 'Reader' uses the 'Ask' instruction. The 'viewAp'
-- function translates a 'ProgramAp' into a syntax tree that we can inspect.
countAsk :: forall r a. Reader r a -> Int
countAsk = count . viewAp
where count :: forall x. ProgramViewAp (ReaderI r) x -> Int
-- Pure :: a -> ProgamViewAp instruction a
count (Pure _) = 0
-- (:<**>) :: instruction a
-- -> ProgramViewAp instruction (a -> b)
-- -> ProgramViewAp instruction b
count (Ask :<**> k) = succ (count k)
据我所知,如果您将countAsk
实施为monad,则无法编写Reader
。 (我的理解是from asking right here in Stack Overflow,我补充说。)
同样的动机实际上是Arrow
背后的一个想法。 Arrow
的一个重要激励示例是解析器组合器设计,它使用“静态信息”来获得比monadic解析器更好的性能。他们所说的“静态信息”与我的Reader
示例大致相同:可以编写一个Arrow
实例,其中解析器可以像我的Reader
一样进行检查可以。然后解析库可以在执行解析器之前检查它以查看它是否可以提前预测它将失败,并在那种情况下跳过它。
在对你的问题的一个直接评论中,jberryman提到箭头可能实际上正在失去人气。我补充说,正如我所看到的那样,Applicative
正是箭头失去了人气。
参考文献:
答案 3 :(得分:1)
问题不太对。这就像问你为什么要吃橘子而不是苹果,因为苹果看起来更有营养。
箭头和monad一样,是表达计算的一种方式,但它们必须服从另一组laws。特别是,当你有类似功能的东西时,法律倾向于使箭头更好用。
Haskell Wiki列出few introductions箭头。特别是,Wikibook是一个很好的高级别介绍,John Hughes的tutorial是对各种箭头的一个很好的概述。
对于一个真实世界的例子,比较使用Hakyll 3基于箭头的界面的this tutorial和基于Hakyll 4的基于monad的界面中的大约the same thing。
答案 4 :(得分:0)
我总是发现一个非常实用的箭头用例流程编程。
看看这个:
data Stream a = Stream a (Stream a)
data SF a b = SF (a -> (b, SF a b))
SF a b
是一个同步流功能。
您可以定义一个函数,将Stream a
转换为永不挂起的Stream b
,并始终为b
输出一个a
:
(<<$>>) :: SF a b -> Stream a -> Stream b
SF f <<$>> Stream a as = let (b, sf') = f a
in Stream b $ sf' <<$>> as
Arrow
有SF
个实例。特别是,您可以撰写 SF
s:
(>>>) :: SF a b -> SF b c -> SF a c
现在尝试在monads中执行此操作。它不能很好地工作。你可能会说Stream a == Reader Nat a
因此它是一个monad,但是monad实例效率非常低。想象一下join
的类型:
join :: Stream (Stream a) -> Stream a
您必须从流中提取对角线。这意味着O(n)
元素的n
复杂度,但使用Arrow
的{{1}}实例原则上会为您提供SF
! (还涉及时间和空间泄漏。)