用 Swift 寫個`函數式`的解釋器(1)
開通專欄了,之後會主要寫一些 iOS開發和函數式編程的內容。尤其是探索在 Swift 上面應用純函數編程思想,比如現在這個系列的文章。
所謂函數式的解釋器,並不是函數式語言的解釋器,而是用函數式的方法來設計的解釋器(Parser Combinator)。
之前為了準備 SwiftGG 的分享會,嘗試用 Swift 以純函數的方式寫個一個簡單的解釋器。事實證明 Swift 提供了友好的函數式特性,用來實現 FP 世界裡一些簡單的模型完全沒有問題(比如State Monad)。只用了不到400行代碼,就完全了基本功能的實現,並且非常易於擴展。
當時的直播講得比較倉促,很多東西並沒有講清楚。乾脆開個系列文章慢慢說吧。需要讀者事先有一點 Swift 語言的基礎,不需要編譯原理的基礎。
完整的項目地址點這裡
關於 Parser Combinator 的理論,可以參考這裡,這篇論文里第一次提出了用「組合子」思想來設計解釋器,並且給出了 Haskell 的實現。
解釋器實現的三個步驟
- 定義AST(抽象語法數)
- 把程序源代碼解析成 AST
- 解釋執行 AST
今天先介紹如何定義 AST。
定義 AST
要定義 AST,首先我們需要確定語言支持哪些功能,這裡我們一切從簡,假設我們要設計的語言長這樣:
i = 0 print i sum = 0 while i<100 { sum=sum+i i=i+1 } print sum
沒錯……就是長得跟數據結構課本里的偽代碼似得。我們就叫他 PLang 語言吧!(Pseudo Language)(逃
從這段簡短的代碼里能發現程序其實是由兩種基本元素組成:表達式和命令。比如上面的i<100, sum+i,i+1 都屬於表達式。而像 i=0,print i,while ...則是一條一條需要執行的命令。一個簡單的區別是,表達式都是有一個結果值的,而命令一般沒有。
表達式
現在我們就可以來設計 AST 了。鑒於 Swift 的 enum 的種種牛逼特性,自然非常適合用來建模我們的 AST。我們用 Exp 來代表表達式:
indirect enum Exp{ case Constant (Int) case Var (String) case Add (Exp,Exp) case Times (Exp,Exp) case Equal (Exp,Exp) case Greater (Exp,Exp) case Less (Exp,Exp)}
- indirect代表這個結構是遞歸的,什麼意思呢?比如我們定義的 Add (加法)類型,他接受兩個參數,參數的類型也是 Exp,代表加法不僅僅是兩個數字的相加,而是兩個表達式的相加。
- Constant 代表常量,比如3 也算一個合法的表達式,用 AST 表示就是 Constant(3)。
- Var 代表變數,接受的 String 類型的參數就代表變數的名字,比如上面例子的i+1,用我們的 AST 表示為 Add(Var(i), Constant(1)),注意這裡的 Add()並不是函數調用,只是構造了一個 Exp.Add 的實例。
- Times 表示乘法,Equal 代表判等(對應 C 語言中的==),Greater對應>,Less 對應<。
為了簡化設計過程,本語言僅支持 Int 類型
命令
接下來我們來設計命令對應的 AST,和表達式一樣,命令也是遞歸的結構。比如while ... {...} 是一個命令,但其內部會包含其他命令(循環體)。
indirect enum Com{ case Assign (String,Exp) case Seq (Com,Com) case Cond (Exp,Com,Com) case While (Exp,Com) case Print (Exp)}
- Assign,代表賦值命令。參數1是 String 類型,代表要賦值到變數的名字,參數2是 Exp 類型,代表變數的值,為什麼用 Exp 呢? 因為賦值可能並不一定是簡單的i=0,而是有可能是i= 3 * (33+sum)這樣複雜的表示。i=0表示為 AST 就是 Assign("i", Constant(0))。
- Seq, 順序執行的命令,通過Seq 來把我們整個程序串起來。比如 i=0 s=1可以用 seq 表示為Seq(Assign(..) , Assign(..)),那如果是像i=0 s=1 b=2這樣三個命令該怎麼連呢?只需要嵌套使用 seq 就可以: Seq(Assign(..) , Seq(Assign(..) , Assign(...)))
- Cond 也就是條件判斷,常見的形式就是 if s then a else b, s a b 分別對應 Cond 的三個參數,不難明白為什麼 s 是 Exp 類型,而 a b 是 Com 類型
- While 表示了形式:while s { a } , 代表循環的結構,在 s 的值為真的前提下循環執行 a 對應的命令。
- Print 就是把一個表達式的值列印出來啦。方便我們測試程序
應用 AST
把上面我們 PLang 語言的片段用 AST 表示的話,就是像這樣:
Seq( Assign("i", Constant(0) ) , Seq( Print(i), Seq(Assign("sum",Constant(0) ), Seq(While( Less("i",Constant(100)) , Seq(Assign("sum", Add("sum" + "i"), Assign("i", Add("i", Constant(1)))), Print(i)) ) ) )
看上去比較蛋疼(手寫起來更蛋疼),不過其實還是很易懂的,之後我們要做的就是通過程序來自動生成這樣的 AST 的結構。
在下一篇文章中,會開始介紹如何從零開始構建一個優雅的解析器(Parser)來幫助我們完成這個過程。
推薦閱讀: