从java走进scala:构建计算器dsl_第1页
从java走进scala:构建计算器dsl_第2页
从java走进scala:构建计算器dsl_第3页
从java走进scala:构建计算器dsl_第4页
从java走进scala:构建计算器dsl_第5页
已阅读5页,还剩10页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

从 Java 走进 Scala:构建计算器 DSL大 中 小本文继续探索 Scala 的语言和库支持,我们将改造一下计算器 DSL 并最终 “完成它” 。DSL 本身有点简单 一个简单的计算器,目前为止只支持 4 个基本数学运算符。但要记住,我们的目标是创建一些可扩展的、灵活的对象,并且以后可以轻松增强它们以支持新的功能。继续上次的讨论说明一下,目前我们的 DSL 有点零乱。我们有一个抽象语法树(Abstract Syntax Tree ) ,它由大量 case 类组成清单 1. 后端(AST)1. package com.tedneward.calcdsl 2. 3. / . 4. 5. privatecalcdsl abstract class Expr 6. privatecalcdsl case class Variable(name : String) extends Expr 7. privatecalcdsl case class Number(value : Double) extends Expr 8. privatecalcdsl case class UnaryOp(operator : String, arg : Expr) extends Expr 9. privatecalcdsl case class BinaryOp(operator : String, left : Expr, right : Expr) 10. extends Expr 11. 12. 对此我们可以提供类似解释器的行为,它能最大限度地简化数学表达式 清单 2. 后端(解释器)1. package com.tedneward.calcdsl 2. 3. / . 4. 5. object Calc 6. 7. def simplify(e: Expr): Expr = 8. / first simplify the subexpressions 9. val simpSubs = e match 10. / Ask each side to simplify 11. case BinaryOp(op, left, right) = BinaryOp(op, simplify(left), simplify(right) 12. / Ask the operand to simplify 13. case UnaryOp(op, operand) = UnaryOp(op, simplify(operand) 14. / Anything else doesnt have complexity (no operands to simplify) 15. case _ = e 16. 17. 18. / now simplify at the top, assuming the components are already simplified 19. def simplifyTop(x: Expr) = x match 20. / Double negation returns the original value 21. case UnaryOp(“-“, UnaryOp(“-“, x) = x 22. 23. / Positive returns the original value 24. case UnaryOp(“+“, x) = x 25. 26. / Multiplying x by 1 returns the original value 27. case BinaryOp(“*“, x, Number(1) = x 28. 29. / Multiplying 1 by x returns the original value 30. case BinaryOp(“*“, Number(1), x) = x 31. 32. / Multiplying x by 0 returns zero 33. case BinaryOp(“*“, x, Number(0) = Number(0) 34. 35. / Multiplying 0 by x returns zero 36. case BinaryOp(“*“, Number(0), x) = Number(0) 37. 38. / Dividing x by 1 returns the original value 39. case BinaryOp(“/“, x, Number(1) = x 40. 41. / Dividing x by x returns 1 42. case BinaryOp(“/“, x1, x2) if x1 = x2 = Number(1) 43. 44. / Adding x to 0 returns the original value 45. case BinaryOp(“+“, x, Number(0) = x 46. 47. / Adding 0 to x returns the original value 48. case BinaryOp(“+“, Number(0), x) = x 49. 50. / Anything else cannot (yet) be simplified 51. case e = e 52. 53. simplifyTop(simpSubs) 54. 55. 56. def evaluate(e : Expr) : Double = 57. 58. simplify(e) match 59. case Number(x) = x 60. case UnaryOp(“-“, x) = -(evaluate(x) 61. case BinaryOp(“+“, x1, x2) = (evaluate(x1) + evaluate(x2) 62. case BinaryOp(“-“, x1, x2) = (evaluate(x1) - evaluate(x2) 63. case BinaryOp(“*“, x1, x2) = (evaluate(x1) * evaluate(x2) 64. case BinaryOp(“/“, x1, x2) = (evaluate(x1) / evaluate(x2) 65. 66. 67. 68. 我们使用了一个由 Scala 解析器组合子构建的文本解析器,用于解析简单的数学表达式清单 3. 前端1. package com.tedneward.calcdsl 2. 3. / . 4. 5. object Calc 6. 7. object ArithParser extends JavaTokenParsers 8. 9. def expr: ParserAny = term rep(“+“term | “-“term) 10. def term : ParserAny = factor rep(“*“factor | “/“factor) 11. def factor : ParserAny = floatingPointNumber | “(“expr“)“ 12. 13. def parse(text : String) = 14. 15. parseAll(expr, text) 16. 17. 18. 19. / . 20. 21. 但在进行解析时,由于解析器组合子当前被编写为返回 ParserAny 类型,所以会生成 String 和 List 集合,实际上应该让解析器返回它需要的任意类型(我们可以看到,此时是一个 String 和 List 集合) 。要让 DSL 成功,解析器需要返回 AST 中的对象,以便在解析完成时,执行引擎可以捕获该树并对它执行 evaluate()。对于该前端,我们需要更改解析器组合子实现,以便在解析期间生成不同的对象。清理语法对解析器做的第一个更改是修改其中一个语法。在原来的解析器中,可以接受像 “5 + 5 + 5” 这样的表达式,因为语法中为表达式( expr)和术语(term)定义了 rep() 组合子。但如果考虑扩展,这可能会引起一些关联性和操作符优先级问题。以后的运算可能会要求使用括号来显式给出优先级,以避免这类问题。因此第一个更改是将语法改为要求在所有表达式中加 “()” 。回想一下,这应该是我一开始就需要做的事情;事实上,放宽限制通常比在以后添加限制容易(如果最后不需要这些限制) ,但是解决运算符优先级和关联性问题比这要困难得多。如果您不清楚运算符的优先级和关联性;那么让我大致概述一下我们所处的环境将有多复杂。考虑 Java 语言本身和它支持的各种运算符(如 Java 语言规范中所示)或一些关联性难题(来自 Bloch 和 Gafter 提供的 Java Puzzlers) ,您将发现情况不容乐观。因此,我们需要逐步解决问题。首先是再次测试语法: 清单 4. 采用括号1. package com.tedneward.calcdsl 2. 3. / . 4. 5. object Calc 6. 7. / . 8. 9. object OldAnyParser extends JavaTokenParsers 10. 11. def expr: ParserAny = term rep(“+“term | “-“term) 12. def term : ParserAny = factor rep(“*“factor | “/“factor) 13. def factor : ParserAny = floatingPointNumber | “(“expr“)“ 14. 15. def parse(text : String) = 16. 17. parseAll(expr, text) 18. 19. 20. object AnyParser extends JavaTokenParsers 21. 22. def expr: ParserAny = (term“+“term) | (term“-“term) | term 23. def term : ParserAny = (factor“*“factor) | (factor“/“factor) | factor 24. def factor : ParserAny = “(“ expr 和 BinaryOp(“+“, lhs, rhs) | 11. (term “-“ term) case lhsminusrhs = BinaryOp(“-“, lhs, rhs) | 12. term 13. 14. def term: ParserExpr = 15. (factor “*“ factor) case lhstimesrhs = BinaryOp(“*“, lhs, rhs) | 16. (factor “/“ factor) case lhsdivrhs = BinaryOp(“/“, lhs, rhs) | 17. factor 18. 19. def factor : ParserExpr = 20. “(“ expr Number(x.toFloat) 22. 23. def parse(text : String) = parseAll(expr, text) 24. 25. 26. def parse(text : String) = 27. ExprParser.parse(text).get 28. 29. / . 30. 31. 32. / . 33. 组合子接收一个匿名函数,其解析结果(例如,假设输入的是 5 + 5,那么解析结果将是 (5+)5))将会被单独传递并得到一个对象 在本例中,是一个适当类型的 BinaryObject。请再次注意模式匹配的强大功能;我将表达式的左边部分与 lhs 实例绑定在一起,将 + 部分与(未使用的) plus 实例绑定在一起,该表达式的右边则与 rhs 绑定,然后我分别使用 lhs 和 rhs 填充 BinaryOp 构造函数的左边和右边。现在运行代码(记得注释掉临时测试) ,单元测试集会再次产生所有正确的结果:我们以前尝试的各种表达式不会再失败,因为现在解析器生成了派生 Expr 对象。前面已经说过,不进一步测试解析器是不负责任的,所以让我们添加更多的测试(包括我之前在解析器中使用的非正式测试):清单 7. 测试解析器(这次是正式的)1. package com.tedneward.calcdsl.test 2. 3. class CalcTest 4. 5. import org.junit._, Assert._ 6. 7. / . 8. 9. Test def parseAnExpr1 = 10. assertEquals( 11. Number(5), 12. Calc.parse(“5“) 13. ) 14. Test def parseAnExpr2 = 15. assertEquals( 16. Number(5), 17. Calc.parse(“(5)“) 18. ) 19. Test def parseAnExpr3 = 20. assertEquals( 21. BinaryOp(“+“, Number(5), Number(5), 22. Calc.parse(“5 + 5“) 23. ) 24. Test def parseAnExpr4 = 25. assertEquals( 26. BinaryOp(“+“, Number(5), Number(5), 27. Calc.parse(“(5 + 5)“) 28. ) 29. Test def parseAnExpr5 = 30. assertEquals( 31. BinaryOp(“+“, BinaryOp(“+“, Number(5), Number(5), Number(5), 32. Calc.parse(“(5 + 5) + 5“) 33. ) 34. Test def parseAnExpr6 = 35. assertEquals( 36. BinaryOp(“+“, BinaryOp(“+“, Number(5), Number(5), BinaryOp(“+“, Number(5), 37. Number(5), 38. Calc.parse(“(5 + 5) + (5 + 5)“) 39. ) 40. 41. / other tests elided for brevity 42. 43. 读者可以再增加一些测试,因为我可能漏掉一些不常见的情况(与 Internet 上的其他人结对编程是比较好的) 。完成最后一步假设解析器正按照我们想要的方式在工作 即生成 AST 那么现在只需要根据 AST 对象的计算结果来完善解析器。这很简单,只需向 Calc 添加代码,如清单 8 所示清单 8. 真的完成啦!1. package com.tedneward.calcdsl 2. 3. / . 4. 5. object Calc 6. 7. / . 8. 9. def evaluate(text : String) : Double = evaluate(parse(text) 10. 11. 同时添加一个简单的测试,确保 evaluate(“1+1“) 返回 2.0清单 9. 最后,看一下 1 + 1 是否等于 21. package com.tedneward.calcdsl.test 2. 3. class CalcTest 4. 5. import org.junit._, Assert._ 6. 7. / . 8. 9. Test def add1 = 10. assertEquals(Calc.evaluae(“1 + 1“), 2.0) 11. 12. 然后运行它,一切正常!扩展 DSL 语言如果完全用 Java 代码编写同一个计算器 DSL,而没有碰到我遇到的问题(在不构建完整的 AST 的情况下递归式地计算每一个片段,等等) ,那么似乎它是另一种能够解决问题的语言或工具。但以这种方式构建语言的强大之处会在扩展性上得到体现。例如,我们向这种语言添加一个新的运算符,即 运算符,它将执行求幂运算;也就是说,2 2 等于 2 的平方 或 4。向 DSL 语言添加这个运算符需要一些简单步骤。首先,您必须考虑是否需要更改 AST。在本例中,求幂运算符是另一种形式的二进制运算符,所以使用现有 BinaryOp case 类就可以。无需对 AST 进行任何更改。其次,必须修改 evaluate 函数,以使用 BinaryOp(“, x, y) 执行正确的操作;这很简单,只需添加一个嵌套函数(因为不必在外部看到这个函数)来实际计算指数,然后向模式匹配添加必要的代码行,如下所示: 清单 10. 稍等片刻1. package com.tedneward.calcdsl 2. 3. / . 4. 5. object Calc 6. 7. / . 8. 9. def evaluate(e : Expr) : Double = 10. 11. def exponentiate(base : Double, exponent : Double) : Double = 12. if (exponent = 0) 13. 1.0 14. else 15. base * exponentiate(base, exponent - 1) 16. 17. simplify(e) match 18. case Number(x) = x 19. case UnaryOp(“-“, x) = -(evaluate(x) 20. case BinaryOp(“+“, x1, x2) = (evaluate(x1) + evaluate(x2) 21. case BinaryOp(“-“, x1, x2) = (evaluate(x1) - evaluate(x2) 22. case BinaryOp(“*“, x1, x2) = (evaluate(x1) * evaluate(x2) 23. case BinaryOp(“/“, x1, x2) = (evaluate(x1) / evaluate(x2) 24. case BinaryOp(“, x1, x2) = exponentiate(evaluate(x1), evaluate(x2) 25. 26. 27. 28. 注意,这里我们只使用 6 行代码就有效地向系统添加了求幂运算,同时没有对 Calc 类进行任何表面更改。这就是封装!(在我努力创建最简单求幂函数时,我故意创建了一个有严重 bug 的版本 这是为了让我们关注语言,而不是实现。也就是说,看看哪位读者能够找到 bug。他可以编写发现 bug 的单元测试,然后提供一个无 bug 的版本) 。但是在向解析器添加这个求幂函数之前,让我们先测试这段代码,以确保求幂部分能正常工作:清单 11. 求平方1. package com.tedneward.calcdsl.test 2. 3. class CalcTest 4. 5. / . 6. 7. Test def evaluateSimpleExp = 8. 9. val expr = 10. BinaryOp(“, Number(4), Number(2) 11. val results = Calc.evaluate(expr) 12. / (4 2) = 16 13. assertEquals(16.0, results) 14. 15. Test def evaluateComplexExp = 16. 17. val expr = 18. BinaryOp(“, 19. BinaryOp(“*“, Number(2), Number(2), 20. BinaryOp(“/“, Number(4), Number(2) 21. val results = Calc.evaluate(expr) 22. / (2 * 2) (4 / 2) = (4 2) = 16 23. assertEquals(16.0, results) 24. 25. 26. 运行这段代码确保可以求幂(忽略我之前提到的 bug) ,这样就完成了一半的工作。最后一个更改是修改语法,让它接受新的求幂运算符;因为求幂的优先级与乘法和除法的相同,所以最简单的做法是将它放在 term 组合子中:清单 12. 完成了,这次是真的!1. package com.tedneward.calcdsl 2. 3. / . 4. 5. object Calc 6. 7. / . 8. 9. object ExprParser extends JavaTokenParsers 10. 11. def expr: ParserExpr = 12. (term “+“ term) case lhsplusrhs = BinaryOp(“+“, lhs, rhs) | 13. (term “-“ term) case lhsminusrhs = BinaryOp(“-“, lhs, rhs) | 14. term 15. 16. def term: ParserExpr = 17. (factor “*“ factor) case lhstimesrhs = BinaryOp(“*“, lhs, rhs) | 18. (factor “/“ factor) case lhsdivrhs = BinaryOp(“/“, lhs, rhs) | 19

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论