前一陣子,收到燁兄的私聊,他忽然要解決這樣一個任務: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。