Scala 解析器组合器入门

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

Scala 提供了一种非常简单的方法来设计您自己的编程语言,使用它的解析器库。这使得创建您自己的领域特定语言(即 DSL)或解释语言比您想象的更容易。作为入门,让我们编写一个解析器来解析简单的数学表达式,例如“1+9*8”和“4*6/2-5”。

对于那些熟悉语言设计的人来说,这种语言的 EBNF 语法看起来像这样:



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?


为了开始使用 Scala 解析库编写解析器,我们编写了一个扩展 Parsers 特性的类。这是一个扩展 RegexParsers 的类的示例, 它是 Parsers 的子特征。



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?


有效标记的 Scala 定义与 EBNF 语法中的定义之间的唯一区别如下:

  • Scala 在每个标记之间使用“~”
  • 而不是使用“?”就像您在 EBNF 语法中一样,Scala 使用关键字“opt”

要执行我们的解析器,我们只需调用作为 Parsers 特性一部分的继承的 parse 方法。



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?


此 println 的结果将是:

(9~一些((*~(8~一些((+~(21~一些((/~(7~无))))))))))


我们完成了!好吧,不是真的。现在的输出是 Scala 看到解析器操作结果的方式。为了使我们的语言更有意义,让我们添加一些 Scala 代码来计算算术运算并将结果打印到输出。


让我们开始通过检查什么来计算结果 "(9~Some((*~(8~Some((+~(21~Some((/~(7~None)))))))))) " 真正意味着在 Scala 的世界里。让我们看一下这个字符串的子集,“(9~Some((*~(8~None))))”。这是解析“9*8”的结果。看起来有趣的第一部分是“9~Some(...)”。在我们的解析器中,我们定义了以下规则:



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?



很明显,“number”的计算结果为“9”,并且“~”被逐字打印出来,您应该记得它在 Scala 解析器中用于连接部分语法。然而,“Some(...)”是怎么回事?好吧,每当 Scala 解析 opt(x) 语句时,它都会将其评估为 Some(...) 或 None,这两者都是 Option 的子类。这是有道理的...... opt(x) 语句评估为一个选项。

让我们看一下将解析器结果转换成更有用的东西,而不是让我们的解析器返回一堆 ~ 和选项。再次查看我们当前的解析器规则:



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?



我们需要修改这个解析器定义,让它返回一个 Int 而不是 Any。我们还需要计算算术运算的结果。我们的语法规则允许单个数字或数字后跟算术运算符和另一个数字。如果我们正在处理单个数字,我们需要告诉解析器将结果转换为 Int。为此,我们对解析器规则进行以下修改:



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?


^^ 只是告诉解析器执行它后面的代码,包含在 {...} 中。我们所做的只是将其转换为 Int。

接下来,我们需要告诉解析器当它遇到一个数字时,或者当它遇到一个数字后跟一个运算符和另一个数字时该怎么做。为此,我们需要为每种情况定义整数运算(单个整数值、两个值的加法、两个值的减法、两个值的除法和两个值的乘法)。



 digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
number ::= digit | digit number
operator ::= "+" | "-" | "*" | "/"
expr ::= number (operator expr)?


我们正在处理五个案例。第一种情况是我们只有一个整数 (a ~ None)。当我们有一个 Int 后面有 None 时,我们只需按原样评估整数值。第二种情况是当我们有一个整数乘以另一个整数时 (a ~ Some("*" ~ b))。在这种情况下,我们只需执行 a * b。然后我们继续定义除法、加法和减法的规则。


本教程的主要内容是:

  • 您在 Parser[ ] 定义的括号内定义解析器规则返回的类型。在这个例子中,它是一个 Int。
  • 您可以添加自定义 Scala 代码以使用 ^^ { ... } 对解析器结果进行操作


现在我们已经为 Scala 解析器组合器奠定了基础,我们可以在这些特性的基础上构建一个功能齐全的解释型语言,其中包含 if-else 条件、循环,甚至函数调用。

这是一篇关于如何使用这种方法创建功能齐全的解释型语言的文章: https://dzone.com/articles/create-a-programming-language-with-scala-parser-co