《领域特定语言》一第2章 使用DSL 2.1定义DSL

第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设计成以片段形式使用,有些旨在独立使用,还有一些则二者均可。

时间: 2024-10-31 05:13:51

《领域特定语言》一第2章 使用DSL 2.1定义DSL的相关文章

《领域特定语言》一第1章 入 门 例 子1.1 哥特式建筑安全系统

第1章 入 门 例 子 落笔之初,我需要快速地解释一下本书的内容,就是解释什么是领域特定语言(Domain– Specific Language,DSL).为达此目的,我一般都会先展示一个具体的例子,随后再给出抽象的定义.因此,我会从一个例子开始,展示DSL可以采用的不同形式.在第2章里,我会试着把这个定义概括为一些更广泛适用的东西. 1.1 哥特式建筑安全系统 在我的童年记忆里,电视上播放的那些低劣的冒险电影是模糊却持久的.通常,这些电影的场景会安排某个古旧的城堡.密室或走廊在其中起着重要的作

《领域特定语言》一导读

前 言 在我开始编程之前,DSL(Domain–Specific Language,领域特定语言)就已经成了程序世界中的一员.随便找个UNIX或者Lisp老手问问,他一定会跟你滔滔不绝地谈起DSL是怎么成为他的镇宅之宝的,直到你被烦得痛不欲生为止.但即便这样,DSL却从未成为计算领域的一大亮点.大多数人都是从别人那里学到DSL,而且只学到了有限的几种技术. 我写这本书就是为了改变这个现状.我希望通过本书介绍的大量DSL技术,让你有足够的信息来做出决策:是否在工作中使用DSL,以及选择哪一种DSL

如何设计一门编程语言(十) 正则表达式与领域特定语言(DSL)

几个月前就一直有博友关心DSL的问题,于是我想一想,我在gac.codeplex.com里面也创建了一些DSL,于是今天就来说一说这个事情. 创建DSL恐怕是很多人第一次设计一门语言的经历,很少有人一开始上来就设计通用语言的.我自己第一次做这种事情是在高中写这个傻逼ARPG的时候了.当时做了一个超简单的脚本语言,长的就跟汇编差不多,虽然每一个指令都写成了调用函数的形态.虽然这个游戏需要脚本在剧情里面控制一些人物的走动什么的,但是所幸并不复杂,于是还是完成了任务.一眨眼10年过去了,现在在写Gac

《领域特定语言》一2.2为何需要DSL

2.2为何需要DSL 至此,我希望,对什么是DSL,我们已经有了一个很好的共识,接下来的问题是,为何要考虑采用DSL.DSL只是一种工具,关注点有限,无法像面向对象编程或敏捷方法论那样,引发软件开发思考方式的深刻变革.相反,它是在特定条件下有专门用途的一种工具.一个普通的项目可能在多个地方采用了多种DSL─事实上很多项目这么做了.在1.4节中,一直强调,DSL只是模型的一个薄壳,这个模型可能是程序库,也可能是框架.这句话提醒我们,当考虑DSL的优劣时,一定要分清它是来自DSL的模型,还是DSL本

《领域特定语言》一2.3DSL的问题

2.3DSL的问题 前面已经讨论了何时该采用DSL,接下来就该谈论什么时候不该采用DSL,或者至少是使用DSL应注意的问题.从根本上说,不使用DSL的唯一原因就是,在你的场景下,使用DSL得不到任何好处,或者,至少是DSL的好处不足以抵消构建它的成本.虽然DSL在有些场合下适用,但同样会带来一些问题.总的来说,我认为通常是高估了这些问题,一般人们不太熟悉如何构造DSL,以及DSL如何适应更为广阔的软件开发图景.还有,许多常提及的DSL问题混淆了DSL和模型,这也伤及了DSL的优势.许多DSL问题

《领域特定语言》一3.3 文法、语法和语义

3.3 文法.语法和语义 如果要处理一种语言的语法,文法是一种很重要的工具.文法是一组规则,用以描述如何将文本流转化为语法树.大多数程序员都会在生命中的某一刻接触文法,因为文法常用以描述我们日常使用的程序设计语言.文法由一系列产生式规则组成,每个生产规则都有一个名字(term)以及一个描述如何分解它的语句(statement).所以,一个加法语句可能看起来就像这样: additionStatement:=number'+'number.它告诉我们,如果遇到语句5+3,解析器能够将其识别为加法语句

《领域特定语言》一1.3 为格兰特小姐的控制器编写程序

1.3 为格兰特小姐的控制器编写程序 至此,我们已经实现了状态机模型,接下来,就可以为格兰特小姐的控制器编写程序了,如下所示: Event doorClosed = new Event("doorClosed", "D1CL"); Event drawerOpened = new Event("drawerOpened", "D2OP"); Event lightOn = new Event("lightOn&quo

《领域特定语言》一1.4 语言和语义模型

1.4 语言和语义模型 在这个例子之初,我谈到了构建一个状态机模型.这种模型的存在,以及它同DSL的关系,是至关重要的.在这个例子里,DSL的角色就是组装状态机模型.因此,当解析定制语法的版本时,遇到: events doorClosed D1CL 会创建一个新的事件对象(new Event("doorClosed","D1CL")),把它保存在一边(在一个 "符号表"(第14章)里),这样,遇到doorClosed=>active时,就可

《领域特定语言》一1.5使用代码生成

1.5使用代码生成 在迄今为止的讨论中,要处理DSL,组装"语义模型"(第11章),然后执行语义模型,提供我们希望从控制器得到的行为.在语言圈子里,这种方式称为解释(interpretation).在解释文本时,会先解析文本,然后程序立刻产生结果.(在软件圈子里,解释是个棘手的词语,它承载了太多含义,然而,这里严格限制为立即执行的形式.)在语言领域里,与解释相对的是编译.在编译(compilation)时,先解析程序文本,产生中间输出,然后单独处理输出,提供预期行为.在DSL的上下文里