XSLT、SQL等都可以算作是外部DSL。外部DSL一般会直接针对特定的领域设计,而不考虑其他方面。James Gosling曾经说过:每个配置文件最终都会变成一门编程语言。一开始你可能只会用它表示一点点东西,慢慢地你便会想要一些规则,而这些规则则变成了表达式,后来你可能还会定义变量,进行条件判断等,而最终它就变成了一种奇怪的编程语言。这样的情况屡见不鲜。现在有一些公司也在关注DSL的开发。例如以前在微软工作的Charles Simonyi提出了Intentional Programming的概念,还有JetBrains公司提供了叫做MPS(Meta Programming System)的产品。最近微软也提出了自己的Oslo项目,而在Eclipse世界里也有Xtext,所以如今在这方面已经有不少尝试。由于外部DSL的独立性,在某些情况下也会出现特定的工具,辅助领域专家或是开发人员编写DSL代码。还有一些DSL会以XML方言的形式提出,利用XML方言的好处在于有不少现成的工具可用,这样可以更快地定义自己的语法。
内部DSL往往只代表一系列特别的API及使用模式,例如LINQ查询语句及Ruby on Rails中的Active Record声明代码等。内部DSL可以使用一系列API来“伪装”成一种DSL,利用一些流畅化的技巧,例如像jQuery那样把一些方法通过“点”连接起来,而另一些也会利用元编程的方式。内部DSL还有一些优势,例如可以访问语言中的代码或变量,以及利用代码补全、重构等母语言的所有特性。
DSL的可读性往往很高。例如,要筛选出单价大于20的产品,并对所属种类进行分组,降序列出每组的分类名称及产品数量。如果是用命令式的编程方式,可能是这样的:
foreach (Product p in products)
{
if (p.UnitPrice >= 20)
{
if (!groups.ContainsKey(p.CategoryName))
{
Grouping g = new Grouping();
g.Name = p.CategoryName;
g.Count = 0;
groups[p.CategoryName] = g;
}
groups[p.CategoryName].ProductCount++;
}
}
var result = new List<Grouping>(groups.Values);
result.Sort(delegate(Grouping x, Grouping y)
{
return
x.Count > y.Count ? -1 :
x.Count < y.Count ? 1 :
0;
});
显然这些代码编写起来需要一点时间,且很难直接看出它的真实目的,换言之,“What”几乎完全被“How”所代替了。这样,一个新的程序员必须花费一定时间才能理解这段代码的目的。但如果使用LINQ,代码便可以改写成:
.Where(p => p.UnitPrice >= 20)
.GroupBy(p => p.CategoryName)
.OrderByDescending(g => g.Count())
.Select(g => new { Name = g.Key, Count = g.Count() });
这段代码更加关注的是“How”而不是“What”,它不会明确地给出过滤的操作方式,也没有涉及到创建字典这样的细节。这段代码还可以利用C# 3.0中内置的DSL,即LINQ查询语句来改写:
from p in products
where p.UnitPrice >= 20
group p by p.CategoryName into g
orderby g.Count() descending
select new { Name = g.Key, Count = g.Count() };
编译器会简单地将LINQ差距语句转化为前一种形式。这段代码只是表现出最终的目的,而不是明确指定做事的方式,这样便可以很容易地并行执行这段代码,如使用PINQ则几乎不需要做出任何修改。
函数式编程
Anders提出的另一个重要的声明式编程方式便是函数式编程。函数式编程历史悠久,如当年的LISP便是函数式编程语言。除了LISP以外还有其他许多函数式编程语言,如APL、Haskell、ML等。函数式编程在学术界已经有过许多研究,大约在5~10年前许多人开始吸收和整理这些研究内容,想要把它们融入更为通用的编程语言。现在的编程语言,如C#、Python、Ruby、Scala等,都受到了函数式编程语言的影响。
使用命令式编程语言写程序时,我们经常会编写如x = x + 1这样的语句,此时我们大量依赖的是可变状态,或者说是变量,它们的值可以随程序运行而改变,可变状态非常强大,但随之而来的便是“副作用”问题,例如一个无需参数的void方法,它会根据调用次数或是在哪个线程上进行调用对程序产生影响,它会改变程序内部的状态,从而影响之后的运行效果。而在函数式编程中则不会出现这个情况,因为所有的状态都是不可变的。事实上对函数式编程的讨论更像是数学、公式,而不是程序语句,如x = x + 1对于数学家来说,似乎只是个永不为真的表达式而已。
函数式编程十分容易并行,因为它在运行时不会修改任何状态,因此无论多少线程在运行时都可以观察到正确的结果。假如两个函数完全无关,那么它们是并行还是顺序执行便没有什么区别。当然,现实中的程序一定是有副作用的,例如向屏幕输出内容,向Socket传输数据等,因此真实世界中的函数式编程往往都会考虑如何将有副作用的代码分离出来。函数式编程默认是不可变的,开发人员必须做些额外的事情才能使用可变状态或是危险的副作用,与之相反,C#或Java必须使用readonly或final来做到这一点。此时,使用函数式编程语言时的思维观念便会有所不同。……