电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

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


发布日期:2020/11/1
 

最近领域特定语言(DSLDomain 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 和C# 你可以列举许多语言方面的API但这不能让我觉得这是一个DSL在任何情况下我的个人偏好是使用具有很高语法灵活性的内部DSL因为我基本上都是在CLR上工作我想利用运行在这个平台上的宿主语言它可以让我重用大部分的使用CLR的知识不要低估这方面的好处在你的手中有一个熟悉的环境是非常重要的

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

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

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

for x in range(): for y in range():

cell[ x+ y+ ] = x * y

formula x sum( x x )

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

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

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

apply_discount_of percent:

when orderTotal > and customerIsPreferred

when orderTotal > suggest_registered_to_preferred:

when orderTotal > and not customerIsPreferred

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

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

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

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

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

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

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

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

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

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

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

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

到目前为止我还没有讨论动态语言运行时这是一个在CLR之上支持动态语言的微软项目(目前支持RubyPython和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():

passmacro transaction: return [|

tx as ITransactionable = $(transactionArguments[])

try:

$(transactionBody)

txCommit()

except:

txRollback()

raise

finally:

txDispse()

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

transaction GetNewDatabaseTransaction() DoSomethingWithTheDatabase()

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

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

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

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

比如

def transaction(tx as ITransactionable transactionalAction as ActionDelegate):

try:

transactionalAction()

txCommit()

except:

txRollback()

raise

finally:

txDispse()

我们仍然可以使用这代码正如我们前面所用的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 NullObjectif val == null # will be compiled as val == null or val isa NullObject print Value is nullelse print Value is not null我们已经扩展了语言认为所有继承自NullObject的对象作为null从长远来看有能力去修改语言的基本组成部分是我的工作(和语言的使用)容易得多

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

[AttributeUsage(AttributeTargetsClass)]class EnsureAttribute(AbstractAstAttribute):

expr as Expression def constructor(expr as Expression):

selfexpr = expr def

Apply(target as Node):

type as ClassDefinition = target for member in typeMembers:

method = member as Method continue if method is null

block = methodBody

methodBody = [|

block:

try:

$block

ensure:

assert $expr

|]Block

用法如下

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

name as string def constructor(name as string):

selfname = name def SetName(newName as string):

name = newName

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

这个例子也示范了直接使用编译器的对象模型(AST)的强大我们不局限于C++宏的文本替换我们可以查询对象模型并以非常自然的方式修改它那么现在我想你相信Boo是一个非常适合构建DSL的语言我只是从表面上浏览了一下它的潜力还有很多有待于你的探索 其他几项优势Boo是静态编译类型的语言这意味着你的DSL拥有标准CLR代码的所有优势(即时编译器垃圾回收调试等等)从性能角度来看你的应用程序代码和DSL代码没有任何区别

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

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

有几个公开的Boo DSL我个人喜欢的是BinsorBinsor是一个Caste Windsor IoC容器的配置DSL使得使用IoC的高级概念易如反掌你需要了解Binsor的更多信息可以通过访问Binsor 发布说明其它用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 MocksRhino Commons和NHibernate Query Analyzer的创始人关于BooAyende创建了Castle MonoRail的模板语言Brail配置Castle Windsor IoC容器的DSL他还写了一本标题为《Building Domain Specific Languages in Boo》的书

上一篇:WCF从理论到实践四:路在何方

下一篇:SNS网站中怎样获取MSN联系人信息