Haskell 箭头可以做什么,而单子可以';T

Haskell 箭头可以做什么,而单子可以';T,haskell,monads,arrows,Haskell,Monads,Arrows,箭在Haskell社区似乎越来越受欢迎,但在我看来,单子更强大。使用箭头有什么好处?为什么不能用单子来代替呢?这个问题不太正确。这就像问为什么你会吃橘子而不是苹果,因为苹果似乎更富有营养 箭头和单子一样,是表示计算的一种方式,但它们必须遵守不同的规则。特别是,当你有类似于功能的东西时,这些法则倾向于使箭头更易于使用 Haskell Wiki列出了一个指向箭头的列表。特别是,这是一个很好的高级介绍,约翰·休斯的介绍很好地概括了各种箭头 对于一个现实世界的例子,比较一下使用Hakyll 3基于箭头的

箭在Haskell社区似乎越来越受欢迎,但在我看来,单子更强大。使用箭头有什么好处?为什么不能用单子来代替呢?

这个问题不太正确。这就像问为什么你会吃橘子而不是苹果,因为苹果似乎更富有营养

箭头和单子一样,是表示计算的一种方式,但它们必须遵守不同的规则。特别是,当你有类似于功能的东西时,这些法则倾向于使箭头更易于使用

Haskell Wiki列出了一个指向箭头的列表。特别是,这是一个很好的高级介绍,约翰·休斯的介绍很好地概括了各种箭头


对于一个现实世界的例子,比较一下使用Hakyll 3基于箭头的界面和Hakyll 4基于单子的界面。每个单子都会产生一个箭头

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))
但是,有些箭不是单子。因此,有一些箭头可以完成单子无法完成的事情。一个很好的例子是添加一些静态信息的arrow transformer

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)
data StaticT m c a b=StaticT m(c a b)
实例(类别c,幺半群m)=>Category(StaticT m c),其中
id=静态内存id
(StaticT m1 f)。(StaticT m2 g)=StaticT(m1 m2)(f.g)
实例(箭头c,幺半群m)=>箭头(StaticT m c),其中
arr f=静态内存(arr f)
first(StaticT m f)=StaticT m(first f)

这个箭头转换器很有用,因为它可以用来跟踪程序的静态属性。例如,您可以使用它来检测您的API,以静态地测量您正在进行的调用的数量。

我总是发现很难用这些术语来思考这个问题:使用箭头可以获得什么。正如其他评论者所提到的,每一个单子都可以很容易地变成一支箭。所以一个单子可以做所有的事情。然而,我们可以制作非单子的箭头。也就是说,我们可以创建可以执行这些箭头-y操作的类型,而不需要使它们支持一元绑定。看起来可能不是这样,但一元绑定函数实际上是一个限制性很强(因此功能强大)的操作,它取消了许多类型的资格

要支持bind,您必须能够断言,无论输入类型如何,输出的内容都将被包装在monad中

(>>=) :: forall a b. m a -> (a -> m b) -> m b
但是,对于像
data Foo a=F Bool a这样的类型,我们如何定义bind呢?
当然,我们可以将一个Foo的a与另一个Foo的a组合在一起,但是我们如何组合Bool呢。假设Bool标记了另一个参数的值是否已更改。如果我有
a=Foo False whatever
并将其绑定到一个函数中,我不知道该函数是否会更改
whatever
。我无法编写正确设置布尔值的绑定。这通常被称为静态元信息问题。我无法检查绑定到的函数以确定它是否会改变
任何内容

还有其他一些类似的情况:表示变异函数的类型,可以提前退出的解析器,等等。但基本思想是:monad设置了一个并非所有类型都可以清除的高条。箭头允许您以强大的方式组合类型(可能支持也可能不支持这种高绑定标准),而无需满足bind。当然,你会失去一些单子的力量

这个故事的寓意是:没有任何一支箭能做蒙纳德做不到的事,因为蒙纳德总是可以被制成箭。然而,有时候你不能将你的类型转换成单子,但是你仍然希望让它们拥有单子的大部分组合灵活性和功能


这些想法中有很多都是从超级()中得到启发的。

好吧,我想在这里略作欺骗,把问题从
箭头
改为
应用
。许多相同的动机也适用于此,我对应用程序比箭头更了解。(事实上,但是,我只是把它往下移一点,到
Functor

就像每个
Monad
是一个
箭头一样,每个
Monad
也是一个
应用程序。有一些应用程序不是
Monad
s(例如
ZipList
),所以这是一个可能的答案

但是假设我们处理的是一个类型,它允许一个
Monad
实例和一个
应用程序。为什么我们有时会使用
Applicative
实例而不是
Monad
?因为
Applicative
的功能不太强大,而且它还有很多好处:

  • 我们知道,
    Monad
    可以做一些
    Applicative
    无法做的事情。例如,如果我们使用
    IO
    Applicative
    实例从更简单的动作中组合出一个复合动作,那么我们组合的动作中没有一个可以使用其他动作的结果。applicative
    IO
    所能做的就是执行组件操作,并将其结果与纯函数相结合
  • Applicative
    类型可以编写,这样我们就可以在执行操作之前对操作进行强大的静态分析。因此,您可以编写一个程序,在执行
    Applicative
    操作之前检查它,找出它将要做什么,并使用它来提高性能,告诉用户将要做什么,等等
  • 作为第一个例子,我一直在使用
    Applicative
    s设计一种计算语言。该类型允许使用
    Monad
    实例,但我故意避免使用该实例,因为我希望查询的功能不如
    Monad
    所允许的那么强大
    Applicative
    意味着每次计算都将以可预测的查询数量见底

    作为后者的一个示例,我将使用来自的一个玩具示例。如果您编写
    读取器{-# 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)
    
    data Stream a = Stream a (Stream a)
    data SF a b = SF (a -> (b, SF a b))
    
    (<<$>>) :: SF a b -> Stream a -> Stream b
    SF f <<$>> Stream a as = let (b, sf') = f a
                             in  Stream b $ sf' <<$>> as
    
    (>>>) :: SF a b -> SF b c -> SF a c
    
    join :: Stream (Stream a) -> Stream a