26

所以我将把我的问题分成 4 个部分,但首先是一些背景:

我对 Monads 感觉比较舒服,但对 Arrows 感觉不太舒服。我想我对它们的主要问题是,我看不出它们有什么用。无论形式上是否正确,我都理解 Monads 是一种允许我们引入计算副作用的工具。当他们将程序片段从纯值概括为包含其他操作的值时。从我的霰弹枪“阅读所有论文”学习箭头的方法中,我遇到了两个相互矛盾的观点:

A. 箭头比 Monads 更强大/是 Monads 的概括。haskell wiki 开头是“它们可以做 monad 可以做的所有事情,甚至更多。它们大致相当于具有静态组件的 monad。”

B. 箭头是 Monad 的子集使用 ArrowApply 我们可以定义一个 monad

  1. 观点A有任何道理吗?
  2. 箭头没有什么样的功能,我读到区别与组合有关,那么 >>> 运算符允许我们做什么 >>= 没有?
  3. 应用究竟是做什么的?它的类型甚至没有 (->)
  4. 为什么我们要在单子上使用应用箭头?
4

3 回答 3

42

多重可解释语句警报:

“A 比 B 更强大”...“C 是 D 的概括”...“E 可以做 F 可以做的一切,甚至更多”...“G 是 H 的子集”...

首先,我们需要了解我们所说的强大等的含义。假设我们有一个类GripHandle用于具有握柄的东西,而另一个类Screwdriver用于螺丝刀。哪个更强大?

  • 很明显,如果你只有一个把手,那不如你是一把螺丝刀那么有用。单独的把手并没有多大用处,所以很明显,因为你可以用螺丝刀做更多的事情而不是把手,所以螺丝刀更强大。
  • 很明显,除了螺丝刀之外,更多的东西都有把手——钻头、刀和叉子,所有东西都有把手,所以把手更强大、更灵活。
  • 很明显,如果你有一把螺丝刀,你不仅可以握住它,还可以转动它,并且能够转动而不是仅仅握住,这使得螺丝刀比手柄更强大和灵活。

好吧,这是一个愚蠢的论点,但它提出了一个很好的观点,即“你可以用 _____ 做更多”这句话是多么模棱两可。

如果你只坚持界面,螺丝刀比手柄更有用,但如果你使用的功能不仅仅是界面,所有带有手柄的东西都比所有螺丝刀更有用。

层次结构如何工作

AB
=更通用A仅接口功能是B's 的子集 = 你可以用B实例做更多事情(单独)
= 所有 s 的类是所有Bs 类的子集A= As 比Bs多= 你可以在A课堂 上做更多的事情

更通用
=更多可能的实例
=能够更广泛地使用
=可以在幕后做额外的事情
=在接口中指定的功能更少=可以通过接口
做更少的事情

箭头和单子之间的层次结构是什么?

  • Arrow比 更一般Monad
  • ArrowApply和 一样一般Monad

这两个陈述在 Petr Pudlák 在他的评论中链接到的论文中得到了充分的证明:成语是健忘的,箭头是细致的,单子是混杂的

A 和 B 中的断言

  • “Arrows 可以做 monad 可以做的一切,甚至更多。”
    这就是营销。这是真的,但你必须在语义上跳跃一下才能使它成为真的。一个单独的ArrowApply实例允许您做一个Monad实例自己做的所有事情。你不能ArrowApply用 a做更多的事情Monad。你可以做更多的事情Arrows。声明“monads 可以做的一切”可能是指,ArrowApply而声明“以及更多”可能是指Arrow!Monad 营销委员会可以说“有了 Monads,你可以做 Arrows 可以做的所有事情,甚至更多”,因为界面的表现力增强了。这些陈述是模棱两可的,因此几乎没有正式的意义。
  • “它们大致相当于具有静态组件的单子。”
    严格来说,不,这只是你可以做的事情Arrow,你不能直接用 a 做Monad,而不是关于箭头接口的数学事实。这是一种帮助您掌握我们可能对箭头所做的事情的方法,类似于将 Monad 类比为具有 in 值的盒子。并非所有 monad 都容易解释为具有 in 值的盒子,但它可能会在早期阶段对您有所帮助。
  • “箭头是 Monad 的一个子集”
    这可能具有误导性。确实,Arrows 的仅接口功能是 Monads 的仅接口功能的子集,但更公平地说,所有 Monads 的类是所有 Arrows 类的子集,因为Arrow它更通用。
  • “使用 ArrowApply,我们可以定义一个单子”
    是的。稍后看,使用 Monad,我们可以定义一个 ArrowApply。

