Welcome

首页 / 软件开发 / 数据结构与算法 / 如何设计一门编程语言(十) 正则表达式与领域特定语言(DSL)

如何设计一门编程语言(十) 正则表达式与领域特定语言(DSL)2014-08-23几个月前就一直有博友关心DSL的问题,于是我想一想,我在gac.codeplex.com里面也创建了一些DSL,于是今天就来说一说这个事情。

创建DSL恐怕是很多人第一次设计一门语言的经历,很少有人一开始上来就设计通用语言的。我自己第一次做这种事情是在高中写这个傻逼ARPG的时候了。当时做了一个超简单的脚本语言,长的就跟汇编差不多,虽然每一个指令都写成了调用函数的形态。虽然这个游戏需要脚本在剧情里面控制一些人物的走动什么的,但是所幸并不复杂,于是还是完成了任务。一眨眼10年过去了,现在在写GacUI,为了开发的方便,我自己做了一些DSL,或者实现了别人的DSL,渐渐地也明白了一些设计DSL的手法。不过在讲这些东西之前,我们先来看一个令我们又爱(对所有人)又恨(反正我不会)的DSL——正则表达式!

一、正则表达式

正则表达式可读性之差我们人人都知道,而且正则表达式之难写好都值得O’reilly出一本两厘米厚的书了。根据我的经验,只要先学好编译原理,然后按照.net的规格自己撸一个自己的正则表达式,基本上这本书就不用看了。因为正则表达式之所以要用奇怪的方法去写,只是因为你手上的引擎是那么实现的,所以你需要顺着他去写而已,没什么特别的原因。而且我自己的正则表达式拥有DFA和NFA两套解析器,我的正则表达式引擎会通过检查你的正则表达式来检查是否可以用DFA,从而可以优先使用DFA来运行,省去了很多其实不是那么重要的麻烦(譬如说a**会傻逼什么的)。这个东西我自己用的特别开心,代码也放在gac.codeplex.com上面。

正则表达式作为一门DSL是当之无愧的——因为它用了一种紧凑的语法来让我们可以定义一个字符串的集合,并且取出里面的特征。大体上语法我还是很喜欢的,我唯一不喜欢的是正则表达式的括号的功能。括号作为一种指定优先级的方法,几乎是无法避免使用的。但是很多流行的正则表达式的括号竟然还带有捕获的功能,实在是令我大跌眼镜——因为大部分时候我是不需要捕获的,这个时候只会浪费时间和空间去做一些多余的事情而已。所以在我自己的正则表达式引擎里面,括号是不捕获的。如果要捕获,就得用特殊的语法,譬如说(<name>pattern)把pattern捕获到一个叫做name的组里面去。

那我们可以从正则表达式的语法里面学到什么DSL的设计原则呢?我认为,DSL的原则其实很简单,只有以下三个:

短的语法要分配给常用的功能

语法要么可读性特别好(从而比直接用C#写直接),要么很紧凑(从而比直接用C#写短很多)

API要容易定义(从而用C#调用非常方便,还可以确保DSL的目标是明确又简单的)

很多DSL其实都满足这个定义。SQL就属于API简单而且可读性好的那一部分(想想ADO.NET),而正则表达式就属于API简单而且语法紧凑的那一部分。为什么正则表达式可以设计的那么紧凑呢?现在让我们来一一揭开它神秘的面纱。

正则表达式的基本元素是很少的,只有连接、分支和循环,还有一些简单的语法糖。连接不需要字符,分支需要一个字符“|”,循环也只需要一个字符“+”或者“*”,还有代表任意字符的“.”,还有代表多次循环的{5,},还有代表字符集合的[a-zA-Z0-9_]。对于单个字符的集合来讲,我们甚至不需要[],直接写就好了。除此之外因为我们用了一些特殊字符所以还得有转义(escaping)的过程。那让我们数数我们定义了多少字符:“|+*[]-{},.()”。用的也不多,对吧。

尽管看起来很乱,但是正则表达式本身也有一个严谨的语法结构。关于我的正则表达式的语法树定义可以看这里:https://gac.codeplex.com/SourceControl/latest#Common/Source/Regex/RegexExpression.h。在这里我们可以整理出一个语法:

DIGIT ::= [0-9]LITERAL ::= [^|+*[]-\{}^,.()]ANY_CHAR ::= LITERAL | "^" | "|" | "+" | "*" | "[" | "]" | "-" | "" | "{" | "}" | "," | "." | "(" | ")"CHAR::= LITERAL::= "" ANY_CHARCHARSET_COMPONENT::= CHAR::= CHAR "-" CHARCHARSET::= CHAR::= "[" ["^"] { CHARSET_COMPONENT } "]"REGEX_0::= CHARSET::= REGEX_0 "+"::= REGEX_0 "*"::= REGEX_0 "{" { DIGIT } ["," [ { DIGIT } ]] "}"::= "(" REGEX_2 ")"REGEX_1::= REGEX_0::= REGEX_1 REGEX_0REGEX_2::= REGEX_1::= REGEX_2 "|" REGEX_1REGULAR_EXPRESSION::= REGEX_2
这只是随手写出来的语法,尽管可能不是那么严谨,但是代表了正则表达式的所有结构。为什么我们要熟练掌握EBNF的阅读和编写?因为当我们用EBNF来看待我们的语言的时候,我们就不会被愈发的表面所困扰,我们会投过语法的外衣,看到语言本身的结构。脱别人衣服总是很爽的。