3.2解析器的工作方式
所以,内部DSL和外部DSL的差别主要体现在解析上。虽然二者确实存在一些细节上的不同,但它们也有很多共通之处。
一个最重要的共同点就是,解析都是一个很强的层级操作。当解析文本时,把数据块组织成一个树结构。考虑一个简单结构,状态机中的事件列表。在外部DSL语法中,它看起来如下所示:
events
doorClosed D1CL
drawerOpened D2OP
end
这个复合结构是一个事件列表,包含一系列事件,每个事件都有名字和代码。
用Ruby编写的内部DSL与上述代码很类似:
event :doorClosed "D1CL"
event :drawerOpened "D2OP"
对于整个列表,这里没有显式的标记,但是每一个事件本身仍是一个层级:每个事件都有表示名字的符号和表示代 码的字符串。
无论何时看到这样的脚本,都可以把它想象为一个层级,这样的层级称为语法树(或者解析树)。任何脚本都可以转化为许多潜在的语法树─这取决于如何分解它。相对于单词(word),语法树是一种更有效的脚本表现形 式,因为可以遍历语法树,使用各种不同的方式来对它进行操作。
如果用到“语义模型”(第11章),可以把一个语法树翻译成语义模型(见图3-2)。如果经常读一些语言社区的资料,我们会发现,语法树得到了非常多的关注─人们通常直接执行语法树,或者基于语法树生成代码。更有效的做法是,语法树可以直接当做语义模型来使用。但大多数时候,我不会这么做,因为语法树同DSL脚本关联非常紧密,这样做只会让DSL的处理同语法产生耦合。
目前为止,我都一直都在谈论语法树,仿佛它是系统里一种有形的数据结构,就像XML DOM一样。有时候,它的确是,但更多的时候,它不是。很多时候,语法树在调用栈中形成,在遍历的过程中得到处理。所以,我们看不到整个树,而只能看到当前处理的分支(类似于XML SAX的工作方式)。尽管如此,尝试理解隐匿于调用栈中鬼魅般的语法树总是有帮助的。对一个内部DSL而言,语法树的形成有赖于方法调用(“嵌套函数”(第34章))的实参和嵌套对象(“方法级联”(第35章))。有时候,我们看不到一个很明显的层次结构,不得不进行模拟(有层次结构的“函数序列”(第33章)可以由“语境变量”(第13章)模拟)。语法树或许形似鬼魅,但它依然是一种有益的脑力工具。使用外部DSL会产生一个更加显式的语法树,事实上,有时候我们确实生成了一个完完全全的语法树数据结构(“树的构建”(第24章))。但即使是外部DSL,通常在处理过程中,也是在调用栈中不断形成和修剪着语法树。(这里引用了几个尚未描述的模式,如果是第一次读到,放心略过即可,但以后再读,这些引用会很有帮助。)