首页 / 软件开发 / JAVA / 面向Java开发人员的Scala指南 - 构建计算器,第1部分
面向Java开发人员的Scala指南 - 构建计算器,第1部分2011-01-30Ted Neward特定于领域的语言已经成为一个热门话题;很多函数性语言之所以受欢迎,主要是因为它们可以用于构建特定于领域的语言。鉴于此,在 面向 Java™ 开发人员的 Scala 指南 系列的第 8 篇文章中,Ted Neward 着手构建一个简单的计算器 DSL,以此来展示函数性语言的构建 “外部” DSL 的强大功能。他研究了 Scala 的一个新的特性:case 类,并重新审视一个功能强大的特性:模式匹配。上个月的文章发表后,我又收到了一些抱怨/评论,说我迄今为止在本系列中所用的示例都没涉及到什么实质性的问题。当然在学习一个新语言的初期使用一些小例子是很合理的,而读者想要看到一些更 “现实的” 示例,从而了解语言的深层领域和强大功能以及其优势,这也是理所当然的。因此,在这个月的文章中,我们来分两部分练习构建特定于领域的语言(DSL)— 本文以一个小的计算器语言为例。特定于领域的语言可能您无法(或没有时间)承受来自于您的项目经理给您的压力,那么让我直接了当地说吧:特定于领域的语言无非就是尝试(再一次)将一个应用程序的功能放在它该属于的地方 — 用户的手中。通过定义一个新的用户可以理解并直接使用的文本语言,程序员成功摆脱了不停地处理 UI 请求和功能增强的麻烦,而且这样还可以使用户能够自己创建脚本以及其他的工具,用来给他们所构建的应用程序创建新的行为。虽然这个例子可能有点冒险(或许会惹来几封抱怨的电子邮件),但我还是要说,DSL 的最成功的例子就是 Microsoft® Office Excel “语言”,用于表达电子表格单元格的各种计算和内容。甚至有些人认为 SQL 本身就是 DSL,但这次是一个旨在与关系数据库相交互的语言(想象一下如果程序员要通过传统 API read()/write() 调用来从 Oracle 中获取数据的话,那将会是什么样子)。这里构建的 DSL 是一个简单的计算器语言,用于获取并计算数学表达式。其实,这里的目标是要创建一个小型语言,这个语言能够允许用户来输入相对简单的代数表达式,然后这个代码来为它求值并产生结果。为了尽量简单明了,该语言不会支持很多功能完善的计算器所支持的特性,但我不也不想把它的用途限定在教学上 — 该语言一定要具备足够的可扩展性,以使读者无需彻底改变该语言就能够将它用作一个功能更强大的语言的核心。这意味着该语言一定要可以被轻易地扩展,并要尽量保持封装性,用起来不会有任何的阻碍。换句话说,(最终的)目标是要允许客户机编写代码,以达到如下的目的:清单 1. 计算器 DSL:目标// This is Java using the Calculator
String s = "((5 * 10) + 7)";
double result = com.tedneward.calcdsl.Calculator.evaluate(s);
System.out.println("We got " + result); // Should be 57我们不会在一篇文章完成所有的论述,但是我们在本篇文章中可以学习到一部分内容,在下一篇文章完成全部内容。从实现和设计的角度看,可以从构建一个基于字符串的解析器来着手构建某种可以 “挑选每个字符并动态计算” 的解析器,这的确极具诱惑力,但是这只适用于较简单的语言,而且其扩展性不是很好。如果语言的目标是实现简单的扩展性,那么在深入研究实现之前,让我们先花点时间想一想如何设计语言。根据那些基本的编译理论中最精华的部分,您可以得知一个语言处理器(包括解释器和编译器)的基本运算至少由两个阶段组成:解析器,用于获取输入的文本并将其转换成 Abstract Syntax Tree(AST)。代码生成器(在编译器的情况下),用于获取 AST 并从中生成所需字节码;或是求值器(在解释器的情况下),用于获取 AST 并计算它在 AST 里面所发现的内容。拥有 AST 就能够在某种程度上优化结果树,如果意识到这一点的话,那么上述区别的原因就变得更加显而易见了;对于计算器,我们可能要仔细检查表达式,找出可以截去表达式的整个片段的位置,诸如在乘法表达式中运算数为 “0” 的位置(它表明无论其他运算数是多少,运算结果都会是 “0”)。您要做的第一件事是为计算器语言定义该 AST。幸运的是,Scala 有 case 类:一种提供了丰富数据、使用了非常薄的封装的类,它们所具有的一些特性使它们很适合构建 AST。