翻開設計模式的書,總會在接近最後的地方看到直譯器模式。這是很容易因為「這到底什麼時候會用到」而隨便跳過的模式。
不過,在製作計算機 App 時,我遇到了必須直接計算字串算式的時刻。這時直譯器模式正好派上用場。
本文會用 Swift 親手打造一個非常小的語言直譯器,也就是迷你直譯器。目標是讀取像 1 + 2 * 3 這樣的算式,並輸出數字 7。
先說結論:
直譯器模式會把每條文法規則轉成類別(或列舉型別),組裝成樹狀結構,再以遞迴方式走訪並取得結果。
聽起來很難,但看過程式碼就會發現比想像中簡單。我們一起做做看。
什麼是直譯器模式?
直譯器模式是一種建立「自己的小型語言」,並用程式碼表達該語言解譯規則的方法。
這裡說的語言並不複雜。只要是具有固定文法的小型表示式,例如算式、搜尋篩選條件或遊戲規則腳本,都屬於這個範圍。
核心概念只有一個。
將文法的每個元素建立成物件,讓這些物件擁有解譯自身的 interpret() 方法。
以 1 + 2 這個算式為例。這裡包含數字 1、數字 2,以及加法運算 +。
數字會解譯為「直接回傳我的值」,加法則會解譯為「解譯左側與右側,再把兩者相加」。
這些小規則組成樹狀結構,只要從最上層呼叫一次解譯,就會一路往下完成計算。
使用 Swift 設計表示式樹
在 Swift 中,enum 非常適合這個模式。使用遞迴列舉型別,就能簡潔地表示樹狀結構。
我一開始用協定和類別實作,但改成 enum 後,程式碼縮減了一半。
首先定義代表表示式的列舉型別,分成單一數字,以及將兩個表示式相加或相乘的情況。
這個列舉型別會再次包含自身,因此必須加上 indirect 關鍵字。
indirect enum Expr {
case number(Double) // 數字常值
case add(Expr, Expr) // 加法
case multiply(Expr, Expr) // 乘法
}
接著建立解譯這棵樹的函式。以遞迴處理每種情況,就是直譯器模式的核心。
以下函式會接收表示式,計算並回傳實際的數值。
func interpret(_ expr: Expr) -> Double {
switch expr {
case .number(let value):
return value
case .add(let l, let r):
return interpret(l) + interpret(r)
case .multiply(let l, let r):
return interpret(l) * interpret(r)
}
}
做到這裡,就能將 1 + 2 * 3 組裝成樹並進行計算。
像 .add(.number(1), .multiply(.number(2), .number(3))) 一樣手動建立樹,再傳入 interpret,結果就會得到 7。
如何將字串算式轉換成樹?
你可能會產生一個疑問:使用者輸入的不是樹,而是像 "1 + 2 * 3" 這樣的字串。
將這個字串轉換成樹的過程稱為剖析(parsing),負責這項工作的程式則稱為剖析器。
按照標準做法,剖析器分成兩個階段。
- 詞法分析器(lexer):將字串拆成一個個 Token(例如:
1、+、2) - 剖析器(parser):依照文法規則將 Token 組裝成樹
完整實作這兩個階段會讓文章太長,所以這裡只說明概念。
重點是:直譯器模式本身只負責解譯樹。將字串轉換成樹的剖析是另一項工作。
因此在學習階段,我建議先暫時跳過剖析器,手動建立樹,先完成直譯器。這樣才能看清模式的本質。
實際上會用在哪裡?
老實說,從底層親自實作直譯器模式的機會並不多。遇到複雜語言時,使用已經成熟的剖析器函式庫好得多。
即使如此,理解這個模式仍然收穫很多。
- 能掌握正規表示式引擎在內部如何運作
- 能理解為什麼 SwiftUI 的宣告式語法是樹狀結構
- 製作搜尋篩選器或算式計算等小型 DSL(Domain-Specific Language,領域專用語言)時,可以直接套用
尤其是計算機、條件篩選器或遊戲腳本這類文法簡單、範圍有限的情況,自己製作反而更簡潔。
學會這個模式後,我看 SwiftUI 的檢視結構完全不同了。每個檢視最終都是一棵會被解譯之樹的節點。
總結
今天一邊用 Swift 製作迷你語言直譯器,一邊了解了直譯器模式。
用遞迴列舉型別建立表示式樹,再透過 interpret 函式遞迴解譯。這兩件事就是全部。
一開始可能不熟悉,但親自計算 1 + 2 * 3 後,就會出現「啊,原來是這樣」的瞬間。請務必親手輸入一次今天學到的程式碼。

