BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

在CLR之上的构建领域特定语言

| 作者 Oren Eini 关注 0 他的粉丝 , Ayende Rahien 关注 0 他的粉丝 ,译者 张善友 关注 0 他的粉丝 发布于 2008年5月27日. 估计阅读时间: 21 分钟 | ArchSummit北京2018 共同探讨机器学习、信息安全、微服务治理的关键点

最近领域特定语言(DSL,Domain Specific Languages)这个话题比较热门。这可以从Rails现象中看到。Rails的流行以及Rails上广泛使用的领域特定语言(从现在起叫DSL),已经引起了对DSL的广泛兴趣。

到现在为止开发人员有这样的印象,建立一个DSL,你需要专业的编译器理论知识,理解Lex和Yacc的内部工作原理并需要投入大量的时间来构建DSL。结果是极少数人愿意去尝试,他们都是从头开始构建自己的语言。

这往往是成本高昂。

同时,动态语言的爱好者可以毫不费力的利用他们喜欢的动态语言的动态特性来构建领域特定语言。事实上,他们中的许多以这种方式构建的任何应用程序,都有着显著的复杂性。

这两种方法的差别有重要意义。第一种方式是创建属于自己的语言,就是所谓外部的DSL(External DSL)。这是一个耗资巨大的项目,因为一切都要从头开始构建,需要考虑运算符的优先级规则、运行时类库、执行代码、错误处理和I/O。第二种方法是利用和修改宿主语言,就是所谓内部的DSL(Internal DSL)。这些都容易构建和维护。你只需要考虑如何修改,所有的其它东西(通常是你不用关心的)都已经被宿主语言处理了。

另一种做法是构建连贯接口(Fluent Interface),把它叫做DSL。我认为这不是一种DSL,这种方法往往在语言的自由性方面受到很大的限制。Java和C#就是很好的例子,包括 Java 6和C# 3。你可以列举许多语言方面的API,但这不能让我觉得这是一个DSL。

在任何情况下,我的个人偏好是使用具有很高语法灵活性的内部DSL。因为我基本上都是在CLR上工作,我想利用运行在这个平台上的宿主语言。它可以让我重用大部分的使用CLR的知识,不要低估这方面的好处。在你的手中有一个熟悉的环境是非常重要的。

在深入语言之前,看看究竟什么是“高语法灵活性的语言”,怎么样?为内部DSL提供一个良好的宿主环境的语言需要具有哪些特性?

我需要有合适的手段来表达我的想法。这可以通过有启发性的命名,表达特定域的概念,并通常和通用语言的做法不一样。你希望能够创建一个第四代语言,这很容易做到。让我们从一个我们电子表格所使用的脚本这样简单的DSL开始如何?

你的任务就是创建乘法网格。

for x in range(100):
for y in range(100):
cell[ x+1 , y+1 ] = x * y
formula x, 100, sum( x1, x100 )

这是不是真的令人印象深刻呢?这和编程语言几乎完全一样,代码也是微不足道。除了和用Excel的自动化API做一样的工作外,更简短。

注意到这就是我们所用到的所有代码。我们不需要一个类的定义,或者是一个主方法。这是一个没有任何语法包袱的可执行的DSL脚本。

如果前面的例子没有给你留下深刻的印象,看看如何定义订单折扣的业务规则:

apply_discount_of 5.percent:
when order.Total > 1000 and customer.IsPreferred
when order.Total > 10000

suggest_registered_to_preferred:
when order.Total > 100 and not customer.IsPreferred

这看起来和编程语言有很大不同,它更像业务分析师在Word文档中定义的业务规则。

从我的角度来看,上面两个例子都是领域特定语言。他们只是表达领域的方法和风格不同。这两个例子,我们实际上已经从语言中移除了和我们的领域没有直接关系的东西。这使得我们可以专注于域,并希望有良好的工具来处理。

除了域概念以外,没有任何东西可以和具有与域相匹配的语法是一样重要的。

当我们开始在CLR上研究高语法灵活性的语言的时候,我们有很多的选择。我们来评估几个语言。我们将从几个来自微软的语言开始。

C# —— 这是一个非常刚性的语言,类型定义,没有独立的方法/代码块,僵硬的语言。所有这些特性使得C#不是一个DSL宿主语言的好选择。他也可以做到的,但它不如其他方法。

VB.Net —— 其实VB.Net更适合面向对象的语言,因为它使用了许多英文单词作为关键字和操作符。令人遗憾的是它也是一个非常冗长的语言,我们要减少冗余性适合我们的域概念。

JScript —— 这可能引来一片笑声,但是JScript是一个非常灵活的语言,为许多事情提供了较好的语法。只要去看看所提供的所有Javascript类库。JScript提供了和Javascript相同的基础功能。虽然这样,有一点不得不考虑的是你可以做到像JQuery或 Prototype那样多大的灵活性。然而它不够成熟,我不确定将来是什么样子的。虽然它在很多方面有灵活的语法,给人有种编程语言的感觉,这会让我在一个DSL中觉得分心。

