本文是本人撰写的编译原理讲义。
本系列讲义适用于:被强迫学习编译原理前端,或者希望弄明白如何做科研的人
上期讲到,我们通过抽象,告别了“屎山代码”,亲手打造出了一台由“状态转移图”驱动的词法分析器。这台机器它能跑,而且跑得很好。
其中,由于状态转移图不能被机器理解,我们把它转变成状态转移表了。
现在,一个远方的同事需要跟你交流这个词法分析器的实现。由于我们正处于70年代(编者注:忘记了我们设定的朋友请回看绪论部分),能传输的东西有限。这时,一连串问题出现了:
-
沟通的成本:我们如何把这个词法分析器传达给同事?是把整个词法分析器的代码连带状态转移表的数据发过去吗?还是只发送状态转移表?他知道怎么用里面的数据吗?还是说我们把原始的有向图画出来寄过去?
-
验证的困难:如果朋友也设计了一个词法分析器,但经过交流发现他的状态转移表和我们的长得完全不一样,但我们都声称能识别同一种单词。要如何证明这两台机器在功能上等价?
正如上一章提到的,血肉苦弱,抽象飞升。这个状态转移表尽管已经是对单词组成规则乃至图的一种抽象,但是在交流、理论分析上依然存在问题。
它依然要依赖于表格等硬件的束缚,依然不够纯粹。
如果可以仅仅通过符号就可以完全表达它的意思,那么我们在对它进行理论推导的时候,就不用受其沉重的身躯(表格或者图形本身)所束缚,能更加自由地进行推导。
为了从一个简单例子看出来我们到底要做啥,我们暂时先把有向图啊状态转移表放到一边,先来玩一下乐高吧。
1.基于乐高的排练
你在网上看到了一个超帅的乐高死星模型,很喜欢。
作者os:自从为了写这篇文章在京东上搜了一下这个东西,现在各大平台天天给我推送这鬼玩意儿。
但买回来之后发现,它是以零件形式+图纸形式来交付的,为什么?
聪明的你一定能脱口而出,因为直接整个运输的话不但占位置,不稳定,甚至还剥夺了我的建造乐趣!
对的,所以乐高公司会把这个模型以一包包零件的模式,通过附上说明书的形式分发给消费者,再由消费者自行组装。
当然,玩家可能会由于看不懂图纸而组装错,但这一步起码是可逆的,而不是在生成一个完整死星模型之后在运输过程中被不可逆损坏。
同理,在分发我们的抽象模型时,我们面临着和乐高公司一样的问题。
为了方便描述、降低沟通成本这个核心原因,我们也选择将模型拆解成"零件"和"安装说明书"再分发出去。
当然,这样做也带来了一些绝佳的“额外好处”:它能让读者在亲手“建造”的过程中,真正理解模型的内部结构;同时也让一部分乐于探索的用户,体验到独立思考的乐趣。(至于作者能不能顺便折磨一下读者,那就是另一个故事了(望月新一教授直呼内行))
现在,想必你已经能大致理解为什么我们要弄这么一套复杂的拆解再组装流程。
作为生产商的我们,零件好拆,说明书难写。下面我们用里面的一个小部分来见微知著。
我们从包装中拿出了人物零件包,然后进行了组装,得到了五个乐高人偶。
gemini组装的
Emm,中间的那两个乐高小人的上下半身组件好像组装错了。这生成了一个问题:这些乐高小人的零件如果可以随便组合,(你:这不简单,5的4....)请你描述一下包含所有合法组装的乐高小人的集合。
是的,排列组合是可以快速帮你算出来有 个可能性,但是真要想描述这个集合,排列组合帮不上多大忙,只能在你穷举的时候不让你混乱罢了。
这时候你想起来,高中第一堂数学课在教集合的时候,我们学过在表示一个集合的时候,除了一个一个元素穷举,还可以把这个集合中符合条件的元素对应的条件列出来,如: 。
可以看出,只要有了右边这坨在总体上描述的 要符合的规则,穷举的任务就从我们手上甩出去转到了读者手里了。
如果某人看不懂 是什么,那我们可以另外再列式解释 。
对于乐高人偶,我们从总体上可以明显看出来他的结构非常简单,头顶物 连接 头颅 连接 上半身 连接 下半身。这些头顶物,头颅,上半身,下半身等,在取用的时候不是随心所欲的,而是被限制在这套乐高所提供的头顶物集合,头颅集合,上半身集合,下半身集合中。
所以我们可以这样写:
但等一下,好像这堆规则只是限制了每次只从这些集合里面取出一个零件,但怎么连接还是没有限制,一不小心可能就会出现下面这样的科学怪人:
gemini生成的图,忽略颜色差异问题
为此我们需要多加一条限制规则,要求这四个东西必须按顺序连接。由于头顶物和头颅、头颅和上半身、上半身和下半身这三对零件类之间有且仅有一种连接方式,我们可以把连接符号简记为“ · ”,从而得到这么一个式子:
乐高人偶->头顶物 · 头颅 · 上半身 · 下半身
最后得到一个完整的集合:
最后再分别列出四个部件集合,就可以无偏倚地向别人完整地描述我们特定的乐高人偶集合啦。
(当然,不论是头顶物或者下半身,依然存在安装角度/方向不同的问题导致非法组装。但此处就不作讨论了,有兴趣的读者可以进一步优化描述规则。)
下面我们正式把这一套方法论套用在有向图/状态转移表中。
2.正式表演
我们重新对上一节那个有向图进行扒皮拆骨式的“人凝”。这个图里面,有什么可以像乐高人偶一样可以拆分出来的组件吗?
-
各个单圈,称为状态。其中圈不重要,圈里面的字母重要,称为状态的名称。
-
各个单箭头,表示的是从一个状态到另一个状态需要经过什么条件的触发,我们称为激励。同理,这些箭头本身不重要,它们连接了哪些状态,本身上面的符号这两点重要
-
那个被双箭头指着的圈圈,作为起点本身需要特定标注
-
双圈,作为成功解析单词的状态,也需要特别标注。
有了这些零件集合,我们就已经可以完整描述出这个有向图了。
但为了更加精准,我们把里面反复用到的共性元素抽取出来单独介绍一遍。比如圈圈,激励的条件(或者称为符号),他们全都是共性的。由此我们一共可以提取出以下这五个部分:
- Q (一个元素数量有限的状态集合)
有向图上的“状态清单”:明确写出我们的机器总共有哪些状态。例如:{S, A, C, E, Error}。
2. Σ (一个有限的输入字母表)
有向图上的“可处理激励符号集合”(简称符号集合):明确这台机器能处理哪些种类的字符。例如: Σ={a, b, c}。
3. F (一个接收状态的集合)
有向图上的“终局集合”:明确指定哪些状态是合法的“终点站”。只要最终停在这些状态里,就代表识别成功。例如:F = {E}。
4. q₀ (一个起始状态)
有向图上的“第一步:从这里开始”:明确指定哪一个状态是起始状态。例如:在这里q₀=S。
5. δ (一个转移函数)
有向图上的“箭头们”:用函数的形式,精确描述“在某个状态,遇到某个字符,应该转移到哪个状态”,如:δ(S, a) = A就描述了在状态S,遇到字符a,应该转移到状态A。这本质上就是“状态转移表”的数学表达。对于没有定义的情况,我们可以直接把结果映射到Error状态即可。
既然是函数,那么这个函数的定义域和值域分别是什么呢?
毫无疑问,第一个参数就是对应于Q,第二个参数对应的是Σ。而值域依然是Q。
在学习数据库的时候,我们学到一个东西叫做笛卡尔积,也就是两个集合的叉积,可以映射到别的集合上。我们可以按照类似的表示方法,从而形式化描述这个函数:δ:Q×Σ→Q。这样,即使别人定义了一个不同名称的函数,我们只要对一下定义域和值域,就可以确定实际内容是否一致了。
看,这五个部分合在一起 {Q, Σ, F, q₀, δ},就可以完整且无歧义地描述一个有向图。它用一种极其凝练的方式,封装了整个词法分析器的整个“灵魂”。
注意到,这个复杂的模型满足了两个关键的性质:
1.状态的数量有限
2. 转移函数每次生成的目标状态只有一个。也就说,只要当前状态和待输入符号确定了,那么下一个状态将会被唯一确定。
由这两个特点出发,我们可以重新给这个特化的有向图重新起一个名字:
确定的有限状态机(英语:deterministic finite automaton, DFA)
最后展示一下上面的有向图G对应的完整定义DFA:
其中:
Q={S, A, C, E},
Σ={a, b, c},
F = {E},
q₀=S,
δ = { (S, a) -> A, (A, c) -> C, (C, c) -> A, (A, b) -> E } //这种定义方法确实是不够优雅,但你就说这是不是函数吧。
你看,拆解之后,所谓“形式化定义”一点也不可怕嘛。
它只是在用一套世界通用的、无歧义的语言,来陈述我们这个抽象模型的方方面面而已。
3.回答初始提问
我要怎么和远方的同事交流各自的词法分析器?
1.沟通的成本问题
我们各自把自己的内部DFA给形式化描述一下,在变成一系列符号并附上它的定义之后,就可以单纯通过纸笔进行交流了,从而避免了把一些双方根本不关心的代码给抄上去,大大减少了我们沟通成本。
2. 如何确定两个不一样的DFA功能是否一致
简单一点当然可以先看他能处理的符号集合是否一样,如果不一样那功能上肯定不一样。
如果要更精确去判定识别的单词是不是完全一致,这个时候就需要先确保两个DFA都已经是不能更简单的形式,也就是所谓的最小化,之后尝试给两个不能更简单的DFA构造一个转换器。
只要两者可以一一对应上,那就可以宣布他们两完全同构了。
比如,如果对面发过来的DFA如下:
其中:
Q={1, 2, 3, 4}, Σ={a, b, c}, δ(1, a) = 2; δ(2, c) = 3; δ(3, c) = 2; δ(2, b) = 4; S=1, F = {4}
容易看出来,虽然定义上有一点点出入,但只要我们让1234分别对应SABE,那么这两个DFA的相似性就证明完毕了。
4.小结
我们为什么非要把一个正则文法转换成DFA呢?
一言以蔽之:正则文法虽然能用,但实现起来很容易变成屎山代码。为了解决这个问题,我们通过抽象,把它的核心逻辑转换成了DFA,之后只需要一个抽象出来的状态转移表外加一个几乎是写死的驱动程序,就可以实现单词识别这个核心功能。即使后面再需要修改识别的规则,只要修改这个状态转移表即可。
聪明的读者马上提问:如果没有任何改动,咱们是不是就没必要搞这么多,直接硬编码就可以了?
是的,所以现在咱们一切的努力都是为了更容易处理日后遇到的变化。
平常听到的所谓有限状态机(FA)与DFA的关系就类似于马与白马,大类与小类。
这些名次容易和限状态自动机混用。后者是数字逻辑这门课中讨论到的另一种特化后的有向图,它的特点在于每次状态变换后都会有输出,和我们编译原理要讨论的问题存在一定差异。
所以,尽管这两个词非常像,但后面我们更多只会用到有限状态机,有限自动机等表述。
至于为什么要强调“确定的”呢?难道还有不确定的自动机吗?不确定的东西我们要来干嘛呢?
另外,在第三节那里我们好像还没完全讲明白要怎么化简一个复杂的DFA。
如此种种,且听下回分解~