9.1 语言层面对DSL设计的支持越来越充分

DSL的意义在于它描述建模领域的表达能力。假如我们对一个会计系统建模,必然希望API在交流中使用借方、贷方、会计账面、分类帐、日记帐这样的专业语汇。这些术语构成了模型中的名词实体,承载着问题域的一部分核心概念。除了名词,问题域中还有动词,同样需要我们用相同的表现力水平表达出来。还记得第1章那个作为引子的咖啡店的例子吗?店员之所以能准确无误地送上我们的点单,是因为我们说的是她能理解的语言。表达的方方面面都要与建模的问题域形成共鸣。请回顾一下第3章的这段Scala代码:

  1. withAccount(trade) {
  2. account => {
  3. settle(
  4. trade using
  5. account.getClient
  6. .getStandingRules
  7. .filter(_.account == account)
  8. .first)
  9. andThen journalize
  10. }
  11. }

这里的DSL来自证券交易系统。可以看出,DSL语法很好地仿效了该领域里的名词动词。Scala支持高阶函数,因此我们可以对领域行为(动词)和领域对象(名词)一视同仁,用相同的方式来建模。这种建模手段上的统一对语言表现力有正面的影响。

读者也许会疑惑我为什么到了最后一章,还要把“表现力”这个已经贯穿全书进行讨论的主题,又重新强调一遍。我认为有必要重提这么一个事实:对于一种能力充分的语言来说,限制其表现力的只有使用者的创造力而已。只要善用元编程、函数式控制结构等方面的惯用法,再加上一个足够灵活的类型系统,足以让程序员用领域本身的语言来描述领域问题。本节我们将讨论当前的一些语言如何拓展其表现力,从而成为DSL开发的中坚力量的。

9.1.1 对表现力的不懈追求

在这个诸多新语言争相亮相的时代,我们看到,语言给我们提供了越来越充分的支持去实现丰富多彩的DSL语法设计。本书用了很多章的篇幅来讨论其中的Ruby、Groovy、Scala和Clojure语言,详细介绍了它们的设计能力。本节我们打算简要介绍其他一些语言的概况。这样做的主要目的不是为了探讨语言细节,而是为了让你感受现在正在发生的,众多语言为了提高自身与人交流的能力而付出的不懈努力。从图9-2可以看出一些主流语言的演变脉络,它们随着时间推演,进化成了另一种表现力更高的语言。

enter image description here

图9-2 编程语言的表现力演进过程

表现力充沛的编程语言可以帮助弥合问题域与解答域之间的鸿沟。在不支持高阶函数的OO语言里,我们只好把对象强扭成函数子(functor),才得以对领域动作建模。显然这样的间接手段会直接地反映在DSL设计结果上,造成非本质复杂性(对非本质复杂性的解释见附录A)。当函数不再是次一等的抽象手段时,DSL将去除那些由间接而产生的干扰成分,变得更干净,更容易被客户接受。

这些年里,由于编程语言表现力的提高,DSL开发实践也随之发生了变化。即使在C语言流行的年代,我们也一样做着编写领域规则的工作,只不过当时要在一个低得多的抽象层次上操作。图9-3列举了这方面的进步。

enter image description here

图9-3 用于DSL开发的编程语言一直在演进着,语言特性有了很大的进步

图中很多语言特性已经处于成熟阶段,但也有一些正处于争取被更多语言采纳的发展阶段。下面的三个小节将详述其中三种逐渐在DSL开发生态中占据一席之地的重要特性。第一种是已经在动态语言中普及的元编程。由于元编程在DSL设计方面的潜力,在众多加入元编程机制的语言当中,甚至有一些属于静态类型语言。

9.1.2 元编程的能力越来越强

观察最近发展起来的语言,我们可以发现它们的元编程能力在不断提高。Ruby和Groovy提供运行时元编程,这在第2章、第3章、第4章、第5章已经有很大篇幅的论述;JVM上的Lisp语言变种Clojure提供的编译时元编程,可以设计出表现力充沛的DSL,又完全没有运行时的性能负担。要想在DSL上有所建树,必须精通手头语言的元编程技术。