F# —— 这是一门由微软开发的,将来会发布的函数式编程语言。F#支持面向对象编程。我已经简略的浏览过这门语言。虽然F#的强大功能令人印象深刻,从我的角度来看,它看起来是BNF【译者注:BNF, Backus-Naur Form的缩写,巴科斯范式一种使用形式化符号来描述给定语言的语法。】定义,其他什么都不像。毫无疑问这是由于作者缺乏函数式编程语言经验方面的问题,但是我不只是考虑它的可读性。

我们已经看完微软开发的语言。让我们看得更远些。据统计去年CLR上运行的语言超过了一百种,所以我选择了两种我认为是DSL宿主语言的候选语言。

Nemerle是一个多范型的语言(面向对象和函数式),完全支持编译器宏(后来更多的是Lisp的变种,而不是C++),以及许多其他的东西,这使得它是一个DSL很好的宿主语言。这不是我阅读Nemerle代码的简单理由(经常是这样)。

Boo是一个基于Python语法的静态类型的面向对象的语言。它支持宏(也是Lisp变种),有一个开放的编译器管道和更容易构建DSL的特性。Boo是我首选的用于构建DSLs的语言,但是为了保证客观性,我们需要在讨论这个主题之前证明Boo有多么的强大。

动态语言运行时(DLR)怎么样呢?

到目前为止我还没有讨论动态语言运行时,这是一个在CLR之上支持动态语言的微软项目(目前支持Ruby,Python和EcmaScript)。

更具体的来说,当人们讨论DLR的时候,他们是在讨论IronRuby和IronPython。

Ruby是一门被证明非常适合写内部DSL的语言,在CLR上运行可以使我们在熟悉的环境下工作。

使用DLR作为一个DSL的平台当然是可能的,但是我至少在一段时间内不使用它。DLR和IronRuby本身都还是在开发之中。我不认为微软对发布日期会有任何的承诺,此外我没有发现Ruby能做的Boo做不了,我觉得Boo的元编程基础功能非常自然和强大。

“自然和非常强大”是什么意思?

让我们深入一点考查Boo。我说它有一个开放的编译器,我并不是指它是开放源代码的(它是,但是和这个无关),我的意思你有办法深入到编译器的内部和在编译的时候打乱编译器的内部对象模型。这意味着我们可以以一种有趣的方式改变编译器的行为。

上面的两个代码示例都是Boo的DSL代码。

全面深入Boo的元编程基础功能超出了本文的范围,但是我可以用一个简单的例子来展示它的威力。

CLR已有IDisposable的概念并配合using语句使用。现在我定义一个ITransactionable,将用它来定义一个事务的声明。

public interface ITransactionable:
def Dispose():
pass
def Commit():
pass
def Rollback():
pass

macro transaction:
return [|
tx as ITransactionable = $(transaction.Arguments[0])
try:
$(transaction.Body)
tx.Commit()
except:
tx.Rollback()
raise
finally:
tx.Dispse()
|]

只需要这个代码,我们就可以作为一个头等语言要素来使用该事务的声明了(实际上,这也正是using语句在Boo的实现方式)。

transaction GetNewDatabaseTransaction():
DoSomethingWithTheDatabase()

现在,如果代码里面的事务抛出了一个异常,事务将自动回滚。如果它执行是成功的,事务就自动提交。

不过这只是使用Boo的一个示例。并注意这里我介绍的唯一一个概念就是宏和有趣的符号[||]。没有更深入的讨论,这指示编译器在事务块内部的代码用宏的内容做了一个代码的替换。

很重要的是这已经超越文本替换,我们直接修改AST(Abstract Syntax Tree,抽象语法树---编译器对象模型)。这是一个微不足道(但很强大)的例子。我们下面将探讨一个更复杂的场景,这将告诉我们为什么这样的区分是重要的。

为了构建一个DSL,这个级别的功能还是不够的。你可以只使用Boo语言的语法而不使用元编程功能,类似于Ruby,有很多可选的语法,这在很多场景是非常有用的。举个例子来说,我们可以不通过元编程创建相同的语法,但是利用Boo的这一特性,如果方法的最后一个参数是一个委托(闭包,块等等),可以给方法传递一个代码块。

比如:

def transaction(tx as ITransactionable, transactionalAction as ActionDelegate):
try:
transactionalAction()
tx.Commit()
except:
tx.Rollback()
raise
finally:
tx.Dispse()

我们仍然可以使用这代码,正如我们前面所用的:

transaction GetNewDatabaseTransaction():
DoSomethingWithTheDatabase()

从语法上来看,是没有差别的。 不过这两个版本还是有微小的差别。CLR确保了如果try程序块能够成功执行,就进入try程序块执行。这是using()语句正确执行的关键,其他的场景也是一样的。

第一个版本可以利用这个能力,第二个版本不能。(原因是第二个版本是在运行时调用方法,而第一个版本只是替换事务代码块得到修改后的结果。)

