第2章 使用DSL
看过上一章的例子后,即便尚未给出DSL的一般定义,对于何为DSL,你也应该已经有了自己的认识。(第10章中有更多例子。)现在,要开始讨论DSL的定义及其优势与问题。这样就可以为下一章讨论DSL实现提供一些上下文。
2.1定义DSL
“领域特定语言”是一个很有用的术语和概念,但其边界很模糊。一些东西很明显是DSL,但另一些可能会引发争议。这一术语由来已久,不过,正如软件行业中的很多东西一样,它也从未有过一个确切的定义。然而,就本书而言,定义是非常有价值的。
领域特定语言(名词): 针对某一特定领域,具有受限表达性的一种计算机程序设计语言。
这一定义包含4个关键元素:
计算机程序设计语言(computer programming language):人们用DSL指挥计算机去做一些事。同大多数现代程序设计语言一样,其结构设计成便于人们理解的样子,但它应该还是可以由计算机执行的语言。
语言性(language nature):DSL是一种程序设计语言,因此它必须具备连贯的表达能力─不管是一个表达式还是多个表达式组合在一起。
受限的表达性(limited expressiveness):通用程序设计语言提供广泛的能力:支持各种数据、控制,以及抽象结构。这些能力很有用,但也会让语言难于学习和使用。DSL只支持特定领域所需要特性的最小集。使用DSL,无法构建一个完整的系统,相反,却可以解决系统某一方面的问题。
针对领域(domain focus):只有在一个明确的小领域下,这种能力有限的语言才会有用。这个领域才使得这种语言值得使用。
注意,“针对领域”在这个列表中最后出现,它纯粹是受限表达性的结果。很多人按字面意思把DSL理解为一种用于专用领域的语言。但字面意思常常有误:比如,我们不会管硬币叫“光盘”(Compact Disk,紧凑的盘),即便它确实是“盘”,而且相比于可以用这个术语称呼的东西,更为紧凑(compact)。
DSL主要分为三类:外部DSL、内部DSL,以及语言工作台。
外部DSL是一种“不同于应用系统主要使用语言”的语言。外部DSL通常采用自定义语法,不过选择其他语言的语法也很常见(XML就是一个常见选择)。宿主应用的代码会采用文本解析技术对使用外部DSL编写的脚本进行解析。一些小语言的传统UNIX就符合这种风格。可能经常会遇到的外部DSL的例子包括:正则表达式、SQL、Awk,以及像Struts和Hibernate这样的系统所使用的XML配置文件。
内部DSL是一种通用语言的特定用法。用内部DSL写成的脚本是一段合法的程序,但是它具有特定的风格,而且只用到了语言的一部分特性,用于处理整个系统一个小方面的问题。用这种DSL写出的程序有一种自定义语言的风格,与其所使用的宿主语言有所区别。这方面最经典的例子是Lisp。Lisp程序员写程序就是创建和使用DSL。Ruby社区也形成了显著的DSL文化:许多Ruby库都呈现出DSL的风格。特别是,Ruby最著名的框架Rails,经常被认为是一套DSL。
语言工作台是一个专用的IDE,用于定义和构建DSL。具体来说,语言工作台不仅用来确定DSL的语言结构,而且是人们编写DSL脚本的编辑环境。最终的脚本将编辑环境和语言本身紧密结合在一起。
多年来,这三种风格分别发展了自己的社区。你会发现,那些非常擅长使用内部DSL的人,完全不了解如何构造外部DSL。我担心这可能会导致人们不能采用最适合的工具来解决问题。我曾与一个团队讨论过,他们采用了非常巧妙的内部DSL处理技巧来支持自定义语法,但我相信,如果他们使用外部DSL的话,问题会变得简单许多。但由于对如何构造外部DSL一无所知,他们别无选择。因此,在本书中,把内部DSL和外部DSL讲清楚对我来说格外重要,这样你就可以了解这些信息,做出适当的选择。(语言工作台稍显粗略,因为它们很新,尚在演化之中。)
另一种看待DSL的方式是:把它看做一种处理抽象的方式。在软件开发中,我们经常会在不同的层面上建立抽象,并处理它们。建立抽象最常见的方式是实现一个程序库或框架。操纵框架最常见的方式是通过命令/查询式API调用。从这种角度来看,DSL就是这个程序库的前端,它提供了一种不同于命令/查询式API风格的操作方式。在这样的上下文中,程序库成了DSL的“语义模型”(第11章),因此,DSL经常伴随着程序库出现。事实上,我认为,对于构建良好的DSL 而言,语义模型是一个不可或缺的附属物。
谈及DSL,人们很容易觉得构造DSL很难。实际上,通常是难在构造模型上,DSL只是位于其上的一层而已。虽然让DSL 工作良好需要花费一定的精力,但相对于构建底层模型,这一部分的付出要少多了。
2.1.1DSL的边界
前面说过,DSL是一种边界模糊的概念。虽然我相信没有人会质疑正则表达式是一种DSL,但确实有许多存在争议的情况。因此我觉得有必要在这里讨论一下这些情况,让我们更好地理解什么是DSL。
每种风格的DSL都有各自的边界条件,下面会分别讨论。当讨论这些内容时,请记住,用于区分各种DSL特征的是语言性、 针对领域以及受限表达性。根据经验,按照“针对领域”进行划分效果欠佳,因此,常常选择按语言性以及受限表达性进行划分。
我将从内部DSL开始。这里的边界问题,其实就是内部DSL与命令/查询式API之间的差异。从许多方面来说,内部 DSL不过是一种特殊的API(就像出自贝尔实验室的一句老话“程序库设计就是语言设计”)。在我看来,核心差异在于语言性。Mike Roberts曾说过,命令/查询式API定义了抽象领域的词汇,而内部DSL则在此基础上添加了语法。
给具有命令/查询式API的类编写文档,一种常见的方式是,列出其拥有的所有方法。采用这种方法,就意味着每个方法自身都有独立含义。从这样的文档中,我们得到了一组“词汇”,每一个都有自己的完备语义。而内部DSL的方法名只在一个更大表达式的上下文中才有明确的含义。在前面Java内部DSL的例子中,有一个名为to的方法,定义了状态迁移的目标。这样的方法名在命令/查询式API中是一个不好的名字,但适用于这样的语 句:.transition(lightOn).to(unlockedPanel)。
正因为这样,内部DSL给人的感觉是一个整句,而非一个无关命令的序列。这正是这种API称为连贯接口的基础。
对内部DSL来说,受限表达性显然不是语言的一项核心属性,因为内部DSL植根于一种通用语言。在这种情况下,受限表达性表现在如何使用它。当构造DSL表达式时,我们会限制自己只使用通用语言的一部分特性,通常不会使用条件判断、循环结构和变量。Piers Cawley将其称为宿主语言的一种混杂用法(pidgin use)。
对外部DSL来说,其边界就是它与通用语言之间的边界。语言可以针对某领域,但仍是通用语言。R语言就是一个很好的例子,它是一种用于统计的语言和平台,主要用于解决统计方面的问题,它也具备通用语言所有的表达性。因此,尽管它针对于某一领域,但是我们依然不会称其为一种DSL。
一种更为明显的DSL是正则表达式。它所针对的领域(文本匹配)与其有限的特性紧密相关─那些特性刚刚好能做到易于匹配文本。DSL的一个普遍特征是,它们不是图灵完备的。一般来说,DSL不支持常见的命令式控制结构(条件和循环),也不能定义变量和子例程。
说到这里,可能会有很多人不同意我的看法,他们会觉得,从DSL的字面语义来说,像R这样的语言应该归为DSL。我之所以如此强调DSL的有限表达性,是因为正是它使得DSL和通用语言之间的区别有了意义。有限表达性让DSL产生了独特性,无论是使用还是实现,这种独特性给了我们完全不同于通用语言的DSL思维方式。
如果这样的边界还不够模糊,我们再来看看XSLT。XSLT针对的领域是XML文档转换,但是它具备我们预期在一门常规程序设计语言中看到的所有特性。这样一来,XSLT是什么样的语言已经不重要了,重要的是人们如何使用它。如果XSLT用于转换XML文档,我愿意把它称之为DSL;但如果用以解决“八皇后”(eight queens)问题,则我认为它是一门通用语言。一门语言的特定用法可能将它置于DSL分界线的任意一边。
外部DSL同序列化的数据结构之间也存在一条边界。配置文件中的属性赋值(如color=blue) 列表算DSL吗?在这种情况下,边界条件是DSL的语言性。因为一系列赋值表达式缺乏表达上的连贯性,所以它不符合标准。
同样的理由也适用于许多配置文件。如今许多环境为用户提供了对配置文件编程的能力,这些配置文件通常采用XML 格式。这些XML配置文件在很多情况下足以成为DSL。然而,并非总是如此。有时,XML文件是由工具生成的,此时 XML只是一种序列化的手段,而非为人所用,因此我不会将其归为DSL。当然,一种存储格式具备易读性肯定是有价值的,毕竟它有利于调试。其实,问题不在于XML是否可读,而在于其表示方式是否是人们与系统某一方面交互的主要方式。
这种配置文件的最大问题在于,虽然它们不是用来人工编辑的,但在实际中,人们却经常人工编辑。于是这种 XML文档就意外地成为了DSL。
对语言工作台来说,语言工作台同任意程序之间的边界在于,它允许用户设计自己的数据结构和表单─就像Microsoft Access。毕竟,随便拿来一个状态模型,都可以用关系数据库结构来表示它(我还见过比这更糟糕的主意),然后就能创建表单,操作模型。这里有两个问题:1)Access是一种语言工作台吗?2)在Access里定义的是一 种DSL吗?
先看第二个问题。既然在构建状态机的一个特定应用,我们就有了领域针对性和受限表达性,关键的问题就在于语言性。如果只是把数据放进表单,然后保存在表格里,这通常不是一种真正的语言。表格可以是语言性的一种表示法─“FIT”(10.6节)和 Excel都采用了表格式的表示法,又都给人一种语言的感觉(我会把FIT当做是领域专用的,而Excel是通用的)。但是绝大部分应用不会追求这种连贯性,它们只是创建表单和窗口,而不强调关联性。例如,Meta-Programming System Language Workbench的文本界面,感觉上迥异于大部分基于表单的界面。类似地,很少有应用像 MetaEdit那样让人通过排列图表来定义事物的整合方式。
至于Acess是否是一种语言工作台,我想我们可以回到其原始的设计意图上。如果真的需要,我们确实可以把它当做语言工作台用,但它并非设计成这样。想想有多少人把Excel当做数据库─即便它也没有设计成那样。
从更广泛的意义上说,一种纯粹在人与人之间使用的行话是否是一种DSL呢? 一个广为流传的例子是人们在星巴克点咖啡所用的语言:“超大杯、低咖、脱脂、无泡沫、不搅拌的拿铁。” 这种语言很不错,因为它有受限的表达性、针对领域、有词汇和语法的感觉。但它在我的定义之外,因为“领域特定语言”只用于指代计算机语言。如果要实现一种理解星巴克表达方式的计算机语言,它就真成为DSL了。但在我们要补充咖啡因时,从我们脱口而出的措辞是一种人类语言。这里把人们在特定领域中使用的语言称为领域语言(domain language),而 把“DSL”保留给计算机语言。
那么,这个关于DSL边界的讨论告诉了我们什么呢?我希望,至少有一件事情是清楚的: 那就是基本上没有明确的边界。追求理性的人可以不认同我的DSL定义。事实上,因为像语言性和受限表达性这样的衡量标准本身就很模糊,所以可以推断出,基于这些标准得到的结果也会同样模糊。而且,并非所有人都会采纳我的这些衡量标准。
在上面的讨论中,许多例子都被排除在DSL的定义之外,但这不代表它们没有价值。定义的价值在于它有助于沟通,这样,不同背景的人对于要讨论的内容有个共同的认识。对本书来说,它帮我们理清这里描述的技术是否与之相关。对我来说,有了这样的DSL定义之后,我就能更有效地选择一些需要讨论的技术。
2.1.2片段DSL和独立DSL
第2章用的那个秘密面板状态机的例子是一种独立的DSL。这意味着,随便拿一段这种 DSL脚本(一 般是一个文件)来看,其中全是这种DSL。只要熟悉这种DSL,我们就能够理解这段DSL在做什么,即便不了解宿主语言,因为根本就没有宿主语言(对外部DSL而言),或者为内部DSL掩盖。
DSL出现的另一种方式是片段形式。对于这种形式,DSL片段用于宿主语言的代码中。我们可以将其视为采用额外特性对宿主语言进行增强。在这种情况下,不了解宿主语言,就看不懂这些DSL在做些什么。
对外部DSL而言,片段DSL的一个典型例子是正则表达式。我们不会见到一个充斥着正则表达式的程序文件,相反,却可以见到在一个程序中点缀着正则表达式片段。还有一个例子是SQL,在大型程序的上下文中,常常会用到SQL语句。
内部DSL也有使用片段形态的情况。单元测试领域是内部DSL的高产区。特别是在mock对象程序库中设置预期的语法,它就是在大规模宿主语言里DSL的集中爆发。此外,用于内部片段DSL的一个流行语言特性是标注,可以用它给宿主代码中的编程元素添加元数据。这种用法使注解适用于片段DSL,而非独立DSL。
还有同样的DSL既可以独立使用,也可以作为片段使用,比如SQL。有些DSL设计成以片段形式使用,有些旨在独立使用,还有一些则二者均可。