静态类型语言Haskell(见第9.6节文献[10])和Ocaml(见第9.6节文献[11])已经着手将元编程融入到语言的基础设施。Template Haskell是Haskell语言的扩展,它在语言中增加了编译时元编程设施。传统上用Haskell语言设计DSL时,一般会采用内部DSL的实现方式。开发者希望的写法和Haskell允许的写法之间往往不一致。而在编译时元编程技术下,我们可以按实际需要去设计语法,由语言设施将之转换为适当的Haskell AST结构,其机制类似于Lisp的宏。

众多语言大力发展元编程技术,直接证明DSL正在成为主流。下一小节我们讲述S表达式的特性,它有潜力在大多数时候取代XML充当数据载体。

9.1.3 S表达式取代XML充当载体

有些表达形式灵活的语言,如Clojure(或Lisp),提供了S表达式(s-expressions)特性,可以用代码来表示数据。现在的企业系统经常大量使用XML来描述配置数据,并冠以DSL数据建模的名头。应用中的XML结构经过适当工具的后续解析和处理,能生成可直接被程序使用的产物。这种设计除了XML难以阅读的问题之外,表达高阶结构如条件结构的能力也有欠缺,充其量只是S表达式的拙劣替代品。

我曾经做过一个项目,项目中通过XML消息来在多处部署之间传输实体。例如一个Account对象可以表示为下面的XML片段:

  1. <account>
  2. <no>a-123</no>
  3. <name>
  4. <primary>John P.</primary>
  5. <secondary>Hughes R.</secondary>
  6. </name>
  7. <dateOfOpening>20101212</dateOfOpening>
  8. <status>active</status>
  9. </account>

XML格式的消息要先经过解析,转换成合适的数据结构后才能供程序使用。我们不妨试试Clojure提供的S表达式,这样代码一下子就能变得清楚而简洁:

  1. (def account
  2. {no 123,
  3. name {primary "John P." secondary "Hughes R."},
  4. date-of-opening "20101212",
  5. status ::active })

与等价的XML相比,新的片段不但语法精简,语义也更丰富,而且是一个可以执行的Clojure数据模型。我们不需要筹划额外的机制去解析它的结构,也不需要把它转换成运行时制品;这个模型直接就能在Clojure运行时中执行。我戏称这种结构是可执行的XML。从DSL的角度看,它不但比原来的XML版本优秀得多,而且语言定义仅仅使用了编程语言本身的特性。今后随着基于DSL的开发日益成熟,这种数据即代码的范式也会用得越来越普遍。

越来越普遍的还有分析器组合子,这一波趋势主要体现在函数式语言当中。我们在第8章见识过分析器组合子优秀的DSL设计能力,下面再用一个小节说说它的发展情况。

9.1.4 分析器组合子越来越流行

我们在第8章学过,用分析器组合子来设计外部DSL时,可以不借助任何外部工具,宿主语言的一个库就已经能满足全部要求。随着函数式编程的普及,我们将会看到分析器组合子库的爆炸性增长。Gilad Bracha开发中的新语言Newspeak(见9.6节文献[4])拥有一个特别丰富的分析器组合子库,比我们用过的Scala分析器组合子能更好地解耦文法规则和语义模型。很多现存语言如F#(见9.6节文献[5])、JavaScript(见9.6节文献[6])、Scheme(见9.6节文献[7]),也都在发展自己的分析器组合子库。

分析器组合子以一种描述性的方式来定义DSL语法,外观近似于EBNF规则。虽然我们用分析器生成器也能写出类似EBNF的描述性文法规则,但分析器组合子完全在宿主语言的范围内运作,因此可以充分利用宿主语言的其他特性。宿主语言的支持将帮助我们把语义动作从文法定义中解脱出来,从而获得结构清晰的DSL实现。

除了常见的文本形式的DSL,DSL开发还存在另一个抽象层次更高的方法流派,就是DSL工作台(DSL workbench)。第7章讨论过的Xtext就是这种DSL开发范式的一个代表。将来,DSL工作台很有可能从根本上改变我们对DSL的认识。