我们还可以利用Boo的元编程做些什么呢?相当多,关于这个内容可以写一本书(实际上我已经写了这方面的一本书:-) )。作为一个简单的例子,而不一定是良好设计的的最好例子,你可以修改语言的if语句的语义。

有一次我不得不这样做,我把if语句修改下面模式:

if foo == null:
# do something

为这个模式:

if foo == null or foo isa NullObject:
# do something

现在,当我们检查null的时候,我们也检查这个对象是否是NullObject的实例,NullObject是我的应用程序中的一个自定义类型。这使得在我的应用程序以一种自然的方式使用NullObject模式。

val = NullObject() # set val to a new instance of NullObject
if val == null: # will be compiled as val == null or val isa NullObject
print "Value is null"
else:
print "Value is not null"

我们已经扩展了语言认为所有继承自NullObject的对象作为null。

从长远来看,有能力去修改语言的基本组成部分是我的工作(和语言的使用)容易得多。

在继续下一步之前来看最后一个例子。我想告诉你如何在Boo应用程序中使用不到20行代码添加一个(简单的)‘契约式设计’(类不变式)。这是代码:

[AttributeUsage(AttributeTargets.Class)]
class EnsureAttribute(AbstractAstAttribute):

expr as Expression

def constructor(expr as Expression):
self.expr = expr

def Apply(target as Node):
type as ClassDefinition = target
for member in type.Members:
method = member as Method
continue if method is null
block = method.Body
method.Body = [|
block:
try:
$block
ensure:
assert $expr
|].Block

用法如下:

[ensure(name is not null)]
class Customer:
name as string

def constructor(name as string):
self.name = name

def SetName(newName as string):
name = newName

现在任何把名字设置为null都将导致一个断言异常。这个技术相当强大和容易使用。我将前置条件的标记的实现留给读者。

这个例子也示范了直接使用编译器的对象模型(AST)的强大。我们不局限于C++宏的文本替换,我们可以查询对象模型并以非常自然的方式修改它。那么现在我想你相信Boo是一个非常适合构建DSL的语言。我只是从表面上浏览了一下它的潜力,还有很多有待于你的探索 。

其他几项优势:Boo是静态编译类型的语言,这意味着你的DSL拥有标准CLR代码的所有优势(即时编译器、垃圾回收、调试等等)。从性能角度来看,你的应用程序代码和DSL代码没有任何区别。

因此基于Boo的DSL对于经常需要修改和需要高性能的代码是理想的选择。在产品中不得不改变的这样的共同的需求往往推动人们使用基于XML的系统,规则引擎等。即使没有考虑完全“用XML编程”这样的辩论,这些选择都遭遇了低性能的问题。

建立一个使用一系列DSL脚本的系统很容易,从长远来说,是要提供高性能和高度可维护性的系统。 他还需要配合领域驱动设计,因为有一个领域特定语言更容易表达自然的域概念。

有几个公开的Boo DSL。

我个人喜欢的是Binsor。Binsor是一个Caste Windsor IoC容器的配置DSL,使得使用IoC的高级概念易如反掌。你需要了解Binsor的更多信息可以通过访问Binsor 2.0发布说明。其它用Boo的DSL是:

Specter是一个行为驱动开发(BDD)框架,提供了一个非常自然的方式书写规格并将规格转换为NUnit测试用例。

Brail是一个文本模板语言。

还有几个,但是使用它的人很少,并没有广为人知。

写一个DSL要求有一些初步知识,但是知识是很简单和容易获取的。一旦你有了这个基础知识,你就可以开始写一个DSL就像制作一个表单那么简单。

实际上我已经写了一个后端处理系统,大部分是从各种来源处理消息的DSL组成的。在这个系统中,我写DSL就像在我的表现层上写表单一样的。

总体来说,Boo是一个用于构建DSL非常好的语言。使用Boo写DSL有助于降低成本,没有性能和灵活性方面的妥协。此外,他还提供了自由的语法和自然的方式表达域的概念。

在最后结束时说一句,Boo也可以在Java上运行

关于作者

Oren Eini,也叫Ayende Rahien,是一个经验丰富的.NET开发者和架构师。他也是好几个开源项目的贡献者,例如NHibernate和Castle。此外,Ayende是 Rhino Mocks、Rhino Commons和NHibernate Query Analyzer的创始人。关于Boo,Ayende创建了Castle MonoRail的模板语言Brail,配置Castle Windsor IoC容器的DSL,他还写了一本标题为《Building Domain Specific Languages in Boo》的书。

查看英文原文:Building Domain Specific Languages on the CLR》。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

boo 的可扩展编译架构得确很有意思 by jianxiao jiang

宏的写法也简单化了,只是这语言过于小众了

Manning要出一本Boo的书 by 刘江 图灵

不知道这算不算进入主流的一种标志?

是不是在推销Boo? by Lee Lawrence

其实微软也没那么糟糕,在Oracs SDK中就已经包含了完整的DSL的工具和设计器

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

3 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT