1. 程式人生 > >【轉】對 Parser 的誤解

【轉】對 Parser 的誤解

思想 建議 遞歸函數 python IT 好處 不能 留下 未定義

一直很了解人們對於parser的誤解,可是一直都提不起興趣來闡述對它的觀點。然而我覺得是有必要解釋一下這個問題的時候了。我感覺得到大部分人對於parser的誤解之深,再不澄清一下,恐怕這些謬誤就要寫進歪曲的歷史教科書,到時候就沒有人知道真相了。

什麽是 Parser

首先來科普一下。所謂parser,一般是指把某種格式的文本(字符串)轉換成某種數據結構的過程。最常見的parser,是把程序文本轉換成編譯器內部的一種叫做“抽象語法樹”(AST)的數據結構。也有簡單一些的parser,用於處理CSV,JSON,XML之類的格式。

舉個例子,一個處理算數表達式的parser,可以把“1+2”這樣的,含有1

+2三個字符的字符串,轉換成一個對象(object)。這個對象就像new BinaryExpression(ADD, new Number(1), new Number(2))這樣的Java構造函數調用生成出來的那樣。

之所以需要做這種從字符串到數據結構的轉換,是因為編譯器是無法直接操作“1+2”這樣的字符串的。實際上,代碼的本質根本就不是字符串,它本來就是一個具有復雜拓撲的數據結構,就像電路一樣。“1+2”這個字符串只是對這種數據結構的一種“編碼”,就像ZIP或者JPEG只是對它們壓縮的數據的編碼一樣。

這種編碼可以方便你把代碼存到磁盤上,方便你用文本編輯器來修改它們,然而你必須知道,文本並不是代碼本身。所以從磁盤讀取了文本之後,你必須先“解碼”,才能方便地操作代碼的數據結構。比如,如果上面的Java代碼生成的AST節點叫node

,你就可以用node.operator來訪問ADD,用node.left來訪問1node.right來訪問2。這是很方便的。

對於程序語言,這種解碼的動作就叫做parsing,用於解碼的那段代碼就叫做parser。

Parser在編譯器中的地位

那麽貌似這樣說來,parser是編譯器裏面很關鍵的一個部分了?顯然,parser是必不可少的,然而它並不像很多人想象的那麽重要。Parser的重要性和技術難度,被很多人嚴重的誇大了。一些人提到“編譯器”,就跟你提LEX,YACC,ANTLR等用於構造parser的工具,仿佛編譯器跟parser是等價的似的。還有些人,只要聽說別人寫了個parser,就覺得這人編程水平很高,開始膜拜了。這些其實都顯示出人的膚淺。

我喜歡把parser稱為“萬裏長征的第0步”,因為等你parse完畢得到了AST,真正精華的編譯技術才算開始。一個先進的編譯器包含許多的步驟:語義分析,類型檢查/推導,代碼優化,機器代碼生成,…… 這每個步驟都是在對某種中間數據結構(比如AST)進行分析或者轉化,它們完全不需要知道代碼的字符串形式。也就是說,一旦代碼通過了parser,在後面的編譯過程裏,你就可以完全忘記parser的存在。所以parser對於編譯器的地位,其實就像ZIP之於JVM,就像JPEG之於PhotoShop。Parser雖然必不可少,然而它比起編譯器裏面最重要的過程,是處於一種輔助性,工具性,次要的地位。

鑒於這個原因,好一點的大學裏的程序語言(PL)課程,都完全沒有關於parser的內容。學生們往往直接用Scheme這樣代碼數據同形的語言,或者直接使用AST數據結構來構造程序。在Kent Dybvig這樣編譯器大師的課程上,學生直接跳過parser的構造,開始學習最精華的語義轉換和優化技術。實際上,Kent Dybvig根本不認為parser算是編譯器的一部分。因為AST數據結構其實才是程序本身,而程序的文本只是這種數據結構的一種編碼形式。

Parser技術發展的誤區

既然parser在編譯器中處於次要的地位,可是為什麽還有人花那麽大功夫研究各種炫酷的parser技術呢。LL,LR,GLR,LEX, YACC,Bison,parser combinator,ANTLR,PEG,…… 制造parser的工具似乎層出不窮,每出現一個新的工具都號稱可以處理更加復雜的語法。

很多人盲目地設計復雜的語法,然後用越來越復雜的parser技術去parse它們,這就是parser技術仍然在發展的原因。其實,向往復雜的語法,是程序語言領域流傳非常廣,危害非常大的錯誤傾向。在人類歷史的長河中,留下了許多難以磨滅的歷史性糟粕,它們固化了人類對於語言設計的理念。很多人設計語言似乎不是為了拿來好用的,而是為了讓用它的人迷惑或者害怕。

有些人假定了數學是美好的語言,所以他們盲目的希望程序語言看起來更加像數學。於是他們模仿數學,制造了各種奇怪的操作符,制定它們的優先級,這樣你就可以寫出2 << 7 - 2 * 3這樣的代碼,而不需要給子表達式加上括號。還有很多人喜歡讓語法變得“簡練”,就為了少打幾個括號,分號,花括號,…… 可是由此帶來的結果是復雜,不一致,有多義性,難擴展的語法,以及障眼難讀,模棱兩可的代碼。

