特定领域语言(Domain-specific languages,DSL)已经成为一个热门话题;围绕函数性语言讨论得最多的话题是构建这种语言的能力。在构建了 AST 模式和基本前端解析器 之后(用于获取文本和生成适合解释的对象图形),作者在这篇文章中将这些知识无缝地整合起来(虽然有点麻烦)。然后他将推荐一些适合 DSL 语言及其解释器的扩展。
欢迎勇于探索的读者回到我们的系列文章中!本月继续探索 Scala 的语言和库支持,我们将改造一下计算器 DSL 并最终 “完成它”。DSL 本身有点简单 — 一个简单的计算器,目前为止只支持 4 个基本数学运算符。但要记住,我们的目标是创建一些可扩展的、灵活的对象,并且以后可以轻松增强它们以支持新的功能。
继续上次的讨论……
说明一下,目前我们的 DSL 有点零乱。我们有一个抽象语法树(Abstract Syntax Tree ),它由大量 case 类组成……
清单 1. 后端(AST)
package com.tedneward.calcdsl
{
// ...
private[calcdsl] abstract class Expr
private[calcdsl] case class Variable(name : String) extends Expr
private[calcdsl] case class Number(value : Double) extends Expr
private[calcdsl] case class UnaryOp(operator : String, arg : Expr) extends Expr
private[calcdsl] case class BinaryOp(operator : String, left : Expr, right : Expr)
extends Expr
}
……对此我们可以提供类似解释器的行为,它能最大限度地简化数学表达式……
清单 2. 后端(解释器)
package com.tedneward.calcdsl
{
// ...
object Calc
{
def simplify(e: Expr): Expr = {
// first simplify the subexpressions
val simpSubs = e match {
// Ask each side to simplify
case BinaryOp(op, left, right) => BinaryOp(op, simplify(left), simplify(right))
// Ask the operand to simplify
case UnaryOp(op, operand) => UnaryOp(op, simplify(operand))
// Anything else doesn't have complexity (no operands to simplify)
case _ => e
}
// now simplify at the top, assuming the components are already simplified
def simplifyTop(x: Expr) = x match {
// Double negation returns the original value
case UnaryOp("-", UnaryOp("-", x)) => x
// Positive returns the original value
case UnaryOp("+", x) => x
// Multiplying x by 1 returns the original value
case BinaryOp("*", x, Number(1)) => x
// Multiplying 1 by x returns the original value
case BinaryOp("*", Number(1), x) => x
// Multiplying x by 0 returns zero
case BinaryOp("*", x, Number(0)) => Number(0)
// Multiplying 0 by x returns zero
case BinaryOp("*", Number(0), x) => Number(0)
// Dividing x by 1 returns the original value
case BinaryOp("/", x, Number(1)) => x
// Dividing x by x returns 1
case BinaryOp("/", x1, x2) if x1 == x2 => Number(1)
// Adding x to 0 returns the original value
case BinaryOp("+", x, Number(0)) => x
// Adding 0 to x returns the original value
case BinaryOp("+", Number(0), x) => x
// Anything else cannot (yet) be simplified
case e => e
}
simplifyTop(simpSubs)
}
def evaluate(e : Expr) : Double =
{
simplify(e) match {
case Number(x) => x
case UnaryOp("-", x) => -(evaluate(x))
case BinaryOp("+", x1, x2) => (evaluate(x1) + evaluate(x2))
case BinaryOp("-", x1, x2) => (evaluate(x1) - evaluate(x2))
case BinaryOp("*", x1, x2) => (evaluate(x1) * evaluate(x2))
case BinaryOp("/", x1, x2) => (evaluate(x1) / evaluate(x2))
}
}
}
}
……我们使用了一个由 Scala 解析器组合子构建的文本解析器,用于解析简单的数学表达式……
清单 3. 前端
package com.tedneward.calcdsl
{
// ...
object Calc
{
object ArithParser extends JavaTokenParsers
{
def expr: Parser[Any] = term ~ rep("+"~term | "-"~term)
def term : Parser[Any] = factor ~ rep("*"~factor | "/"~factor)
def factor : Parser[Any] = floatingPointNumber | "("~expr~")"
def parse(text : String) =
{
parseAll(expr, text)
}
}
// ...
}
}
……但在进行解析时,由于解析器组合子当前被编写为返回 Parser[Any] 类型,所以会生成 String 和 List 集合,实际上应该让解析器返回它需要的任意类型(我们可以看到,此时是一个 String 和 List 集合)。
要让 DSL 成功,解析器需要返回 AST 中的对象,以便在解析完成时,执行引擎可以捕获该树并对它执行 evaluate()。对于该前端,我们需要更改解析器组合子实现,以便在解析期间生成不同的对象。