Haskell 箭头可以做什么,而单子可以';T
箭在Haskell社区似乎越来越受欢迎,但在我看来,单子更强大。使用箭头有什么好处?为什么不能用单子来代替呢?这个问题不太正确。这就像问为什么你会吃橘子而不是苹果,因为苹果似乎更富有营养 箭头和单子一样,是表示计算的一种方式,但它们必须遵守不同的规则。特别是,当你有类似于功能的东西时,这些法则倾向于使箭头更易于使用 Haskell Wiki列出了一个指向箭头的列表。特别是,这是一个很好的高级介绍,约翰·休斯的介绍很好地概括了各种箭头Haskell 箭头可以做什么,而单子可以';T,haskell,monads,arrows,Haskell,Monads,Arrows,箭在Haskell社区似乎越来越受欢迎,但在我看来,单子更强大。使用箭头有什么好处?为什么不能用单子来代替呢?这个问题不太正确。这就像问为什么你会吃橘子而不是苹果,因为苹果似乎更富有营养 箭头和单子一样,是表示计算的一种方式,但它们必须遵守不同的规则。特别是,当你有类似于功能的东西时,这些法则倾向于使箭头更易于使用 Haskell Wiki列出了一个指向箭头的列表。特别是,这是一个很好的高级介绍,约翰·休斯的介绍很好地概括了各种箭头 对于一个现实世界的例子,比较一下使用Hakyll 3基于箭头的
对于一个现实世界的例子,比较一下使用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
实例从更简单的动作中组合出一个复合动作,那么我们组合的动作中没有一个可以使用其他动作的结果。applicativeIO
所能做的就是执行组件操作,并将其结果与纯函数相结合
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