欢迎勇于探索的读者回到我们的系列文章中!本月继续探索 Scala 的语言和库支持,我们将改造一下计算器 DSL 并最终 “完成它”。DSL 本身有点简单 — 一个简单的计算器,目前为止只支持 4 个基本数学运算符。但要记住,我们的目标是创建一些可扩展的、灵活的对象,并且以后可以轻松增强它们以支持新的功能。
继续上次的讨论……
说明一下,目前我们的 DSL 有点零乱。我们有一个抽象语法树(Abstract Syntax Tree ),它由大量 case 类组成……
清单 1. 后端(AST)
……对此我们可以提供类似解释器的行为,它能最大限度地简化数学表达式……
清单 2. 后端(解释器)
……我们使用了一个由 Scala 解析器组合子构建的文本解析器,用于解析简单的数学表达式……
清单 3. 前端
……但在进行解析时,由于解析器组合子当前被编写为返回 Parser[Any] 类型,所以会生成 String 和 List 集合,实际上应该让解析器返回它需要的任意类型(我们可以看到,此时是一个 String 和 List 集合)。
要让 DSL 成功,解析器需要返回 AST 中的对象,以便在解析完成时,执行引擎可以捕获该树并对它执行 evaluate()。对于该前端,我们需要更改解析器组合子实现,以便在解析期间生成不同的对象。
清理语法
对解析器做的第一个更改是修改其中一个语法。在原来的解析器中,可以接受像 “5 + 5 + 5” 这样的表达式,因为语法中为表达式(expr)和术语(term)定义了 rep() 组合子。但如果考虑扩展,这可能会引起一些关联性和操作符优先级问题。以后的运算可能会要求使用括号来显式给出优先级,以避免这类问题。因此第一个更改是将语法改为要求在所有表达式中加 “()”。
回想一下,这应该是我一开始就需要做的事情;事实上,放宽限制通常比在以后添加限制容易(如果最后不需要这些限制),但是解决运算符优先级和关联性问题比这要困难得多。如果您不清楚运算符的优先级和关联性;那么让我大致概述一下我们所处的环境将有多复杂。考虑 Java 语言本身和它支持的各种运算符(如 Java 语言规范中所示)或一些关联性难题(来自 Bloch 和 Gafter 提供的 Java Puzzlers),您将发现情况不容乐观。
因此,我们需要逐步解决问题。首先是再次测试语法:
清单 4. 采用括号
我已经将旧的解析器重命名为 OldAnyParser,添加左边的部分是为了便于比较;新的语法由 AnyParser 给出;注意它将 expr 定义为 term + term、term - term,或者一个独立的 term,等等。另一个大的变化是 factor 的定义,现在它使用另一种组合子 ~> 和 <~ 在遇到 ( 和 ) 字符时有效地抛出它们。
因为这只是一个临时步骤,所以我不打算创建一系列单元测试来查看各种可能性。不过我仍然想确保该语法的解析结果符合预期,所以我在这里编写一个不是很正式的测试:
清单 5. 测试解析器的非正式测试
请记住,这纯粹是出于教学目的(也许有人会说我不想为产品代码编写测试,但我确实没有在编写产品代码,所以我不需要编写正式的测试。这只是为了方便教学)。但是,运行这个测试后,得到的许多结果与标准单元测试结果文件相符,表明没有括号的表达式(5 + 5 + 5)执行失败,而有括号的表达式则会执行成功。真是不可思议!
不要忘了给解析测试加上注释。更好的方法是将该测试完全删除。这是一个临时编写的测试,而且我们都知道,真正的 Jedi 只在研究或防御时使用这些源代码,而不在这种情况中使用。
清理语法
现在我们需要再次更改各种组合子的定义。回顾一下上一篇文章,expr、term 和 factor 函数中的每一个实际上都是 BNF 语句,但注意每一个函数返回的都是一个解析器泛型,参数为 Any(Scala 类型系统中一个基本的超类型,从其名称就可以知道它的作用:指示可以包含任何对象的潜在类型或引用);这表明组合子可以根据需要返回任意类型。我们已经看到,在默认情况下,解析器可以返回一个 String,也可以返回一个 List(如果您还不信的话,可以在运行的测试中加入临时测试。这也会看到同样的结果)。
要将它更改为生成 case 类 AST 层次结构的实例(Expr 对象),组合子的返回类型必须更改为 Parser[Expr]。如果让它自行更改,编译将会失败;这三个组合子知道如何获取 String,但不知道如何根据解析的内容生成 Expr 对象。为此,我们使用了另一个组合子,即 ^^ 组合子,它以一个匿名函数为参数,将解析的结果作为一个参数传递给该匿名函数。
如果您和许多 Java 开发人员一样,那么就要花一点时间进行解析,让我们查看一下实际效果:
清单 6. 产品组合子
^^ 组合子接收一个匿名函数,其解析结果(例如,假设输入的是 5 + 5,那么解析结果将是 ((5~+)~5))将会被单独传递并得到一个对象 — 在本例中,是一个适当类型的 BinaryObject。请再次注意模式匹配的强大功能;我将表达式的左边部分与 lhs 实例绑定在一起,将 + 部分与(未使用的)plus 实例绑定在一起,该表达式的右边则与 rhs 绑定,然后我分别使用 lhs 和 rhs 填充 BinaryOp 构造函数的左边和右边。
现在运行代码(记得注释掉临时测试),单元测试集会再次产生所有正确的结果:我们以前尝试的各种表达式不会再失败,因为现在解析器生成了派生 Expr 对象。前面已经说过,不进一步测试解析器是不负责任的,所以让我们添加更多的测试(包括我之前在解析器中使用的非正式测试):
清单 7. 测试解析器(这次是正式的)
读者可以再增加一些测试,因为我可能漏掉一些不常见的情况(与 Internet 上的其他人结对编程是比较好的)。
完成最后一步
假设解析器正按照我们想要的方式在工作 — 即生成 AST — 那么现在只需要根据 AST 对象的计算结果来完善解析器。这很简单,只需向 Calc 添加代码,如清单 8 所示……
清单 8. 真的完成啦!
……同时添加一个简单的测试,确保 evaluate("1+1") 返回 2.0……
清单 9. 最后,看一下 1 + 1 是否等于 2
……然后运行它,一切正常!
扩展 DSL 语言
如果完全用 Java 代码编写同一个计算器 DSL,而没有碰到我遇到的问题(在不构建完整的 AST 的情况下递归式地计算每一个片段,等等),那么似乎它是另一种能够解决问题的语言或工具。但以这种方式构建语言的强大之处会在扩展性上得到体现。
例如,我们向这种语言添加一个新的运算符,即 ^ 运算符,它将执行求幂运算;也就是说,2 ^ 2 等于 2 的平方 或 4。向 DSL 语言添加这个运算符需要一些简单步骤。
首先,您必须考虑是否需要更改 AST。在本例中,求幂运算符是另一种形式的二进制运算符,所以使用现有 BinaryOp case 类就可以。无需对 AST 进行任何更改。
其次,必须修改 evaluate 函数,以使用 BinaryOp("^", x, y) 执行正确的操作;这很简单,只需添加一个嵌套函数(因为不必在外部看到这个函数)来实际计算指数,然后向模式匹配添加必要的代码行,如下所示:
清单 10. 稍等片刻
注意,这里我们只使用 6 行代码就有效地向系统添加了求幂运算,同时没有对 Calc 类进行任何表面更改。这就是封装!
(在我努力创建最简单求幂函数时,我故意创建了一个有严重 bug 的版本 —— 这是为了让我们关注语言,而不是实现。也就是说,看看哪位读者能够找到 bug。他可以编写发现 bug 的单元测试,然后提供一个无 bug 的版本)。
但是在向解析器添加这个求幂函数之前,让我们先测试这段代码,以确保求幂部分能正常工作:
清单 11. 求平方
运行这段代码确保可以求幂(忽略我之前提到的 bug),这样就完成了一半的工作。
最后一个更改是修改语法,让它接受新的求幂运算符;因为求幂的优先级与乘法和除法的相同,所以最简单的做法是将它放在 term 组合子中:
清单 12. 完成了,这次是真的!
当然,我们需要对这个解析器进行一些测试……
清单 13. 再求平方
……运行并通过后,还要进行最后一个测试,看一切是否能正常工作:
清单 14. 从 String 到平方
成功啦!
继续上次的讨论……
说明一下,目前我们的 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()。对于该前端,我们需要更改解析器组合子实现,以便在解析期间生成不同的对象。
清理语法
对解析器做的第一个更改是修改其中一个语法。在原来的解析器中,可以接受像 “5 + 5 + 5” 这样的表达式,因为语法中为表达式(expr)和术语(term)定义了 rep() 组合子。但如果考虑扩展,这可能会引起一些关联性和操作符优先级问题。以后的运算可能会要求使用括号来显式给出优先级,以避免这类问题。因此第一个更改是将语法改为要求在所有表达式中加 “()”。
回想一下,这应该是我一开始就需要做的事情;事实上,放宽限制通常比在以后添加限制容易(如果最后不需要这些限制),但是解决运算符优先级和关联性问题比这要困难得多。如果您不清楚运算符的优先级和关联性;那么让我大致概述一下我们所处的环境将有多复杂。考虑 Java 语言本身和它支持的各种运算符(如 Java 语言规范中所示)或一些关联性难题(来自 Bloch 和 Gafter 提供的 Java Puzzlers),您将发现情况不容乐观。
因此,我们需要逐步解决问题。首先是再次测试语法:
清单 4. 采用括号
package com.tedneward.calcdsl { // ... object Calc { // ... object OldAnyParser 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) } } object AnyParser extends JavaTokenParsers { def expr: Parser[Any] = (term~"+"~term) | (term~"-"~term) | term def term : Parser[Any] = (factor~"*"~factor) | (factor~"/"~factor) | factor def factor : Parser[Any] = "(" ~> expr <~ ")" | floatingPointNumber def parse(text : String) = { parseAll(expr, text) } } // ... } }
我已经将旧的解析器重命名为 OldAnyParser,添加左边的部分是为了便于比较;新的语法由 AnyParser 给出;注意它将 expr 定义为 term + term、term - term,或者一个独立的 term,等等。另一个大的变化是 factor 的定义,现在它使用另一种组合子 ~> 和 <~ 在遇到 ( 和 ) 字符时有效地抛出它们。
因为这只是一个临时步骤,所以我不打算创建一系列单元测试来查看各种可能性。不过我仍然想确保该语法的解析结果符合预期,所以我在这里编写一个不是很正式的测试:
清单 5. 测试解析器的非正式测试
package com.tedneward.calcdsl.test { class CalcTest { import org.junit._, Assert._ // ... _cnnew1@Test def parse = { import Calc._ val expressions = List( "5", "(5)", "5 + 5", "(5 + 5)", "5 + 5 + 5", "(5 + 5) + 5", "(5 + 5) + (5 + 5)", "(5 * 5) / (5 * 5)", "5 - 5", "5 - 5 - 5", "(5 - 5) - 5", "5 * 5 * 5", "5 / 5 / 5", "(5 / 5) / 5" ) for (x <- expressions) System.out.println(x + " = " + AnyParser.parse(x)) } } }
请记住,这纯粹是出于教学目的(也许有人会说我不想为产品代码编写测试,但我确实没有在编写产品代码,所以我不需要编写正式的测试。这只是为了方便教学)。但是,运行这个测试后,得到的许多结果与标准单元测试结果文件相符,表明没有括号的表达式(5 + 5 + 5)执行失败,而有括号的表达式则会执行成功。真是不可思议!
不要忘了给解析测试加上注释。更好的方法是将该测试完全删除。这是一个临时编写的测试,而且我们都知道,真正的 Jedi 只在研究或防御时使用这些源代码,而不在这种情况中使用。
清理语法
现在我们需要再次更改各种组合子的定义。回顾一下上一篇文章,expr、term 和 factor 函数中的每一个实际上都是 BNF 语句,但注意每一个函数返回的都是一个解析器泛型,参数为 Any(Scala 类型系统中一个基本的超类型,从其名称就可以知道它的作用:指示可以包含任何对象的潜在类型或引用);这表明组合子可以根据需要返回任意类型。我们已经看到,在默认情况下,解析器可以返回一个 String,也可以返回一个 List(如果您还不信的话,可以在运行的测试中加入临时测试。这也会看到同样的结果)。
要将它更改为生成 case 类 AST 层次结构的实例(Expr 对象),组合子的返回类型必须更改为 Parser[Expr]。如果让它自行更改,编译将会失败;这三个组合子知道如何获取 String,但不知道如何根据解析的内容生成 Expr 对象。为此,我们使用了另一个组合子,即 ^^ 组合子,它以一个匿名函数为参数,将解析的结果作为一个参数传递给该匿名函数。
如果您和许多 Java 开发人员一样,那么就要花一点时间进行解析,让我们查看一下实际效果:
清单 6. 产品组合子
package com.tedneward.calcdsl { // ... object Calc { object ExprParser extends JavaTokenParsers { def expr: Parser[Expr] = (term ~ "+" ~ term) ^^ { case lhs~plus~rhs => BinaryOp("+", lhs, rhs) } | (term ~ "-" ~ term) ^^ { case lhs~minus~rhs => BinaryOp("-", lhs, rhs) } | term def term: Parser[Expr] = (factor ~ "*" ~ factor) ^^ { case lhs~times~rhs => BinaryOp("*", lhs, rhs) } | (factor ~ "/" ~ factor) ^^ { case lhs~div~rhs => BinaryOp("/", lhs, rhs) } | factor def factor : Parser[Expr] = "(" ~> expr <~ ")" | floatingPointNumber ^^ {x => Number(x.toFloat) } def parse(text : String) = parseAll(expr, text) } def parse(text : String) = ExprParser.parse(text).get // ... } // ... }
^^ 组合子接收一个匿名函数,其解析结果(例如,假设输入的是 5 + 5,那么解析结果将是 ((5~+)~5))将会被单独传递并得到一个对象 — 在本例中,是一个适当类型的 BinaryObject。请再次注意模式匹配的强大功能;我将表达式的左边部分与 lhs 实例绑定在一起,将 + 部分与(未使用的)plus 实例绑定在一起,该表达式的右边则与 rhs 绑定,然后我分别使用 lhs 和 rhs 填充 BinaryOp 构造函数的左边和右边。
现在运行代码(记得注释掉临时测试),单元测试集会再次产生所有正确的结果:我们以前尝试的各种表达式不会再失败,因为现在解析器生成了派生 Expr 对象。前面已经说过,不进一步测试解析器是不负责任的,所以让我们添加更多的测试(包括我之前在解析器中使用的非正式测试):
清单 7. 测试解析器(这次是正式的)
package com.tedneward.calcdsl.test { class CalcTest { import org.junit._, Assert._ // ... @Test def parseAnExpr1 = assertEquals( Number(5), Calc.parse("5") ) @Test def parseAnExpr2 = assertEquals( Number(5), Calc.parse("(5)") ) @Test def parseAnExpr3 = assertEquals( BinaryOp("+", Number(5), Number(5)), Calc.parse("5 + 5") ) @Test def parseAnExpr4 = assertEquals( BinaryOp("+", Number(5), Number(5)), Calc.parse("(5 + 5)") ) @Test def parseAnExpr5 = assertEquals( BinaryOp("+", BinaryOp("+", Number(5), Number(5)), Number(5)), Calc.parse("(5 + 5) + 5") ) @Test def parseAnExpr6 = assertEquals( BinaryOp("+", BinaryOp("+", Number(5), Number(5)), BinaryOp("+", Number(5), Number(5))), Calc.parse("(5 + 5) + (5 + 5)") ) // other tests elided for brevity } }
读者可以再增加一些测试,因为我可能漏掉一些不常见的情况(与 Internet 上的其他人结对编程是比较好的)。
完成最后一步
假设解析器正按照我们想要的方式在工作 — 即生成 AST — 那么现在只需要根据 AST 对象的计算结果来完善解析器。这很简单,只需向 Calc 添加代码,如清单 8 所示……
清单 8. 真的完成啦!
package com.tedneward.calcdsl { // ... object Calc { // ... def evaluate(text : String) : Double = evaluate(parse(text)) } }
……同时添加一个简单的测试,确保 evaluate("1+1") 返回 2.0……
清单 9. 最后,看一下 1 + 1 是否等于 2
package com.tedneward.calcdsl.test { class CalcTest { import org.junit._, Assert._ // ... @Test def add1 = assertEquals(Calc.evaluae("1 + 1"), 2.0) } }
……然后运行它,一切正常!
扩展 DSL 语言
如果完全用 Java 代码编写同一个计算器 DSL,而没有碰到我遇到的问题(在不构建完整的 AST 的情况下递归式地计算每一个片段,等等),那么似乎它是另一种能够解决问题的语言或工具。但以这种方式构建语言的强大之处会在扩展性上得到体现。
例如,我们向这种语言添加一个新的运算符,即 ^ 运算符,它将执行求幂运算;也就是说,2 ^ 2 等于 2 的平方 或 4。向 DSL 语言添加这个运算符需要一些简单步骤。
首先,您必须考虑是否需要更改 AST。在本例中,求幂运算符是另一种形式的二进制运算符,所以使用现有 BinaryOp case 类就可以。无需对 AST 进行任何更改。
其次,必须修改 evaluate 函数,以使用 BinaryOp("^", x, y) 执行正确的操作;这很简单,只需添加一个嵌套函数(因为不必在外部看到这个函数)来实际计算指数,然后向模式匹配添加必要的代码行,如下所示:
清单 10. 稍等片刻
package com.tedneward.calcdsl { // ... object Calc { // ... def evaluate(e : Expr) : Double = { def exponentiate(base : Double, exponent : Double) : Double = if (exponent == 0) 1.0 else base * exponentiate(base, exponent - 1) 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)) case BinaryOp("^", x1, x2) => exponentiate(evaluate(x1), evaluate(x2)) } } } }
注意,这里我们只使用 6 行代码就有效地向系统添加了求幂运算,同时没有对 Calc 类进行任何表面更改。这就是封装!
(在我努力创建最简单求幂函数时,我故意创建了一个有严重 bug 的版本 —— 这是为了让我们关注语言,而不是实现。也就是说,看看哪位读者能够找到 bug。他可以编写发现 bug 的单元测试,然后提供一个无 bug 的版本)。
但是在向解析器添加这个求幂函数之前,让我们先测试这段代码,以确保求幂部分能正常工作:
清单 11. 求平方
package com.tedneward.calcdsl.test { class CalcTest { // ... @Test def evaluateSimpleExp = { val expr = BinaryOp("^", Number(4), Number(2)) val results = Calc.evaluate(expr) // (4 ^ 2) => 16 assertEquals(16.0, results) } @Test def evaluateComplexExp = { val expr = BinaryOp("^", BinaryOp("*", Number(2), Number(2)), BinaryOp("/", Number(4), Number(2))) val results = Calc.evaluate(expr) // ((2 * 2) ^ (4 / 2)) => (4 ^ 2) => 16 assertEquals(16.0, results) } } }
运行这段代码确保可以求幂(忽略我之前提到的 bug),这样就完成了一半的工作。
最后一个更改是修改语法,让它接受新的求幂运算符;因为求幂的优先级与乘法和除法的相同,所以最简单的做法是将它放在 term 组合子中:
清单 12. 完成了,这次是真的!
package com.tedneward.calcdsl { // ... object Calc { // ... object ExprParser extends JavaTokenParsers { def expr: Parser[Expr] = (term ~ "+" ~ term) ^^ { case lhs~plus~rhs => BinaryOp("+", lhs, rhs) } | (term ~ "-" ~ term) ^^ { case lhs~minus~rhs => BinaryOp("-", lhs, rhs) } | term def term: Parser[Expr] = (factor ~ "*" ~ factor) ^^ { case lhs~times~rhs => BinaryOp("*", lhs, rhs) } | (factor ~ "/" ~ factor) ^^ { case lhs~div~rhs => BinaryOp("/", lhs, rhs) } | (factor ~ "^" ~ factor) ^^ { case lhs~exp~rhs => BinaryOp("^", lhs, rhs) } | factor def factor : Parser[Expr] = "(" ~> expr <~ ")" | floatingPointNumber ^^ {x => Number(x.toFloat) } def parse(text : String) = parseAll(expr, text) } // ... } }
当然,我们需要对这个解析器进行一些测试……
清单 13. 再求平方
package com.tedneward.calcdsl.test { class CalcTest { // ... @Test def parseAnExpr17 = assertEquals( BinaryOp("^", Number(2), Number(2)), Calc.parse("2 ^ 2") ) @Test def parseAnExpr18 = assertEquals( BinaryOp("^", Number(2), Number(2)), Calc.parse("(2 ^ 2)") ) @Test def parseAnExpr19 = assertEquals( BinaryOp("^", Number(2), BinaryOp("+", Number(1), Number(1))), Calc.parse("2 ^ (1 + 1)") ) @Test def parseAnExpr20 = assertEquals( BinaryOp("^", Number(2), Number(2)), Calc.parse("2 ^ (2)") ) } }
……运行并通过后,还要进行最后一个测试,看一切是否能正常工作:
清单 14. 从 String 到平方
package com.tedneward.calcdsl.test { class CalcTest { // ... @Test def square1 = assertEquals(Calc.evaluate("2 ^ 2"), 4.0) } }
成功啦!
发表评论
-
Scala + Twitter = Scitter(scala代码学习第15天)
2011-04-08 09:11 879Twitter 迅速占领了 Interne ... -
面向 Java 开发人员的 Scala 指南: Scala 和 servlet(scala代码学习第十一天)
2011-04-02 07:40 740Scala 显然是一门有趣的语言,很适合体现语言理论和创新方面 ... -
scala代码学习构建计算器,第2 部分(代码学习第九天)
2011-03-31 10:53 812回忆一下我们的英雄所处的困境:在试图创建一个 DSL(这里只不 ... -
Scala构建计算器,第1 部分(代码学习第8天)
2011-03-30 11:59 1199特定于领域的语言 可能您无法(或没有时间)承受来自于您的项目 ... -
scala包和访问修饰符(代码学习第七天)
2011-03-29 15:51 1620系列的过程中我遗漏了 ... -
实现继承(代码学习第五天)
2011-03-26 10:13 969近十几年来,面向对象语言设计的要素一直是继承的核心。不支持继承 ... -
关于特征和行为(代码学习第四天)
2011-03-25 09:38 705著名科学家、研究学者 ... -
Scala 控制结构内部揭密(scala代码学习第三天)
2011-03-24 09:15 1313迄今为止,在此 系列 ... -
面向 Java 开发人员的 Scala 指南: 类操作(代码学习第2天)
2011-03-22 19:06 748第一天中只是些简单应用 ,您只是稍微了解了一些 Scala 语 ... -
programming in scala 2nd代码学习(第一天)
2011-03-22 18:42 941近来没事,拿出了原先学习scala的代码 书中代码噢、拿出自己 ... -
scalatra web框架快速搭建(官方使用文档)
2011-03-21 22:42 2523昨天写了个sbt构建scala项目的文章,就是为了今天的sca ... -
A build tool for Scala(simple-build-tool) sbt安装指南
2011-03-20 22:49 2207今天有位写框架的大哥叫我学一学scalatra框架,找了 ... -
Scala functional style deferent from java OOP(特点)
2011-03-20 17:34 988该程序通过一段斐波那契数列的计算,比较一下Scala的函数式编 ... -
Java 开发人员的 Scala 指南: 面向对象的函数编程
2011-03-20 11:59 1046函数概念 开始之前, ...
相关推荐
3. **构建DSL**:在计算器DSL的例子中,我们可能创建一系列操作符和表达式,使得写出如`5 + 3 * 2`这样的代码,就能被解析并执行相应的计算。这可以通过定义运算符重载、方法链以及使用`case class`来构造表达式树来...
通过这四周的学习,你将能够掌握 Scala 语言的基本概念,运用变量和控制结构来解决问题,提升算法能力,并能构建简单的后端应用程序。在 Scala-Backend-main 文件夹中,你可能会找到相关的代码示例和练习,这些资源...
《永磁无刷直流电机控制系统与软件综合研究——集成电机计算软件、电机控制器及电磁设计软件的创新设计与实践》,永磁无刷直流电机计算与控制软件:高效电机控制器与电磁设计工具,永磁无刷直流电机计算软件,电机控制器,无刷电机设计软件,电机电磁设计软件 ,永磁无刷直流电机计算软件; 电机控制器; 无刷电机设计软件; 电机电磁设计软件,无刷电机设计专家:永磁无刷直流电机计算与控制器设计软件
新能源汽车VCU开发模型及策略详解:从控制策略到软件设计全面解析,新能源汽车VCU开发模型及策略详解:从控制策略到软件设计全面解析,新能源汽车VCU开发模型及控制策略,MBD电控开发 新能源汽车大势所向,紧缺VCU电控开发工程师,特别是涉及新能源三电系统,工资仅仅低于无人驾驶、智能驾驶岗位。 ——含控制策略模型 整车控制策略详细文档 通讯协议文档 接口定义 软件设计说明文档 等(超详细,看懂VCU电控策略开发就通了) 内容如下: 新能源汽车整车控制器VCU学习模型,适用于初学者。 1、模型包含高压上下电,行驶模式管理,能量回馈,充电模式管理,附件管理,远程控制,诊断辅助功能。 2、软件说明书(控制策略说明书) 3、模型有部分中文注释 对想着手或刚开始学习整车控制器自动代码生成或刚接触整车控制器有很大帮助。 ,新能源汽车VCU开发模型; 控制策略; MBD电控开发; 模型学习; 代码生成; 整车控制器; 能量回馈; 诊断辅助功能,新能源汽车电控开发详解:VCU控制策略模型及学习手册
内容概要:本文详细介绍了两种利用 Python 读取 Excel 文件的不同方法,分别是基于 pandas 和 openpyxl。对于想要利用Python 处理 Excel 数据的读者来说,文中不仅提供了简洁明了的具体代码片段以及执行效果展示,还针对每个库的应用特性进行了深度解析。此外,文档提到了一些进阶应用技巧如只读特定的工作薄、过滤某些列等,同时强调了需要注意的地方(像是路径设置、engine 参数调整之类),让读者可以在面对实际项目需求时做出更加明智的选择和技术选型。 适合人群:对 Python 有基本掌握并希望提升数据读取能力的开发人员。 使用场景及目标:适用于任何涉及到批量数据导入或是与 Excel 进行交互的业务流程。无论是做初步的数据探索还是深入挖掘隐藏于电子表格背后的故事,亦或是仅为了简化日常办公自动化任务都可以从中受益。最终目标帮助使用者熟悉两大主流 Excel 解决方案的技术特性和最佳实践。 阅读建议:本文既是一份详尽的学习指南也是一份方便随时查阅的手册。因此初学者应当认真研究所提供的示例,而有一定经验者也可以快速定位到感兴趣的部分查看关键要点。
# 医护人员排班系统 ## 1. 项目介绍 本系统是一个基于SpringBoot框架开发的医护人员排班管理系统,用于医院管理医护人员的排班、调班等工作。系统提供了完整的排班管理功能,包括科室管理、人员管理、排班规则配置、自动排班等功能。 ## 2. 系统功能模块 ### 2.1 基础信息管理 - 科室信息管理:维护医院各科室基本信息 - 医护人员管理:管理医生、护士等医护人员信息 - 排班类型管理:配置不同的排班类型(如:早班、中班、晚班等) ### 2.2 排班管理 - 排班规则配置:设置各科室排班规则 - 自动排班:根据规则自动生成排班计划 - 排班调整:手动调整排班计划 - 排班查询:查看各科室排班情况 ### 2.3 系统管理 - 用户管理:管理系统用户 - 角色权限:配置不同角色的操作权限 - 系统设置:管理系统基础配置 ## 3. 技术架构 ### 3.1 开发环境 - JDK 1.8 - Maven 3.6 - MySQL 5.7 - SpringBoot 2.2.2 ### 3.2 技术栈 - 后端框架:SpringBoot - 持久层:MyBatis-Plus - 数据库:MySQL - 前端框架:Vue.js - 权限管理:Spring Security ## 4. 数据库设计 主要数据表: - 科室信息表(keshixinxi) - 医护人员表(yihurengyuan) - 排班类型表(paibanleixing) - 排班信息表(paibanxinxi) - 用户表(user) ## 5. 部署说明 ### 5.1 环境要求 - JDK 1.8+ - MySQL 5.7+ - Maven 3.6+ ### 5.2 部署步骤 1. 创建数据库并导入SQL脚本 2. 修改application.yml中的数据库配置 3. 执行maven打包命令:mvn clean package 4. 运行jar包:java -jar xxx.jar ## 6. 使用说明 ### 6.1 系统登录 - 管理员账号:admin - 初始密码:admin ### 6.2 基本操作流程 1. 维护基础信息(科室、人员等) 2. 配置排班规则 3. 生成排班计划 4. 查看和调整排班 ## 7. 注意事项 1. 首次使用请及时修改管理员密码 2. 定期备份数据库 3. 建议定期检查和优化排班规则
MATLAB仿真的夫琅禾费衍射强度图:圆孔、圆环、矩形孔定制研究,MATLAB仿真:夫琅禾费衍射强度图的可定制性——以圆孔、圆环及矩形孔为例的研究分析,MATLAB夫琅禾费衍射强度图仿真 圆孔,圆环,矩形孔可定制。 ,MATLAB; 夫琅禾费衍射; 强度图仿真; 圆孔; 圆环; 矩形孔; 可定制。,MATLAB仿真夫琅禾费衍射强度图:定制孔型(圆孔/圆环/矩形)
详细介绍及样例数据:https://blog.csdn.net/samLi0620/article/details/145652300
基于Dugoff轮胎模型与B08_01基础建模的七自由度车辆动力学模型验证:利用MATLAB 2018及以上版本与CarSim 2020.0软件的仿真对比研究,基于Dugoff轮胎模型与B08_01框架的七自由度车辆动力学模型验证——使用MATLAB 2018及以上版本与CarSim 2020.0软件进行仿真对比研究,七自由度车辆动力学模型验证(Dugoff轮胎模型,B08_01基础上建模) 1.软件: MATLAB 2018以上;CarSim 2020.0 2.介绍: 基于Dugoff轮胎模型和车身动力学公式,搭建7DOF车辆动力学Simulink模型,对相关变量(质心侧偏角,横摆角速度,纵、横向速度及加速度)进行CarSim对比验证。 ,核心关键词:七自由度车辆动力学模型验证; Dugoff轮胎模型; B08_01建模基础; MATLAB 2018以上; CarSim 2020.0; Simulink模型; 变量对比验证。,基于Dugoff轮胎模型的七自由度车辆动力学模型验证与CarSim对比
【毕业设计】基于Java+servlet+jsp+css+js+mysql实现“转赚”二手交易平台_pgj
微猫恋爱聊妹术小程序源码介绍: 微猫恋爱聊妹术小程序源码是一款全新升级的聊天工具,它采用全新主题和UI,完美支持分享朋友圈功能。同时,它的独立后台也进行了大规模更新,让操作更加简单。其中,课堂页面、搜索页面和子话术列表页面等,均增加了流量主展示,具有超多的功能。 安装教程: 您可以先加入微猫恋爱聊妹术小程序源码的赞助群,然后在群内找到魔方安装说明。根据源码编号找到相应的安装说明,非常详细,让您轻松完成安装。
电气安装工程安全技术规程_蒋凯,杨华甫,马仲范,王清禄译;孙照森校;鞍钢工程技术编委会编
基于Copula函数的风光空间相关性联合场景生成与K-means聚类削减MATLAB研究,基于Copula函数的风光空间相关性联合场景生成与K-means聚类削减算法研究,基于copula的风光联合场景生成?K-means聚类并削减 MATLAB 由于目前大多数研究的是不计风光出力之间的相关性影响,但是地理位置相近的风电机组和光伏机组具有极大的相关性。 因此,采用 Copula 函数作为风电、光伏联合概率分布,生成风、光考虑空间相关性联合出力场景,在此基础上,基于Kmeans算法,分别对风光场景进行聚类,从而实现大规模场景的削减,削减到5个场景,最后得出每个场景的概率与每个对应场景相乘求和得到不确定性出力 ,基于Copula的风光联合场景生成; K-means聚类削减; 空间相关性; 概率分布; 场景削减,基于Copula与K-means的风光联合场景生成与削减研究
模块化多电平变流器MMC的VSG控制技术研究:基于MATLAB-Simulink的仿真分析与定制实现——支持三相与任意电平数,构网型模块化多电平变流器MMC的VSG控制策略与仿真模型:三相负荷变动下的虚拟同步发电机控制研究,构网型 模块化多电平变流器 MMC 的VSG控制 同步发电机控制 MATLAB–Simulink仿真模型,可按需求定制 10电平.14电平,任意电平可做。 三相MMC,采用VSG控制。 设置负荷变动,调整有功无功,保持电网电压和频率 ,构网型模块化多电平变流器; MMC的VSG控制; 虚拟同步发电机控制; MATLAB–Simulink仿真模型; 任意电平可做; 三相MMC; 负荷变动; 有功无功调整; 电网电压和频率保持。,基于VSG控制的模块化多电平变流器(MMC)的构网型仿真模型
暗通道算法DCP-Python实现
南师大实验室安全准入知识供学习
纯openMV寻迹小车.zip
【毕业设计】基于Java mvc架构开发的完整购物网站
以下是针对初学者的 **51单片机入门教程**,内容涵盖基础概念、开发环境搭建、编程实践及常见应用示例,帮助你快速上手。
springboot医院信管系统--