使用 Scala 解析器的运算符关联性

Posted

技术标签:

【中文标题】使用 Scala 解析器的运算符关联性【英文标题】:Operator associativity using Scala Parsers 【发布时间】:2012-06-07 23:57:19 【问题描述】:

所以我一直在尝试用 Scala 的解析器编写一个计算器,这很有趣,除了我发现运算符关联性是倒退的,而且当我尝试让我的语法左递归时,即使它是完全明确的,我得到一个堆栈溢出。

澄清一下,如果我有这样的规则: def 减法:Parser[Int] = num ~ "-" ~ add x => x._1._1 - x._2 然后评估 7 - 4 - 3 的结果是 6 而不是 0。

我实际实现的方式是我正在组成一个二叉树,其中运算符是非叶节点,叶节点是数字。我评估树的方式是左孩子(运算符)右孩子。在为 7 - 4 - 5 构建树时,我希望它看起来像:

-
-   5
7   4   NULL   NULL

其中 - 是根,它的子节点是 - 和 5,第二个 - 的子节点是 7 和 4。

但是,我唯一可以轻松构建的树是

-
7   -
NULL   NULL   4   5

这是不同的,不是我想要的。

基本上,简单的括号是 7 - (4 - 5) 而我想要 (7 - 4) - 5。

我怎样才能破解这个?我觉得无论如何我都应该能够编写一个具有正确运算符优先级的计算器。我应该先对所有内容进行标记,然后再反转我的标记吗?我可以通过取右孩子的所有左孩子并使他们成为右孩子父母的右孩子并使父母成为前右孩子的左孩子来翻转我的树吗?它似乎很适合第一个近似值,但我并没有真正考虑过它。我觉得一定有一些我失踪的案例。

我的印象是我只能用 scala 解析器制作 LL 解析器。如果你知道另一种方法,请告诉我!

【问题讨论】:

请更具体地说明您所说的“运算符关联性是向后的”。 顺便说一句,结帐scala-dist 以获取更多示例——我只是使用该链接编辑我的答案。 【参考方案1】:

Scala 的解析器组合器的标准实现(Parsers 特征)不支持左递归语法。但是,如果您需要左递归,您可以使用PackratParsers。也就是说,如果您的语法是一个简单的算术表达式解析器,那么您绝对不需要左递归。

编辑

有一些方法可以使用右递归并仍然保持左结合性,如果你热衷于此,只需查找算术表达式和递归下降解析器。当然,正如我所说,您可以使用PackratParsers,它允许左递归。

但不使用PackratParsers 处理关联性的最简单方法是避免使用递归。只需使用其中一个重复运算符来获取List,然后根据需要使用foldLeftfoldRight。简单例子:

trait Tree
case class Node(op: String, left: Tree, right: Tree) extends Tree
case class Leaf(value: Int) extends Tree

import scala.util.parsing.combinator.RegexParsers

object P extends RegexParsers 
  def expr = term ~ (("+" | "-") ~ term).* ^^ mkTree
  def term = "\\d+".r ^^ (_.toInt)
  def mkTree(input: Int ~ List[String ~ Int]): Tree = input match 
    case first ~ rest => ((Leaf(first): Tree) /: rest)(combine)
  
  def combine(acc: Tree, next: String ~ Int) = next match 
    case op ~ y => Node(op, acc, Leaf(y))
  

您可以在 scala-dist 存储库中找到其他更完整的示例。

【讨论】:

没有左递归怎么办?另外,我的印象是,默认的 Scala 解析库从左到右评估并且是左递归的,因此是 LL,如果不是 LL(k)。 @nnythm:实际上,你是对的。默认的 Scala 解析库是递归下降解析器,因此是 LL(k),尽管我不知道 Scala 的解析器组合器的 k 是什么。 LL(k) 文法不能处理左递归。可以处理左递归的是 LR 解析器,Scala 的解析器组合器不是 LR 解析器。 对,我的意思是它们生成最左边的推导,而不是它们是左递归的。 这里是思维模式解析器import scala.util.parsing.combinator._ object SO JavaTokenParsers with PackratParsers lazy val left: Parser[String] = left ~ ("+" ~> ident) ^^ case a1 ~ a2 => s"Sum($a1,$a2)" | ident ; println(parseAll(left, "a+b+c+d"))。尽管有lazy val,为什么它会堆栈溢出?【参考方案2】:

我将您的问题解释如下:

如果你写像def expression = number ~ "-" ~ expression这样的规则,然后在语法树的每个节点上求值,那么你会发现在3 - 5 - 4中,5 - 4首先被计算,结果是1,然后是3 - 1计算结果为 2。

另一方面,如果你写像def expression = expression ~ "-" ~ number这样的规则,规则是左递归的,会溢出堆栈。

这个问题有三种解决方案:

    对抽象语法树进行后处理,将其从右关联树转换为左关联树。如果您使用语法规则上的操作来立即进行计算,这对您不起作用。

    将规则定义为def expression = repsep(number, "-"),然后在评估计算时,在任何方向上循环解析的数字(将出现在平面列表中)为您提供所需的关联性。如果会出现一种以上的运算符,则不能使用它,因为运算符将被丢弃。

    将规则定义为def expression = number ~ ( "-" ~ number) *。您将拥有一个初始数字,以及一组平面列表中的操作符-数字对,以便按您想要的任何方向进行处理(尽管在这里从左到右可能更容易)。

    按照 Daniel Sobral 的建议使用 PackratParsers。这可能是您最好和最简单的选择。

【讨论】:

在进行任何评估之前,我正在构建树。我可以将右关联树转换为左关联树吗?我在网上找不到任何关于它的文献,尽管它在我的脑海中似乎运作良好。 PackratParsers 还在我的左递归中给了我堆栈溢出,所以我想如果它是正确的,我会去转换树。 @nnythm 您可能没有将 Packrat 解析器声明为 lazy val,而是声明为 defdef 与传统的解析器组合器一起使用,lazy val 与 Packrat 解析器一起使用。实际上,def 只是启用了前向引用和递归而没有问题,lazy val 也以很小的性能代价做到了这一点。在没有前向引用或递归的语法中,您可以将所有内容声明为val @Daniel:老实说,我不知道是否不可能创建 LL(k) parserc 组合器,或者它是否只是未实现。如果您知道这是不可能的,请随时删除“(当前)”,但请记住,我还进行了一些其他编辑以澄清您的答案,因此除非它们也错了,否则不要删除它们。 @Daniel:在对 Wikipedia 进行了一些研究之后,我认为到目前为止我们提到的所有 LL 解析器,实际上是指 LR 解析器? @KenBloom 看起来像。我将相应地编辑我的答案。

以上是关于使用 Scala 解析器的运算符关联性的主要内容,如果未能解决你的问题,请参考以下文章

Scala 中的右关联方法有啥好处?

组织多个 scala 相互关联的 sbt 和 git 项目 - 最佳实践建议

pyspark vs scala中的FPgrowth计算关联

Jmeter正则表达提取器的使用(提取一组有关联的参数)

运算符关联性,优先级

如何在垃圾邮件过滤中嵌入带有朴素贝叶斯分类器的关联规则?