軟體設計

Swift 直譯器模式(Interpreter Pattern):親手打造迷你語言直譯器

翻開設計模式的書,總會在接近最後的地方看到直譯器模式。這是很容易因為「這到底什麼時候會用到」而隨便跳過的模式。

閱讀 4 分鐘
Swift 直譯器模式(Interpreter Pattern):親手打造迷你語言直譯器 封面圖

翻開設計模式的書,總會在接近最後的地方看到直譯器模式。這是很容易因為「這到底什麼時候會用到」而隨便跳過的模式。

不過,在製作計算機 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),負責這項工作的程式則稱為剖析器。

按照標準做法,剖析器分成兩個階段。

  1. 詞法分析器(lexer):將字串拆成一個個 Token(例如:1、+、2)
  2. 剖析器(parser):依照文法規則將 Token 組裝成樹

完整實作這兩個階段會讓文章太長,所以這裡只說明概念。

重點是:直譯器模式本身只負責解譯樹。將字串轉換成樹的剖析是另一項工作。

因此在學習階段,我建議先暫時跳過剖析器,手動建立樹,先完成直譯器。這樣才能看清模式的本質。

程式碼在畫面上,樹畫在筆記上。這樣畫出來就很容易理解
程式碼在畫面上,樹畫在筆記上。這樣畫出來就很容易理解

實際上會用在哪裡?

老實說,從底層親自實作直譯器模式的機會並不多。遇到複雜語言時,使用已經成熟的剖析器函式庫好得多。

即使如此,理解這個模式仍然收穫很多。

  • 能掌握正規表示式引擎在內部如何運作
  • 能理解為什麼 SwiftUI 的宣告式語法是樹狀結構
  • 製作搜尋篩選器或算式計算等小型 DSL(Domain-Specific Language,領域專用語言)時,可以直接套用

尤其是計算機、條件篩選器或遊戲腳本這類文法簡單、範圍有限的情況,自己製作反而更簡潔。

學會這個模式後,我看 SwiftUI 的檢視結構完全不同了。每個檢視最終都是一棵會被解譯之樹的節點。

理解這個模式後,SwiftUI 檢視看起來完全不同
理解這個模式後,SwiftUI 檢視看起來完全不同

總結

今天一邊用 Swift 製作迷你語言直譯器,一邊了解了直譯器模式。

用遞迴列舉型別建立表示式樹,再透過 interpret 函式遞迴解譯。這兩件事就是全部。

一開始可能不熟悉,但親自計算 1 + 2 * 3 後,就會出現「啊,原來是這樣」的瞬間。請務必親手輸入一次今天學到的程式碼。

延伸閱讀