21

Arrows 似乎在 Haskell 社区中越来越受欢迎,但在我看来,Monads 似乎更强大。使用箭可以获得什么?为什么不能使用 Monads 代替?

4

5 回答 5

13

每个单子都会产生一个箭头

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 以静态测量您正在进行的调用次数。

于 2013-04-22T20:29:48.390 回答
11

我一直觉得很难用这些术语来思考这个问题:使用箭头可以获得什么。正如其他评论者所提到的,每个单子都可以轻松地变成一个箭头。所以一个单子可以做所有箭头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 箭头备份

于 2013-04-22T22:37:53.910 回答
3

好吧,我将通过将问题从 更改为 来稍微Arrow作弊Applicative。许多相同的动机都适用,而且我比箭头更了解应用程序。(事实上​​,everyArrow也是 aApplicative反之亦然,所以我只是将它从斜坡往下移一点到Functor.)

就像everyMonad是一个Arrow,everyMonad也是一个Applicative。有些Applicatives不是Monads(例如,ZipList),所以这是一个可能的答案。

但是假设我们正在处理一个允许Monad实例和Applicative. 为什么我们有时会使用Applicative实例而不是Monad?因为Applicative功能不那么强大,并且带来了好处:

  1. 有些事情我们知道Monad可以做Applicative而不能做。例如,如果我们使用 of 的Applicative实例IO从更简单的动作中组合出一个复合动作,那么我们组成的任何动作都不能使用任何其他动作的结果。applicativeIO所能做的就是执行组件动作并将它们的结果与纯函数结合起来。
  2. Applicative可以编写类型,以便我们可以在执行动作之前对动作进行强大的静态分析。所以你可以编写一个程序,Applicative在执行之前检查一个动作,弄清楚它要做什么,然后用它来提高性能,告诉用户要做什么,等等。

作为第一个例子,我一直致力于设计一种使用s的OLAP计算语言。Applicative该类型承认一个Monad实例,但我故意避免这样做,因为我希望查询不如Monad允许 的强大。Applicative意味着每次计算都将达到可预测的查询数量。

作为后者的示例,我将使用我仍在开发的操作Applicative中的一个玩具示例。如果您将Readermonad 编写为操作程序Applicative,则可以检查结果Readers 以计算它们使用该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 中的提问,我会补充。)

同样的动机实际上是Arrows 背后的想法之一。一个很大的激励示例Arrow是解析器组合器设计,它使用“静态信息”来获得比单子解析器更好的性能。他们所说的“静态信息”的含义与我的示例中的或多或少相同Reader:可以编写一个Arrow可以像我一样检查解析器的实例Reader。然后解析库可以在执行解析器之前检查它,看它是否可以提前预测它将失败,并在这种情况下跳过它。

在对您的问题的直接评论之一中,jberryman 提到箭头实际上可能正在失去人气。我要补充一点,正如我所看到的,Applicative箭头正在失去人气。


参考:

于 2013-04-23T01:05:50.113 回答
1

这个问题不太对。这就像问你为什么要吃橙子而不是苹果,因为苹果看起来更有营养。

箭头和 monad 一样,是一种表达计算的方式,但它们必须遵守一组不同的定律。特别是,当你有类似函数的东西时,这些定律往往会使箭头更容易使用。

Haskell Wiki 列出了一些关于箭头的介绍。特别是,Wikibook是一个很好的高级介绍,John Hughes 的教程很好地概述了各种箭头。

对于一个真实世界的例子,比较本教程使用 Hakyll 3 的基于箭头的界面,与 Hakyll 4 的基于 monad 的界面大致相同

于 2013-04-22T20:29:08.267 回答
1

我总是发现箭头的真正实际用例之一是流式编程

看这个:

data Stream a = Stream a (Stream a)
data SF a b = SF (a -> (b, SF a b))

SF a b是一个同步流函数。您可以从中定义一个函数,该函数转换Stream aStream 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。特别是,您可以编写 SFs:

(>>>) :: 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 的实例会给你!(并且还处理时间和空间泄漏。)nArrowSFO(1)

于 2018-06-18T17:37:40.477 回答