你的四个问题

  1. 观点A有任何道理吗?
    一些。往上看。B也有一些道理。两者都在某种程度上具有误导性。
  2. 箭头没有什么样的功能,我已经读到差异与组合有关,那么 >>> 运算符允许我们做什么 >>= 没有?
    实际上>>=可以让你做的不止是>>>(更多接口提供的能力)。它允许您进行上下文切换。这是因为is 是一个函数,因此您可以在决定运行哪个单子的东西之前Monad m => a -> m b在输入上执行任意纯代码,而is 不是一个函数,并且您在检查 input 之前已经决定了要运行的箭头。aArrow m => m a ba

    monadSwitch :: Monad m => m a -> m a -> (Bool -> m a)
    monadSwitch computation1 computation2 test 
          = if test then computation1 else computation2
    

    Arrow不使用appfrom就无法模拟这个ArrowApply

  3. 应用究竟是做什么的?它的类型甚至没有 (->)
    它允许您将箭头的输出用作箭头。让我们看看类型。

    app :: ArrowApply m => m (m b c, b) c
    

    我更喜欢使用mto,a因为m感觉更像是一种计算并且a感觉像是一种价值。有些人喜欢使用类型运算符(中缀类型构造函数),所以你得到

    app :: ArrowApply (~>) => (b ~> c, b) ~> c
    

    我们认为b ~> c是一个箭头,我们认为箭头是一个接受bs、做某事并给出cs 的事物。所以这意味着app一个箭头,它接受一个箭头和一个值,并且可以产生第一个箭头在该输入上产生的值。

    它在类型签名中没有->,因为在使用箭头进行编程时,我们可以使用 将任何函数转换为箭头arr :: Arrow (~>) => (b -> c) -> b ~> c,但您不能将每个箭头都转换为函数,因此在哪里都(b ~> c, b) ~> c可以使用,(b ~> c, b) -> c(b -> c, b) ~> c可以不在。

    即使没有 ArrowApply,我们也可以轻松地制作一个产生一个箭头甚至多个箭头的箭头,只需produceArrow :: Arrow (~>) => (b ~> c) -> (any ~> (b ~> c))定义 with即可produceArrow a = arr (const a)。困难在于让那个箭头做任何箭头工作 - 你如何让你生产的箭头成为下一个箭头?您不能>>>像使用 monadic函数 Monad m => a -> m b那样将其作为下一个计算使用(只需执行id :: m a -> m a!),因为至关重要的是,箭头不是函数,但是使用app,我们可以使下一个箭头执行箭头产生的任何操作按前面的箭头就可以了。

    因此,ArrowApply 为您提供了从 Monad 获得的运行时生成的计算可运行性。

  4. 为什么我们要在单子上使用应用箭头?
    呃,你是指箭头还是应用函子?应用函子很棒。它们比 Monad 或 Arrow 更通用(参见论文),因此具有更少的接口指定功能,但更广泛适用(明白了吗?适用/应用 chortle chortle lol rofl 类别理论幽默哈哈哈)。

    Applicative Functors 有一个可爱的语法,看起来很像纯函数应用程序。然后f <$> ma <*> mb <*> mc运行ma然后将纯函数应用于三个结果。例如。从用户那里读取两个整数并将它们相加。mbmcf(+) <$> readLn <*> readLn

    你可以使用 Applicative 来获得通用性,你可以使用 Monads 来获得接口功能,所以你可以争辩说理论上我们不需要它们,但是有些人喜欢箭头的表示法,因为它就像 do 表示法,而你确实可以Arrow用来实现具有静态组件的解析器,从而应用编译时优化。我相信你可以用 Applicative 做到这一点,但它首先是用 Arrow 完成的。

    关于 Applicative “不太强大”的注释:
    论文指出它Applicative比 更通用Monad,但是您可以run :: Applicative f => f (f b) -> f b通过提供一个允许您运行生成的计算或use :: Applicative f => f (a -> f b) -> f a -> f b允许您提升生成的计算的函数来使 Applicative 函子具有相同的能力计算到计算。如果我们定义join = rununit = (<$>)获得了为 Monads 提供一个理论基础的两个函数,并且如果我们定义(>>=) = flip (use.pure)return = unit获得了在 Haskell 中使用的另一个函数。没有一个ApplicativeRun类,仅仅是因为如果你能做到,你就可以做一个 monad,并且类型签名几乎是相同的。我们拥有ArrowApply而不是重用的唯一原因Monad是类型不一样;~>在 ArrowApply 中被抽象(概括)到接口中,但函数应用程序->直接在 Monad 中使用。尽管 ArrowApply 和 Monad 是等价的,但这种区别使得使用 Arrows 编程在许多方面与使用 monad 编程感觉不同。

  5. <咳嗽>为什么我们要在 Monad 上使用 Arrows/ArrowApply?
    好吧,我承认我知道这就是你的意思,但是想谈谈 Applicative functors 并且被忘得一干二净,忘了回答!

    能力原因:是的,如果你有一些不能变成单子的东西,你会想在单子上使用箭头。首先为我们带来 Arrows 的激励示例是解析器——您可以使用 Arrow 编写一个解析器库,在组合器中进行静态分析,从而制作更高效的解析器。以前的 Monadic 解析器无法做到这一点,因为它们将解析器表示为一个函数,它可以对输入做任意事情而无需静态记录它们,因此您无法在编译时/组合时对其进行分析。

    语法原因:不,我个人不想使用基于箭头的解析器,因为我不喜欢箭头proc/do符号 - 我发现它比单子符号更糟糕。我对解析器的首选表示法是 Applicative,您也许可以编写一个 Applicative 解析器库来执行 Arrow 所做的高效静态分析,尽管我坦率地承认我常用的解析器库不这样做,可能是因为他们想要提供一个 Monadic 接口。

    • 单子:

          parseTerm = do
               x <- parseSubterm
               o <- parseOperator
               y <- parseSubterm
               return $ Term x o y
      
    • 箭:

          parseTerm = proc _ -> do
               x <- parseSubterm -< ()
               o <- parseOperator -< ()
               y <- parseSubterm -< ()
               returnA -< Term x o y
      
    • 适用:

          parseTerm = Term <$> parseSubterm <*> parseOperator <*> parseSubterm
      

      这看起来就像函数应用程序使用$了几次。嗯。整洁的。清除。低语法。提醒我为什么我更喜欢 Haskell 而不是任何命令式编程语言。

