Arrows 似乎在 Haskell 社区中越来越受欢迎,但在我看来,Monads 似乎更强大。使用箭可以获得什么?为什么不能使用 Monads 代替?
5 回答
每个单子都会产生一个箭头
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))
但是,有些箭头不是单子。因此,有些箭头可以做你不能用单子做的事情。一个很好的例子是箭头转换器添加一些静态信息
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 以静态测量您正在进行的调用次数。
我一直觉得很难用这些术语来思考这个问题:使用箭头可以获得什么。正如其他评论者所提到的,每个单子都可以轻松地变成一个箭头。所以一个单子可以做所有箭头y的事情。但是,我们可以制作不是单子的箭头。也就是说,我们可以使类型可以做这些箭头-y 的事情,而无需让它们支持单子绑定。看起来可能不是这样,但一元绑定函数实际上是一个相当严格(因此很强大)的操作,它取消了许多类型的资格。
看,为了支持绑定,你必须能够断言无论输入类型如何,将要输出的内容将被包装在 monad 中。
(>>=) :: forall a b. m a -> (a -> m b) -> m b
但是,我们将如何为类型定义绑定,data Foo a = F Bool a
当然,我们可以将一个 Foo 的 a 与另一个的结合,但我们将如何结合 Bools。想象一下,Bool 标记了另一个参数的值是否发生了变化。如果我有a = Foo False whatever
并且我将它绑定到一个函数中,我不知道该函数是否会改变whatever
。我无法编写正确设置 Bool 的绑定。这通常被称为静态元信息问题。我无法检查绑定到的函数以确定它是否会改变whatever
。
还有其他几种情况:表示变异函数的类型、可以提前退出的解析器等。但基本思想是:monad 设置了一个并非所有类型都能清除的高标准。箭头允许您以强大的方式组合类型(可能支持也可能不支持这种高绑定标准),而不必满足绑定。当然,你确实失去了一些单子的力量。
故事的寓意:没有什么是单子不能做的,因为单子总是可以变成箭头。然而,有时你不能将你的类型变成 monad,但你仍然希望让它们拥有 monad 的大部分组合灵活性和强大功能。
许多这些想法的灵感来自于精湛的理解 Haskell 箭头(备份)
好吧,我将通过将问题从 更改为 来稍微Arrow
作弊Applicative
。许多相同的动机都适用,而且我比箭头更了解应用程序。(事实上,everyArrow
也是 aApplicative
但反之亦然,所以我只是将它从斜坡往下移一点到Functor
.)
就像everyMonad
是一个Arrow
,everyMonad
也是一个Applicative
。有些Applicatives
不是Monad
s(例如,ZipList
),所以这是一个可能的答案。
但是假设我们正在处理一个允许Monad
实例和Applicative
. 为什么我们有时会使用Applicative
实例而不是Monad
?因为Applicative
功能不那么强大,并且带来了好处:
- 有些事情我们知道
Monad
可以做Applicative
而不能做。例如,如果我们使用 of 的Applicative
实例IO
从更简单的动作中组合出一个复合动作,那么我们组成的任何动作都不能使用任何其他动作的结果。applicativeIO
所能做的就是执行组件动作并将它们的结果与纯函数结合起来。 Applicative
可以编写类型,以便我们可以在执行动作之前对动作进行强大的静态分析。所以你可以编写一个程序,Applicative
在执行之前检查一个动作,弄清楚它要做什么,然后用它来提高性能,告诉用户要做什么,等等。
作为第一个例子,我一直致力于设计一种使用s的OLAP计算语言。Applicative
该类型承认一个Monad
实例,但我故意避免这样做,因为我希望查询不如Monad
允许 的强大。Applicative
意味着每次计算都将达到可预测的查询数量。
作为后者的示例,我将使用我仍在开发的操作Applicative
库中的一个玩具示例。如果您将Reader
monad 编写为操作程序Applicative
,则可以检查结果Reader
s 以计算它们使用该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
据我了解,如果您实现Reader
为 monad ,您将无法编写。(我的理解来自于 Stack Overflow 中的提问,我会补充。)
同样的动机实际上是Arrow
s 背后的想法之一。一个很大的激励示例Arrow
是解析器组合器设计,它使用“静态信息”来获得比单子解析器更好的性能。他们所说的“静态信息”的含义与我的示例中的或多或少相同Reader
:可以编写一个Arrow
可以像我一样检查解析器的实例Reader
。然后解析库可以在执行解析器之前检查它,看它是否可以提前预测它将失败,并在这种情况下跳过它。
在对您的问题的直接评论之一中,jberryman 提到箭头实际上可能正在失去人气。我要补充一点,正如我所看到的,Applicative
箭头正在失去人气。
参考:
- Paolo Capriotti & Ambrus Kaposi,“免费应用函子”。非常推荐。
- Gergo Erdi,“应用程序的静态分析”。鼓舞人心,但我很难跟上...
我总是发现箭头的真正实际用例之一是流式编程。
看这个:
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
您必须从流中提取对角线。这意味着th 元素O(n)
的复杂性,但是原则上使用 s 的实例会给你!(并且还处理时间和空间泄漏。)n
Arrow
SF
O(1)