更有甚者,對數學的愚蠢做法執迷不悟的人,設計了像Haskell和Coq那樣的語言。在Haskell裏面,你可以在代碼裏定義新的操作符,指定它的“結合律”(associativity)和“優先級”(precedence)。這樣的語法設計,要求parser必須能夠在parse過程中途讀入並且加入新的parse規則。Coq試圖更加“強大”一些,它讓你可以定義“mixfix操作符”,也就是說你的操作符可以連接超過兩個表達式。這樣你就可以定義像if...then...else...這樣的“操作符”。

制造這樣復雜難懂的語法,其實沒有什麽真正的好處。不但給程序員的學習造成了不必要的困難,讓代碼難以理解,而且也給parser的作者帶來了嚴重的挑戰。可是有些人就是喜歡制造問題,就像一句玩笑話說的:有困難要上,沒有困難,制造困難也要上!

如果你的語言語法很簡單(像Scheme那樣),你是不需要任何高深的parser理論的。說白了,你只需要知道如何parse匹配的括號。最多一個小時,幾百行Java代碼,我就能寫出一個Scheme的parser。

可是很多人總是嫌問題不夠有難度,於是他們不停地制造更加復雜的語法,甚至會故意讓自己的語言看起來跟其它的不一樣,以示“創新”。當然了,這樣的語言就得用更加復雜的parser技術,這正好讓那些喜歡折騰復雜parser技術的人洋洋得意。

編譯原理課程的誤導

程序員們對於parser的誤解,很大程度上來自於大學編譯原理課程照本宣科的教育。很多老師自己都不理解編譯器的精髓,所以就只有按部就班的講一些“死知識”,灌輸“業界做法”。一般大學裏上編譯原理課,都是捧著一本大部頭的“龍書”或者“虎書”,花掉一個學期1/3甚至2/3的時間來學寫parser。由於parser占據了大量時間,以至於很多真正精華的內容都被一筆帶過:語義分析,代碼優化,類型推導,靜態檢查,機器代碼生成,…… 以至於很多人上完了編譯原理課程,記憶中只留下寫parser的痛苦回憶。

“龍書”之類的教材在很多人心目中地位是如此之高,被譽為“經典”,然而其實除了開頭很大篇幅來講 parser 理論,這本書其它部分的水準其實一般般。大部分學生的反應其實是“看不懂”,然而由於一直以來沒有更好的選擇,它經典的地位真是難以動搖。“龍書”後來的新版我瀏覽過一下,新加入了類型檢查/推導的部分,可是我看得出來,其實作者們自己對於類型理論都是一知半解,所以也就沒法寫清楚。

如果你想真的深入理解編譯理論,最好是從PL課程的讀物,比如 EOPL 開始。我可以說 PL 這個領域,真的和編譯器的領域很不一樣。請不要指望編譯器的作者能夠輕易設計出好的語言,因為他們可能根本不理解很多語言設計的東西,他們只是會實現某些別人設計的語言。可是反過來,理解了 PL 的理論,編譯器的東西只不過是把一種語言轉換成另外一種語言(機器語言)而已。工程的細枝末節很麻煩,可是當你掌握了精髓的原理,那些都容易摸索出來。

我寫parser的心得和秘訣

雖然我已經告訴你,給過度復雜的語言寫parser其實是很苦逼,沒有意思的工作,然而有些歷史性的錯誤已經造成了深遠的影響,所以很多時候雖然心知肚明,你也不得不妥協一下。由於像C++,Java,JavaScript,Python之類語言的流行,有時候你是被迫要給它們寫parser。在這一節,我告訴你一些秘訣,也許可以幫助你更加容易的寫出這些語言的parser。

