许多东西都可以看成是状态机,我们只看有限状态机的维基:
Finite-state_machine
A finite-state machine (FSM) or finite-state automaton (plural: automata), or simply a state machine, is a mathematical model of computation used to design both computer programs and sequential logic circuits. It is conceived as an abstract machine that can be in one of a finite number of states. The machine is in only one state at a time; the state it is in at any given time is called the current state. It can change from one state to another when initiated by a triggering event or condition; this is called a transition. A particular FSM is defined by a list of its states, and the triggering condition for each transition.
在非函数式语言里,一个结构体+一组API,或者一个类的接口,都可以看作是一组操作一个状态机的函数。
在网络编程中,一个发包和一个回包,是最基本的收发操作(Atom Operation)。当我们引入状态机,将连续的收、发包操作组装起来,就构成了某个网络协议,一组有效的收发包要么让这个状态机保持某种状态,要么切换到另一个状态。
最早接触状态机是在写OpenGL的时候,知道了OpenGL是一个全局状态机:
OpenGL是一个状态机
OpenGL是一个状态机,它维持自己的状态,并根据用户调用的函数来改变自己的状态。根据状态的不同,调用同样的函数也可能产生不同的效果。
为什么OpenGL的状态机是全局的呢?
Why does OpenGL be designed as a state machine originally?
The reason for this kind of "state machine" is simple: that's how the hardware generally works.
Because originally, back in OpenGL 1.0, there were no objects (besides display lists). Yes, not even texture objects. When they decided that this was stupid and that they needed objects, they decided to implement them in a backwards compatible way. Everyone was already using functions that operated on global state. So they just said that by binding an object, you override the global state. Those functions that used to change global state now change the object's state.
OpenGL已经很久没用来写代码干点什么。但是OpenGL的几个模式却常见。
if( glIsEnabled(GL_BLEND) ) {
// 当前开启了混合功能
} else {
// 当前没有开启混合功能
}
glPushMatrix();
// 里面干什么都不会影响到外面的矩阵世界
glPopMatrix();
写代码生成器,嵌套的block栈让事情变的简单:
.Line("int test()")
.PushLine("{")
.Code()
.PopLine("}");
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (GLfloat) w/(GLfloat) h, 1.0, 20.0);
glMatrixMode(GL_MODELVIEW);
我记得小时候,去别人家看录像机,大人们操作几次之后,一帮小孩把大人们按哪个键开关、播放、切换什么的都会记得清晰。后面好几年,接触电子设备少,偶尔来一个相机都不知道那么多按钮是干嘛用的,为什么一会儿这个按钮是干这个用的,一会儿又是那个效果。绕了一圈,后面学了VIM,VIM的每个快捷键的是什么效果会取决于编辑器当前处于什么模式,例如编辑模式、命令模式、可视模式等等。后面我再次用相机的时候,一下就想明白了,电子设备的按钮不可能无限多个,所以引入“模式”即可解决。某个按钮用来控制当前的模式,切换到不同模式后,同一个按钮的作用可以不同。OpenGL的glMatrixMode这个套路在很多场景下都一再的被使用。
写一个代码生成器,先给一组最简单的渲染操作:
private static int indent=0;
public static StringBuilder Push(this StringBuilder sb) {
indent++;
return sb;
}
public static StringBuilder Pop(this StringBuilder sb) {
indent--;
return sb;
}
public static StringBuilder Line(this StringBuilder sb, string text, bool ignoreIndent=false) {
if (ignoreIndent) {
sb.AppendLine(text);
} else {
var space=new string('\t', indent);
sb.AppendFormat("{0}{1}\n", space, text);
}
return sb;
}
public static StringBuilder Line(this StringBuilder sb) {
sb.AppendLine();
return sb;
}
public static StringBuilder FormatLine(this StringBuilder sb, string format, params object[] args) {
var space=new string('\t', indent);
var text=string.Format(format, args);
sb.AppendFormat("{0}{1}\n", space, text);
return sb;
}
public static StringBuilder PushLine(this StringBuilder sb, string text) {
return sb.Line(text).Push();
}
public static StringBuilder PopLine(this StringBuilder sb, string text) {
return sb.Pop().Line(text);
}
public static StringBuilder PopLine(this StringBuilder sb) {
return sb.Pop().Line();
}
其中Push和Pop正是借用Stack的概念,来简化block块代码的生成。我们可以如下使用:
var sb = new StringBuilder();
sb.Line("#include \"./PackageUtil.h\"")
.Line("PACKAGE_HANDLE PackageUtil::CreateEmptyPackage( PackageHeader& header )")
.PushLine("{")
.Line("int ret = RESULT_FAILED;")
.Line("PackageInitData initData = {PackageInitDataType_Header,&header};")
.Line("switch(header.PackageType)")
.PushLine("{");
foreach (var p in protocols) {
sb.FormatLine("case XXX_PACKAGE_{0}:",p.Name)
.Push()
.FormatLine("return CreatePackage(PackageTrait<XXX_PACKAGE_{0}>::GetClassName(),&initData,&ret);", p.Name.ToUpper())
.Pop();
}
sb.Line("default:")
.Push().Line("return NULL;").Pop()
.PopLine("}")
.PopLine("}");
然而,这里的foreach打断了代码的链式形式,实际上这样也就够用了,但我们可以再折腾一下,做点无聊的事情:
public class EachBuilder<T> {
public IEnumerable<T> List;
public StringBuilder Builder;
public List<Action<T>> Actions;
}
public static EachBuilder<T> BeginEach<T>(this StringBuilder sb, IEnumerable<T> list) {
return new EachBuilder<T>(){
List = list,
Builder = sb
};
}
public static EachBuilder<T> Push<T>(this EachBuilder<T> sbe) {
var action=new Action<T>(t => sbe.Builder.Push());
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> Line<T>(this EachBuilder<T> sbe, string text, bool ignoreIndent=false) {
var action=new Action<T>(t => sbe.Builder.Line(text, ignoreIndent));
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> Line<T>(this EachBuilder<T> sbe) {
var action=new Action<T>(t => sbe.Builder.Line());
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> PushLine<T>(this EachBuilder<T> sbe, string text){
var action=new Action<T>(t => sbe.Builder.PushLine(text));
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> PopLine<T>(this EachBuilder<T> sbe, string text) {
var action=new Action<T>(t => sbe.Builder.PopLine(text));
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> PopLine<T>(this EachBuilder<T> sbe) {
var action=new Action<T>(t => sbe.Builder.PopLine());
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> Pop<T>(this EachBuilder<T> sbe) {
var action=new Action<T>(t => sbe.Builder.Pop());
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> FormatLine<T>(this EachBuilder<T> sbe, string format, Func<T,string> args){
var action=new Action<T>(t => sbe.Builder.FormatLine(format, args(t)));
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> FormatLine<T>(this EachBuilder<T> sbe, string format, Func<T, string> args1, Func<T, string> args2) {
var action=new Action<T>(t => sbe.Builder.FormatLine(format, args1(t),args2(t)));
sbe.Actions.Add(action);
return sbe;
}
public static EachBuilder<T> FormatLine<T>(this EachBuilder<T> sbe, string format, Func<T, string> args1, Func<T, string> args2, Func<T, string> args3) {
var action=new Action<T>(t => sbe.Builder.FormatLine(format, args1(t), args2(t), args3(t)));
sbe.Actions.Add(action);
return sbe;
}
public static StringBuilder EndEach<T>(this EachBuilder<T> sbe){
foreach (var item in sbe.List){
foreach (var action in sbe.Actions){
action(item);
}
}
return sbe.Builder;
}
我们把StringBuilder通过通过BeginEach绑定一个IEnumearble变成一个EachBuilder,然后为EachBuilder提供一组同之前同语义的操作,这些操作内部只是把要做的事情通过Action的形式收集起来,保持顺序。在EndEach的时候,再一次性对之前绑定的List做操作。这里我希望使用者感觉这些API和之前的版本没有太大差别。当EndEach完成的时候,EachBuilder就再次转回到StringBuilder。从而,我们可以把代码写成:
sb.Line("#include \"./PackageUtil.h\"")
.Line("PACKAGE_HANDLE PackageUtil::CreateEmptyPackage( PackageHeader& header )")
.PushLine("{")
.Line("int ret = RESULT_FAILED;")
.Line("PackageInitData initData = {PackageInitDataType_Header,&header};")
.Line("switch(header.PackageType)")
.PushLine("{")
.BeginEach(protocols)
.FormatLine("case XXX_PACKAGE_{0}:", p => p.Name)
.Push()
.FormatLine("return CreatePackage(PackageTrait<XXX_PACKAGE_{0}>::GetClassName(),&initData,&ret);",
p => p.Name.ToUpper())
.Pop()
.EndEach()
.Line("default:")
.Push().Line("return NULL;").Pop()
.PopLine("}")
.PopLine("}");
这样,通过切换模式的上下文,就达到了对API语义的切换,使之适用于批处理。可以看到上面的代码的结构和目标生成代码之间具有结构的一致性。如果我们再无聊点,折腾一些模板语言来“声明性”表达这个过程,再写一个parser把它转换为上述代码的逻辑,我们就可以定义一个DSL,这是后话。通过同样的方式,我们可以为树形结构设计BeginTree、EndTree。为图结构设计BeginGraphics、EndGraphics,and so on。
从上面的过程,我们可以说代码生成器和渲染器(至少OpenGl)有诸多共同的地方,我们可以说代码生成就是在做文本渲染。开头说过网络协议本身是一个状态机,传统上,认为写网络层代码是一件“难”的事情,然而,网络协议状态机是一种适合严格的形式化方法的场景。协议包可以被生成、协议的状态机可以被生成、这样我们可以把精力放在如何设计协议、如何设计好的拥塞控制算法等等真正需要数学和脑力的地方。