前一阵子,收到烨兄的私聊,他忽然要解决这样一个任务:html
作以下格式的表达式转换:前端
Multi(a, Multi(b, c)) --> a * (b * c)
Divide(a, Sub(b, c)) --> a / (b - c)
支持的运算符有:git
Add
: +Sub
: -Multi
: *Divide
: /
并且好死不死的须要用他没怎么用过的 C++ 来写。我发现这是一个 parser 的问题,第一反应是推荐他用 flex/bison,但想到为了这么大点任务大费周章不太合适,又开始想手写这样一个表达式的 parser 难不难。最后得出的结论是,不难。程序员
了解编译原理的人都知道什么是 parser。Parser 中文名(语法)分析器,是每一个编译器的前端都会有的一个东西。不过,从编译原理的视角来看,“语言”的范畴要比咱们理解的编程语言要广义得多,任何有必定规则的字符串构成方式,均可以当作是语言,例如上面的那个任务里用 Add
、Sub
这样的函数描述的表达式。github
那么,要解决上面这个任务,只须要对表达式的字符串进行语法分析,获得一个中间表示(通常是分析树或抽象语法树),再将中间表示输出为所需的格式便可。也就是说咱们须要为表达式提供一个 parser,这个任务的任何解决方式,本质上均可以当作是写了一个 parser。编程
在平时,咱们彻底没有任何须要去手写一个 parser,由于这东西已经有工具能够为咱们生成。感谢几十年前伟大的程序员就已经发明了这样的工具。我用过的有 C/C++ 的 flex/bison,以及 Java 的 ANTLR。你只须要提供一个文法描述,这些工具就能够为你自动生成对应的语法分析器。若是要手写分析器,会很复杂,也很容易出错,不是一个明智的选择。数组
不过,面对上面举例的这种小任务,使用自动生成 parser 的工具备时候显得过重了,这时候也许手写一个 parser 是更好的选择。并且在这样的任务场景下,咱们的 parser 有两个地方起码是能够获得大大简化的:app
第一,咱们要处理的语言应该不会像通用编程语言那样,有很复杂的状态转移。一般状况下,应该能看到当前的字符串就知道下面要分析什么类型的内容。通常标记语言都会是这种风格的,好比:编程语言
<tag>
就知道是一个标签的开始,直到 </tag>
为止;
分隔#
开头就是标题,以 1.
开头就是有序列表项第二,咱们不须要进行复杂的语法错误处理,只须要报“语法错误”就行了,而不须要费力说明到底发生了什么错误。ide
有了这两个前提,咱们开始思考如何手写一个语法分析器。固然,我已经思考好了,下面是我给出的一个简单的分析器的实现。我是用 Java 实现的,用到了一点 lambda 表达式的语法,不过不难理解。由于 parser 的主要工做是作字符串比较,因此用任何语言都差很少。后面我会考虑再用其余语言实现。
在实现上咱们再作一点简化:咱们把要分析的字符串做为字符数组保存下来,而不是从所谓“字符流”中读入。这样咱们没必要考虑读 (get) 了字符却不用掉 (consume) 的状况下,这些是输入模块要考虑的部分,咱们专一于 parser 自己。
首先,咱们的 SimpleParser
是这样定义的:
public class SimpleParser {
private char[] input;
private int pos;
public SimpleParser(String source) {
this.input = source.toCharArray();
this.pos = 0;
}
}
复制代码
咱们将输入保存为字符数组,pos
是一个指向待读取的下一个字符的指针。将 pos
加一,就至关于从读入了一个字符。
下面,咱们添加一些脚手架函数:
private void consumeWhitespace() {
consumeWhile(Character::isWhitespace);
}
private String consumeWhile(Predicate<Character> test) {
StringBuilder sb = new StringBuilder();
while (!eof() && test.test(nextChar())) {
sb.append(consumeChar());
}
return sb.toString();
}
private char consumeChar() {
return input[pos++];
}
private boolean startsWith(String s) {
return new String(input, pos, input.length - pos).startsWith(s);
}
private char nextChar() {
return input[pos];
}
private boolean eof() {
return pos >= input.length;
}
复制代码
这些函数的来源于我以前看过的一个系列文章:Let's build a browser engine!(原文是用 Rust 语言的)。咱们来看一下这几个函数:
其中,nextChar
, startsWith
这两个函数是用来“向后看”,判断后面输入的状态。这实际上已经和编译原理中说的语法分析不太同样了(回忆一下,编译原理中说的语法分析方法只会向后看一个字符),可是由于咱们只是判断是否是等于一个固定的字符串,因此也不是太大的问题。
以 consume...
开头的几个函数就是真正的读取输入的函数了。其中,consumeWhile
是一个通用的函数,consumeWhitespace
也是基于其实现的。相似地,咱们还能够基于其实现解析变量名的函数:
private String parseVariableName() {
return consumeWhile(Character::isAlphabetic);
}
复制代码
注意到这实际上就是在解析咱们任务中的变量名了,以此为思路,后面的实现其实很简单。咱们一上来会以为手写 parser 会很复杂,其实是由于没找到入手点。因此这几个脚手架函数特别重要,先有了他们,后面就能够一步一步写出整个 parser 的功能了。
那么咱们接下来能够这么写:
// 解析由单个变量组成的表达式
private VariableExpression parseVariableExpression() {
String name = parseVariableName();
// VariableExpression 的定义略
return new VariableExpression(name);
}
复制代码
// 解析加减乘除表达式
private CompoundExpression parseCompoundExpression(String name) {
for (char c : name.toCharArray()) {
checkState(c == consumeChar());
}
checkState('(' == consumeChar());
// 递归解析
Expression left = parseExpression();
checkState(',' == consumeChar());
consumeWhitespace();
Expression right = parseExpression();
checkState(')' == consumeChar());
// CompoundExpression 的定义略
return new CompoundExpression(name, left, right);
}
// VariableExpression 和 CompoundExpression 都是 Expression
private Expression parseExpression() {
if (startsWith("Add")) {
return parseCompoundExpression("Add");
} else if (startsWith("Sub")) {
return parseCompoundExpression("Sub");
} else if (startsWith("Multi")) {
return parseCompoundExpression("Multi");
} else if (startsWith("Divide")) {
return parseCompoundExpression("Divide");
} else {
return parseVariableExpression();
}
}
复制代码
写到这里,咱们 parser 的主要工做已经作完了,接下来的任务就很是简单了。彷佛咱们的任务有点太简单了?在这种场景下,手写 parser 确实不难,接下来能够手写一个 Markdown 的 parser 练习一下了😜。
P.S. 烨兄后来并无作这个任务,我也是到如今才想起来把这个 parser 实现出来,只是我本身以为好玩想了这件事。
文章中的 parser 的完整代码,能够到个人 GitHub 上查看:simpleparser。