我觉得这几周学到了很多和以前所知不太相同的东西,大致记录一下。
最主要的一个方面,源自下面这段代码:
type stateFn func(*lexer) stateFn
for state := startState; state != nil; {
state = state(lexer)
}
(这里是YouTube视频地址:http://youtu.be/HxaD_trXwRE;
演讲的稿子在这里:http://rspace.googlecode.com/hg/slide/lex.html)
这段代码是用go语言写的(go语言在大部分情况下句尾无需加分号,for语句不需要加括号),下面是用类C的语法写的版本(注意:下面的代码不能执行):
typedef stateFn (*stateFn)(lexer*);
for (stateFn state = startState; state != NULL;) {
state = state(lexer);
}
单从语法上看,对我而言有两点很特别:
- 递归的函数类型定义。(C好像无法做到,C++可以通过特定技巧实现(链接:http://www.gotw.ca/gotw/057.htm))在这里,它这样定义了stateFn:返回stateFn类型的函数就是stateFn函数类型;当然,这个函数类型还得接受一个指向lexer类型的指针作为参数(典型的用C来实现OO设计的方法,本篇中暂不讨论)。
- 原来类似函数指针的机制能实现这样的程序逻辑表达。把函数看作普通变量,为其定义一个类型,就能把所有符合该类型定义的函数……同等对待。
要体会这段代码的妙处,先要了解它的用处,最好还要像我这样体会没用它的难处,呵呵。
用处:
既然使用在词法器里,脑子里回忆起编译原理书上的随便一个DFA图(DFA:确定有限自动机)。针对词法器,最简单的,就是从起始状态开始,读入一个字符,跳到下一个状态,再读入一个字符,跳到下一个状态,……,最终跳到终止状态,整段代码就被读入了。
实现中,往往使用一个特定的函数(比如:readNumber())来处理当前的状态(比如:发现读入了一个数字类型的起始字符)。那么,因为DFA里有多个状态,往往就添加多个这样的函数来对每个状态分别进行操作。
显然,状态之间需要有完善的跳转机制,而机制就实现在这些函数里面。遇到什么字符的时候跳?跳到哪里?上面这段代码的用处就是从最抽象的层面表达了状态跳转的机制。
我所遇到的难处:
关于跳转到哪里,我想到两种设计。举个例子说明,假设当前程序运行在readNumber()里,也就是说,词法器处在读取数字的状态;假设readNumber()里有一个循环,每读入一个字符就判断其是否属于数字;当该循环读入一个不属于数字的字符时,要么跳转到DFA的起始状态,交由其判断逻辑来决定接下来是跳到操作符,还是跳到变量名……,要么直接在readNumber()内部实现判断逻辑,直接跳转到另一个内部状态。
两种设计各有利弊。前者在不使用额外状态变量的情况下会导致在起始状态做不必要的判断;比如,如果是从readNumber()跳出,那么在起始状态的函数里就不需要再对其是否属于数字做判断。而后者将迫使实现中为每个状态函数添加有很大重复的判断逻辑代码,不好维护,特别是如果要在未来添加一个状态的话,就需要在所有状态函数中都添加其判断逻辑,牵一发而动全身;极端的实现例子,就是我的goto版了。
两种设计我都尝试了,但总觉得缺了点什么。
嘿嘿,妙处:
- “状态”是一个名词(英文:state),按照平常的思维,名词常可对应OO设计里的一个对象;而这里,状态在程序中的实质是函数,它是一段运行逻辑,不是一个数据对象。我在看到上面那段代码之前,只想到这儿。我不知道,原来函数可以和对象一样,被归在一个类型(stateFn)里。
- 词法器用的是DFA的模型,也就是说,它的结构是图,而且其图中的状态节点在逻辑上还有一定的层次之分。我在看到上面那段代码之前,也只想到这儿。钻在细节里的时候,我没想到,退一步看,所有的状态其实都是一样的:无论当前在哪个状态,它在停止前总会遇到一个跳转,然后跳到另一个状态。
- 结合上面两点思路,就导致了这段代码的设计。为所有状态函数定义一个类型(stateFn),让它们返回所要跳转到的下一个状态函数,把“进入状态->状态运行->跳转进入下一个状态”的过程放到for循环里面;这个循环结构,就像齿轮,做着最简单的旋转,却能带动各种各样的机械运动。
- 注意,其实这并没有解决上面提到的两种设计各有利弊的问题!这段代码所带来的好处在于为这个词法器实现了一个异常简单的操作接口,具体下面的状态函数之间的跳转关系到底怎么样,被完全规避了;这样,其实我就可以随便选择两种设计的平衡,而不用去担心给“用户”添加理解上的困难。
这篇写到这里先吧,另加一句,我用C++实现了这个结构,用g++编译器-O3优化生成的程序就比goto版稍微慢一点点:)用函数指针其实和用goto在逻辑上有很大相似性的。
注:上面提到C++可以使用特殊技巧实现递归的函数类型定义,下面是代码:
struct stateFn;
typedef stateFn (*stateFP)();
struct stateFn {
stateFn(stateFP fp) : p(fp) {}
operator stateFP() { return p; } // 提供从stateFn到stateFP的隐式类型转换
stateFP p;
};