【问题标题】:What are the benefits of applicative parsing over monadic parsing?与单子解析相比,应用解析有什么好处?
【发布时间】:2011-10-22 19:01:00
【问题描述】:

似乎有一个共识,您应该将 Parsec 用作应用程序而不是 monad。应用解析比单子解析有什么好处?

  • 风格
  • 性能
  • 抽象

monadic 解析出来了吗?

【问题讨论】:

    标签: haskell monads parsec applicative


    【解决方案1】:

    一元解析和应用解析之间的主要区别在于如何处理顺序组合。对于应用解析器,我们使用 (<*>),而对于 monad,我们使用 (>>=)

    (<*>) :: Parser (a -> b) -> Parser a -> Parser b
    (>>=) :: Parser a -> (a -> Parser b) -> Parser b
    

    一元方法更灵活,因为它允许第二部分的语法依赖于第一部分的结果,但我们在实践中很少需要这种额外的灵活性。

    您可能认为拥有一些额外的灵活性并没有什么坏处,但实际上它可以。它阻止我们在不运行解析器的情况下对它进行有用的静态分析。例如,假设我们想知道解析器是否可以匹配空字符串,以及匹配中可能的第一个字符。我们想要函数

    empty :: Parser a -> Bool
    first :: Parser a -> Set Char
    

    使用应用解析器,我们可以轻松回答这个问题。 (我在这里有点作弊。假设我们有一个数据构造函数对应于我们的候选解析器“语言”中的(&lt;*&gt;)(&gt;&gt;=))。

    empty (f <*> x) = empty f && empty x
    first (f <*> x) | empty f   = first f `union` first x
                    | otherwise = first f
    

    但是,对于单子解析器,我们不知道第二部分的语法是什么,而无需知道输入。

    empty (x >>= f) = empty x && empty (f ???)
    first (x >>= f) | empty x   = first x `union` first (f ???)
                    | otherwise = first x
    

    通过允许更多,我们能够减少推理。这类似于动态和静态类型系统之间的选择。

    但这有什么意义呢?我们可以将这些额外的静态知识用于什么?好吧,我们可以使用它来避免在 LL(1) 解析中回溯,方法是将下一个字符与每个备选的 first 集合进行比较。我们还可以通过检查两个备选方案的first 集是否重叠来静态确定这是否会产生歧义。

    另一个例子是它可以用于错误恢复,如 S. Doaitse Swierstra 和 Luc Duponcheel 的论文 Deterministic, Error-Correcting Combinator Parsers 中所示。

    但是,通常情况下,您正在使用的解析库的作者已经在应用解析和单子解析之间做出了选择。当像 Parsec 这样的库同时公开这两个接口时,选择使用哪一个纯粹是一种风格。在某些情况下,应用程序代码比一元代码更容易阅读,有时恰恰相反。

    【讨论】:

    • 等等!直到今天我也这么想,当我想到 empty 测试也可以应用于单子解析器时。原因是我们可以通过对空字符串应用解析器x 来获取您命名为??? 的值。更一般地说,您可以将空字符串输入解析器,看看会发生什么。类似地,至少可以以函数形式first :: Parser a -&gt; (Char -&gt; Bool)获得首字符集。当然,将后者转换为Set Char 会涉及到字符的低效枚举,这就是应用函子的优势所在。
    • @HeinrichApfelmus 你不能这样回答empty。或者你可以,但这就像用“让我们运行程序看看它是否停止”来回答 [en.wikipedia.org/wiki/Halting_problem]
    • @hammar:如果我们运行 let x = pure f &lt;*&gt; y &lt;*&gt; x in empty x 会怎样。如果empty yFalse,那么计算不会终止……只是要指出,它并不是那么简单。
    【解决方案2】:

    如果解析器是纯粹的应用程序,则可以在运行之前分析其结构并“优化”它。如果解析器是单子的,它基本上是一个图灵完备的程序,并且对其执行几乎任何有趣的分析都相当于解决停机问题(即,不可能)。

    哦,是的,还有风格上的差异......

    【讨论】:

    • Applicative 和 Monad 的区别与图灵完备性无关。在 Haskell 中,优化 Monad 实例的相对困难只是由于在类型类中单独暴露 (&gt;&gt;=) 的历史错误,使得实例无法提供像 ap 这样的运算符的更优化实现。 Applicative 类避免了这个错误,并暴露了&lt;*&gt;(相当于ap)。
    • @PietDelport,我认为问题在于 monadic 解析器的底层表示通常不适用于优化的 Applicative 实例,而应用程序解析器通常不支持 &gt;&gt;=
    • @PiDelport 它绝对与图灵完备性有关。 &gt;&gt;= 接受左侧解析器的结果并将其传递给右侧的解析器,从而使生成的解析器无法静态分析,因为现在解析器本身已经完成。 &lt;*&gt;&lt;$&gt; 不检查参数,因此虽然解析器本身的输出是图灵完整的,因为您可以将任何内容放在 &lt;$&gt; 的左侧,解析方面本身受到限制并且可以静态分析。跨度>
    【解决方案3】:

    我可以看出,我更喜欢应用程序解析器而不是单子解析器的主要原因与在任何情况下更喜欢应用程序代码而不是单子代码的主要原因相同:应用程序功能不那么强大,使用起来更简单。

    这是更通用的工程格言的一个实例:使用完成工作的最简单的工具。不要使用叉车。不要使用台锯切割优惠券。不要在IO 中编写代码,因为它可能是纯粹的。把事情简单化。

    但有时,您需要Monad 的额外功能。一个明确的迹象是,当您需要根据目前已计算的内容更改计算过程时。在解析方面,这意味着根据到目前为止已经解析的内容来确定如何解析接下来的内容;换句话说,您可以通过这种方式构建上下文相关的语法。

    【讨论】:

    • 不,“使用最简单的工具”似乎是一个很好的经验法则,但实际上并非如此。例如,我们用电脑写信,但是电脑对一张纸就像一把台锯和一把剪刀。
    • 我的意思是,每一种选择总是有利有弊,但仅仅简单是一个糟糕的选择基础。 尤其是当您决定是否使用 Haskell 时。 :D
    • 是的,你是对的。最好这样说,“正确的工具是效率最高、复杂性最低的工具”。我的描述中缺少关于效率的部分:您需要一个足够强大的工具,不仅可以完成工作,而且可以使工作尽可能简单。但与此同时,您也不希望工具有很多不适用于手头任务的花里胡哨,因为这些很可能会增加操作的复杂性而无济于事。
    【解决方案4】:

    借助 Parsec,使用 Applicative 的好处就是风格。 Monad 的优势在于它更强大——你可以实现上下文相关的解析器。

    Doaitse Swierstra 的 UU 解析如果仅用于应用程序,效率会更高。

    【讨论】:

    • ISTR 表示,因为 Haskell 允许无限语法,monad 实际上并没有增加可识别语言的数量。
    • @luqui 我很好奇你的评论。这是一种语言。字母表是 Haskell Strings,语言是所有字母都相等的单词集合。作为一个单子解析器,这非常容易:option [] (anyToken &gt;&gt;= many . exactToken)(这里的anyTokenexactToken 实际上并不是 Parsec 库的一部分,但可能应该是;问我是否不确定他们做了什么)。相应的应用解析器看起来如何?
    • @stephen,你能给上下文敏感的解析器一个参考吗?我很好奇一元解析器和应用解析器的确切力量是什么。
    • @sdcvvc:this paper 讨论了箭头解析器与单子解析器的相对优势,并指出单子解析器可以解析上下文相关的语法,而箭头则不能。我相信应用解析器严格来说不如箭头解析器强大。
    • 这篇文章(和 cmets)解释了应用解析器允许解析所有递归可枚举的语言:byorgey.wordpress.com/2012/01/05/…
    【解决方案5】:

    Monad 严格来说是一种比 Applicatives 更具功能性的抽象。你可以写

    instance (Monad m) => Applicative m where
      pure  = return
      (<*>) = ap
    

    但是没有办法写

    instance (Applicative a) => Monad a where
      return = pure
      (>>=) = ???
    

    所以是的,这本质上是风格的问题。我想如果你使用returnap,那么使用pure&lt;*&gt; 应该不会有性能损失 因为Applicative 是一个比Monad 更小的接口。 ,这意味着&lt;*&gt; 有时可以比ap 得到更高的优化。 (但通过巧妙的 GHC 重写规则,通常可以实现相同的优化。)

    monadic 解析出来了吗?

    由于 Monad 是 Applicatives 的子集,我会得出结论,applicative 解析是 monadic 解析的子集。

    【讨论】:

    • 你的意思是说 Monads 是 Applicatives 的 superset 吗?
    • @Guildenstern Monadic 操作是 Applicative 操作的超集。但换一种说法:有Monad 实例的类型是有Applicative 实例的类型的子集。当谈到“Monads”和“Applicatives”时,通常指的是类型,而不是操作。
    • “我想如果你使用returnap,那么使用pure&lt;*&gt;应该不会有性能损失。” IIRC 并非如此。有很多情况(解析器只是其中之一)&lt;*&gt; 的性能优于 ap
    • @semicolon 你说得对;我已经相应地更新了我的答案。
    猜你喜欢
    • 2015-09-29
    • 1970-01-01
    • 1970-01-01
    • 2021-03-21
    • 1970-01-01
    • 2011-11-26
    • 2023-03-22
    • 2020-01-01
    • 2010-12-20
    相关资源
    最近更新 更多