为什么 ArrowApply 中的 app 会生成 Monad?

在Control.Arrow模块的 ArrowApply 部分中有一个 Monad 实例,为了清晰起见,我将(~>)对其进行a编辑。(我已经离开Functor了,因为在没有 Functor 的情况下定义 Monad 很愚蠢——你应该定义fmap f xs = xs >>= return . f.):

newtype ArrowMonad (~>) b = ArrowMonad (() ~> b)

instance Arrow (~>) => Functor (ArrowMonad (~>)) where
    fmap f (ArrowMonad m) = ArrowMonad $ m >>> arr f

instance ArrowApply (~>) => Monad (ArrowMonad (~>)) where
    return x = ArrowMonad (arr (\_ -> x))
    ArrowMonad m >>= f = ArrowMonad $
        m >>> arr (\x -> let ArrowMonad h = f x in (h, ())) >>> app

那有什么作用?好吧,首先,ArrowMonad是 anewtype而不是类型同义词,这样我们就可以在没有各种讨厌的类型系统问题的情况下创建实例,但是让我们忽略这一点,以便通过替换来实现概念上的清晰度而不是可编译性,就好像它是type ArrowMonad (~>) b = () ~> b

instance Arrow (~>) => Functor (() ~>) where
    fmap f m = m >>> arr f

(使用不可编译的类型运算符部分(()~>)作为类型构造函数)

instance ArrowApply (~>) => Monad (() ~>) where
 -- return :: b -> (() ~> b)
    return x = arr (\_ -> x)
 -- (>>=) ::   ()~>a   ->    (a  ->  ()~>b )   ->   ()~>b 
    m >>= f = 
        m >>> arr (\x ->  (f x, ()) ) >>> app

