最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

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

  • 2008-05-26
  • 本文字数:6039 字

    阅读完需:约 20 分钟

最近领域特定语言(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):<br></br> for y in range(100):<br></br> cell[ x+1 , y+1 ] = x * y<br></br> formula x, 100, sum( x1, x100 )这是不是真的令人印象深刻呢?这和编程语言几乎完全一样,代码也是微不足道。除了和用 Excel 的自动化 API 做一样的工作外,更简短。

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

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

apply_discount_of 5.percent:<br></br> when order.Total > 1000 and customer.IsPreferred<br></br> when order.Total > 10000<p>suggest_registered_to_preferred:</p><br></br> 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:<br></br> def Dispose():<br></br> pass<br></br> def Commit():<br></br> pass<br></br> def Rollback():<br></br> pass<p>macro transaction:</p><br></br> return [|<br></br> tx as ITransactionable = $(transaction.Arguments[0])<br></br> try:<br></br> $(transaction.Body)<br></br> tx.Commit()<br></br> except:<br></br> tx.Rollback()<br></br> raise<br></br> finally:<br></br> tx.Dispse()<br></br> |]只需要这个代码,我们就可以作为一个头等语言要素来使用该事务的声明了(实际上,这也正是 using 语句在 Boo 的实现方式)。

transaction GetNewDatabaseTransaction():<br></br> DoSomethingWithTheDatabase()现在,如果代码里面的事务抛出了一个异常,事务将自动回滚。如果它执行是成功的,事务就自动提交。

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

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

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

比如:

def transaction(tx as ITransactionable, transactionalAction as ActionDelegate):<br></br> try:<br></br> transactionalAction()<br></br> tx.Commit()<br></br> except:<br></br> tx.Rollback()<br></br> raise<br></br> finally:<br></br> tx.Dispse()我们仍然可以使用这代码,正如我们前面所用的:

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

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

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

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

if foo == null:<br></br> # do something为这个模式:

if foo == null or foo isa NullObject:<br></br> # do something现在,当我们检查 null 的时候,我们也检查这个对象是否是 NullObject 的实例,NullObject 是我的应用程序中的一个自定义类型。这使得在我的应用程序以一种自然的方式使用 NullObject 模式。

val = NullObject() # set val to a new instance of NullObject<br></br>if val == null: # will be compiled as val == null or val isa NullObject<br></br> print "Value is null"<br></br>else:<br></br> print "Value is not null"我们已经扩展了语言认为所有继承自 NullObject 的对象作为 null。

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

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

[AttributeUsage(AttributeTargets.Class)]<br></br><span color="#0000ff">class</span> EnsureAttribute(AbstractAstAttribute):<p> expr as Expression</p><p><span color="#0000ff">def constructor</span>(expr as Expression):</p><br></br> self.expr = expr<p><span color="#0000ff">def</span> Apply(target as Node):</p><br></br> type as ClassDefinition = target<br></br><span color="#0000ff">for</span> member <span color="#0000ff">in</span> type.Members:<br></br> method = member as Method<br></br> continue if method is null<br></br> block = method.Body<br></br> method.Body = [|<br></br> block:<br></br> try:<br></br> $block<br></br> ensure:<br></br> assert $expr<br></br> |].Block用法如下:

[ensure(name <span color="#0000ff">is not null</span>)]<br></br><span color="#0000ff">class</span> Customer:<br></br> name <span color="#0000ff">as string</span><p><span color="#0000ff">def constructor</span>(name <span color="#0000ff">as string</span>):</p><br></br> self.name = name<p><span color="#0000ff">def</span> SetName(newName <span color="#0000ff">as string</span>):</p><br></br> 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 》。

2008-05-26 09:291671
用户头像

发布了 45 篇内容, 共 65702 次阅读, 收获喜欢 1 次。

关注

评论

发布
暂无评论
发现更多内容

创新技术领航者!华为云GaussDB获颁2022年云原生数据库领域权威奖项

科技云未来

Spring源码分析(三)Spring是如何把元素解析成BeanDefinition对象的

石臻臻的杂货铺

spring 源码 8月月更

【Django | 开发】 为已有遗留系统数据库生成管理后台

计算机魔术师

8月月更

直播预告 | Homebrew 作者 Max Howell:如何打造杰出的开发者工具

思码逸研发效能

开源 研发效能 开发者工具 开发工具 科技

加速拥抱数字化,云时通打造全新数字中台!

创意时空

一文快速上手 Nacos 注册中心+配置中心!

王磊

Java SpringCloud

Apache DolphinScheduler 简单任务定义及复杂的跨节点传参

Apache DolphinScheduler

工作流调度 海豚调度 调度器 大数据调度 任务传参

长安链国密TLS设计和实现学习

[CSS入门到进阶] 4行CSS实现footer置底!超常见的需求,快来收藏

HullQin

CSS JavaScript html 前端 8月月更

在线XML转HTMLTable工具

入门小站

工具

Spring源码解析(四)Spring是怎么处理BeanDefinition的?

石臻臻的杂货铺

spring 源码 8月月更

一文详解产品经理与项目经理的区别

产品海豚湾

产品经理 职业发展 项目经理 产品设计与思考 9月月更

长安链源码分析启动(7)

长安链

如何正确理解Java对象创建过程,我们主要需要注意些什么问题?

PivotalCloud

飞向深空:ColorOS 超算平台带来性能之变

脑极体

日常工作最常用6大Git命令讲解

流浪的漂流瓶

git git stash 8月月更 git命令

C/C++模板类模板与函数模板区别,以及用法详解

CtrlX

c c++ 面向对象 模板方法 8月月更

学习 Go 语言数据结构:实现哈希表

宇宙之一粟

哈希表 8月月更

乘云远航|共赢新征程,打造数字化供应链竞争力!

创意时空

华为云GaussDB(for Redis)全面对比Codis

科技云未来

揭秘华为云GaussDB(for Redis)六大秒级能力盘点

科技云未来

在线图片隐写术解密解码传递数据

入门小站

工具

上了NVMe的路,才能飙起全闪存的车

白洞计划

长安链RPC服务学习笔记

长安链

职场人的未来,GO!咱们上云办公

创意时空

长篇图解etcd核心应用场景及编码实战

字母哥哥

Java etcd #Kubernetes#

【云原生| Docker】 部署 Django & mysql 项目

计算机魔术师

8月月更

每日一R「20」网络编程(二)

Samson

学习笔记 8月月更 ​Rust

【Python | 词云】聊天记录绘制超美词云(七夕快乐 ,曾同学)

计算机魔术师

8月月更

【操作系统 | Linux】介绍与安装(虚拟机)

计算机魔术师

8月月更

使用 ECK 在 Kubernetes 集群中管理 Elastic Stack

Se7en

在CLR之上的构建领域特定语言_.NET_Oren Eini_InfoQ精选文章