很多人都覺得寫parser很難,一方面是由於語言設計的錯誤思想導致了復雜的語法,另外一方面是由於人們對於parser構造過程的思維誤區。很多人不理解parser的本質和真正的用途,所以他們總是試圖讓parser幹一些它們本來不應該幹的事情,或者對parser有一些不切實際的標準。當然,他們就會覺得parser非常難寫,非常容易出錯。

  1. 盡量拿別人寫的parser來用。維護一個parser是相當繁瑣耗時,回報很低的事情。一旦語言有所改動,你的parser就得跟著改。所以如果你能找到免費的parser,那就最好不要自己寫。現在的趨勢是越來越多的語言在標準庫裏提供可以parse它自己的parser,比如Python和Ruby。這樣你就可以用那語言寫一小段代碼調用標準的parser,然後把它轉換成一種常用的數據交換格式,比如JSON。然後你就可以用通用的JSON parser解析出你想要的數據結構了。

    如果你直接使用別人的parser,最好不要使用它原來的數據結構。因為一旦parser的作者在新版本改變了他的數據結構,你所有的代碼都會需要修改。我的秘訣是做一個“AST轉換器”,先把別人的AST結構轉換成自己的AST結構,然後在自己的AST結構之上寫其它的代碼,這樣如果別人的parser修改了,你可以只改動AST轉換器,其它的代碼基本不需要修改。

    用別人的parser也會有一些小麻煩。比如Python之類語言自帶的parser,丟掉了很多我需要的信息,比如函數名的位置,等等。我需要進行一些hack,找回我需要的數據。相對來說,這樣小的修補還是比從頭寫一個parser要劃得來。但是如果你實在找不到一個好的parser,那就只好自己寫一個。

  2. 很多人寫parser,很在乎所謂的“one-pass parser”。他們試圖掃描一遍代碼文本就構造出最終的AST結構。可是其實如果你放松這個條件,允許用多pass的parser,就會容易很多。你可以在第一遍用很容易的辦法構造一個粗略的樹結構,然後再寫一個遞歸樹遍歷過程,把某些在第一遍的時候沒法確定的結構進行小規模的轉換,最後得到正確的AST。

    想要一遍就parse出最終的AST,可以說是一種過早優化(premature optimization)。有些人盲目地認為只掃描一遍代碼,會比掃描兩遍要快一些。然而由於你必須在這一遍掃描裏進行多度復雜的操作,最終的性能也許還不如很快的掃完第一遍,然後再很快的遍歷轉換由此生成的樹結構。

  3. 另外一些人試圖在parse的過程中做一些本來不屬於它做的事情,比如進行一些基本的語義檢查。有些人會讓parser檢查“使用未定義的變量”等語義錯誤,一旦發現就在當時報錯,終止。這種做法其實混淆了parser的作用,造成了不必要的復雜性。

    就像我說的,parser其實只是一個解碼器。parser要做的事情,應該是從無結構的字符串裏面,解碼產生有結構的數據結構。而像“使用未定義的變量”這樣的語義檢查,應該是在生成了AST之後,使用單獨的樹遍歷來進行的。人們常常混淆“解碼”,“語法”和“語義”三者的不同,導致他們寫出過度復雜,效率低下,難以維護的parser。

  4. 另一種常見的誤區是盲目的相信YACC,ANTLR之類所謂“parser generator”。實際上parser generator的概念看起來雖然美好,可是實際用起來幾乎全都是噩夢。事實上最好的parser,比如EDG C++ parser,幾乎全都是直接用普通的程序語言手寫而成的,而不是自動生成的。

    這是因為parser generator都要求你使用某種特殊的描述語言來表示出語法,然後自動把它們轉換成parser的程序代碼。在這個轉換過程中,這種特殊的描述語言和生成的parser代碼之間,並沒有很強的語義連接關系。如果生成的parser有bug,你很難從生成的parser代碼回溯到語法描述,找到錯誤的位置和原因。你沒法對語法描述進行debug,因為它只是一個文本文件,根本不能運行。

    所以如果你真的要寫parser,我建議你直接用某種程序語言手寫代碼,使用普通的遞歸下降(recursive descent)寫法,或者parser combinator的寫法。只有手寫的parser才可以方便的debug,而且可以輸出清晰,人類可理解的出錯信息。

  5. 有些人喜歡死扣BNF範式,盲目的相信“LL”,“LR”等語法的區別,所以他們經常落入誤區,說“哎呀,這個語法不是LL的”,於是采用一些像YACC那樣的LR parser generator,結果落入非常大的麻煩。其實,雖然有些語法看起來不是LL的,它們的parser卻仍然可以用普通的recursive descent的方式來寫。

    這裏的秘訣在於,語言規範裏給出的BNF範式,其實並不是唯一的可以寫出parser的做法。BNF只是一個基本的參照物,它讓你可以對語法有個清晰的概念,可是實際的parser卻不一定非得按照BNF的格式來寫。有時候你可以把語法的格式稍微改一改,變通一下,卻照樣可以正確地parse原來的語言。其實由於很多語言的語法都類似於C,所以很多時候你寫parser只需要看一些樣例程序,然後根據自己的經驗來寫,而不需要依據BNF。

    Recursive descent和parser combinator寫出來的parser其實可以非常強大,甚至可以超越所謂“上下文無關文法”,因為在遞歸函數裏面你可以做幾乎任意的事情,所以你甚至可以把上下文傳遞到遞歸函數裏,然後根據上下文來決定對當前的節點做什麽事情。而且由於代碼可以得到很多的上下文信息,如果輸入的代碼有語法錯誤,你可以根據這些信息生成非常人性化的出錯信息。

總結

所以你看到了,parser並不是編譯器,它甚至不屬於編譯裏很重要的東西。程序語言和編譯器裏面有比parser重要很多,有趣很多的東西。Parser的研究,其實是在解決一些根本不存在,或者人為制造的問題。復雜的語法導致了復雜的parser技術,它們仍然在給計算機世界帶來不必要的困擾和麻煩。對parser寫法的很多誤解,過度工程和過早優化,造成了很多人錯誤的高估寫parser的難度。

能寫parser並不是什麽了不起的事情,其實它是非常苦逼,真正的程序語言和編譯器專家根本不屑於做的事情。所以如果你會寫parser,請不要以為是什麽了不起的事情,如果你看到有人寫了某種語言的parser,也不要表現出讓人哭笑不得的膜拜之情。

【轉】對 Parser 的誤解