好的,这更清楚了发生了什么。首先注意箭头和 monad 之间的对应关系在 and 之间Monad m => b -> m cArrow (~>) => b ~> c但是 monad 类不涉及b声明中的 。这就是为什么我们需要提供虚拟值()() ~> b让事情从零输入开始并复制一些类型的东西m b

  • 相当于fmap将函数应用于输出的地方,只是产生输出,然后以箭头形式运行函数:fmap f m = m >>> arr f
  • return 的等价物(仅产生指定的 value x)只是以const x箭头形式运行函数,因此return x = arr (\_ -> x).
  • 相当于 bind >>=,它运行一个计算,然后使用输出作为一个函数的输入,f然后可以计算下一个要运行的计算是:首先m >>>运行第一个计算m,然后arr (\x -> (f x, ....使用输出,应用函数f,然后使用该箭头作为输入的app行为就好像它是()像往常一样作用于提供的输入的输出箭头。整洁的!
于 2013-07-16T10:06:39.133 回答
10

观点 A 有点奇怪——一般来说,一个抽象不会比其他抽象更强大和更通用;两人不和。拥有“更多权力”意味着更多地了解你正在使用的东西的结构,这意味着更多的限制。在一种极端情况下,您确切地知道您正在使用哪种类型。这是非常强大的;您可以对其应用任何有效的功能。另一方面,它至少也不是通用的:用这种假设编写的代码只适用于那种类型!在另一个极端,您对您的类型一无所知(例如,拥有一个类型变量a)。这是非常普遍的,适用于每个类型,但也不强大,因为您根本没有足够的信息来做任何事情!

Functor一个更植根于真实代码的例子是和之间的区别Applicative。这里,Functor更一般——严格来说,Functors 比Applicatives 更多的类型,因为everyApplicative也是 aFunctor但反之则不然。但是,由于Applicative具有更多的结构,因此它的功能更强大。使用Functor,您只能将单参数函数映射到您的类型;使用Applicative,您可以映射任意数量参数的函数。再说一遍:一个更通用,另一个更强大。

那么它是哪一个?箭头是否比单子更强大或更通用?这是一个比比较函子、应用函子和单子更难的问题,因为箭头是一种非常不同的野兽。他们甚至有不同的种类:monads 等人有 kind* -> *而箭头有 kind * -> * -> *。令人高兴的是,事实证明我们可以用 applicative functors/monads 来识别箭头,因此我们实际上可以有意义地回答这个问题:箭头比 monads 更通用,因此功能更弱。给定任何单子,我们可以从中构造一个箭头,但我们不能为每个箭头构造一个单子。

基本思路如下:

instance Monad m => Category (a -> m b) where
  id = return
  (f . g) x = g x >>= f

instance Monad m => Arrow (a -> m b) where
  arr f = return . f
  first f (x, y) = f x >>= \ x' -> return (x', y)

但是,由于我们有一个箭头实例a -> b,我们必须将其包装a -> m bnewtype实际代码中。这newtype被称为Klesli(因为Klesli 类别)。

但是,我们不能走另一条路——没有构造可以Monad任何 Arrow. 发生这种情况是因为Arrow计算不能根据流经它的值来改变其结构,而 monad 可以。解决这个问题的唯一方法是通过一些额外的原始函数来为你的箭头抽象增加力量;这正是这样ArrowApply做的。

>>>箭头运算符是函数的泛化,.因此具有相同的一般限制。>>=另一方面,更像是对函数应用的概括。注意类型:对于>>>,两边都是箭头;对于>>=,第一个参数是一个值 ( m a),第二个参数是一个函数。此外,结果>>>是另一个箭头,其中的结果>>=是一个值。由于箭头只有>>>但没有等价于 的概念>>=,因此您一般不能将它们“应用”到参数上——您只能构造箭头管道。实际的应用/运行功能必须特定于任何给定的箭头。另一方面,单子被定义为>>=因此默认情况下带有一些应用程序的概念。

ArrowApply只是用 扩展箭头app,这是应用的一般概念。让我们想象一下正常的功能应用:

apply :: (b -> c) -> b -> c
apply f x = f x

我们可以 uncurry 得到:

apply :: ((b -> c), b) -> c

箭头泛化函数的方式基本上是用->变量 ( a) 替换。让我们通过用中缀apply替换两个出现来做到这一点:->a

apply :: (b `a` c, b) `a` c

我们仍然可以看到与 的第一个版本相同的结构apply,只是 uncurried 和 with`a`而不是->。现在,如果我们去掉反引号并加上a前缀,我们就得到了 的签名app

app :: a (a b c, b) c

因此,我们看到如何ArrowApply将一些应用概念添加到箭头。这是与 的平行>>=,它是单子的应用概念(或者,特别是形状的函数a -> m b)。这是足够的附加结构来从箭头构建一个 monad,因此ArrowApply同构于Monad.

我们为什么要使用这些?老实说,我认为我们不会。箭头被高估了,所以坚持使用 monads 和 applicative functors。

于 2013-07-16T10:21:10.850 回答
1

Monad是一种工具,它允许我们以命令式(逐步)编写。

箭头是一种工具,它允许我们以框图样式编写。

所以,monad for arrows 看起来像线性框图。

http://www.soi.city.ac.uk/~ross/talks/fop.pdf

http://www.haskell.org/arrows/syntax.html

于 2013-08-01T22:37:09.193 回答