[PL] 支援 recursion 的直譯器
1. 直譯器與閉包
直譯器中表示閉包(closure)的方式一般是捕捉當前環境(environment)與紀錄函數體的 term 來達成,例如
這裡採用 DBI1 形式的 environment
type Env = [Val] data Clos = Clos Env Exp data Val = Lam Clos | ...
而化約規則是把引數插入 environment 之後執行
extend :: Env -> Val -> Env extend env v = v : env apply :: Val -> Val -> Val apply (Lam (Clos env e)) v = eval (extend env v) e eval :: Env -> Exp -> Val eval env (App t u) = eval env t `apply` eval env u
這個方式運作良好,也很容易重複使用 Clos
到其他有一樣化約規則的形式上,例如 Pi type ,但是這個方法遇到遞迴定義的時候就會失敗
2. 遞迴定義
考慮下面的情況
let rec f : 'a -> 'a = fun x -> f x
f
可以這樣檢查:
- 用
'a -> 'a
檢查fun x -> f x
- 在 context 中安插
f : 'a -> 'a
- 在 environment 中安插
f = fun x -> f x
但要是真的這樣檢查的話,第一步就會失敗,原因是 context 中沒有 f
。於是我們可以反過來做
- 在 context 中安插
f : 'a -> 'a
- 用
'a -> 'a
檢查fun x -> f x
- 在 environment 中安插
f = fun x -> f x
這樣型別檢查應該就會通過了,但我們需要仔細思考 environment 到底是放了什麼進去。把 lambda 的 term 變成 value 的時候,一般就是把當前 environment 捕捉下來:
eval env (ELam body) = Lam (Clos env body)
因為第三步新的 Lam
需要新的 environment,然而新的 environment 也需要新的 Lam
才能構造出來,這是個不可能完成的死循環。所以上面天真的 eval
沒辦法拿到遞迴的函數自身,執行時應該會發現找不到 f
。在有參考的語言裡面,總是可以用捕獲 ref env
而非 env
來取得一個可能被更新的環境。但這個辦法會讓函數可以存取的範圍變得比預期的更大,而且在沒有這類功能的語言如 Haskell、Lean 等,這個方法是不可行的,所以我們需要不同的辦法來做這件事。
3. 解法
重新思考遞迴到底會發生什麼事,可以發現其實我們只是希望函數的 body 會再次成為 lambda 並可以被套用,於是解決方法也就變得明確了:
- 在 environment 中安插
f = recur
- 在 context 中安插
f : 'a -> 'a
- 用
'a -> 'a
檢查fun x -> f x
- 在 environment 中安插
f = fun x -> f x
這時候 f
的 Lam
會捕捉到的 environment 一定有 recur : env
的形式,現在我們說這是「recur-head environment」。在 apply
裡面需要對有 recur-head 的 lambda 做特殊處置,把 recur-head lambda 複製一次再插入 environment 中,接著才插入引數並執行 body。
apply (Lam (Clos (Recur : env) t)) u = let lam = Lam (Clos (extend env Recur) t) in eval (extend (extend env lam) u) t
這個方法之所以可以運作是因為每次潛在的 recursive point 都會對 environment 進行一次替換,把 Recur
換成能化簡的 lambda,於是自我參考就會拿到合適的 value 而非 stub value。
這裡有個顯著的缺陷是每個頂層定義都會被自動當成可能 recursive 並進行替換,但要是函數其實不是遞迴函數那這個替換就是多餘的,所以也可以多做一層分析確定函數是否有參照自身,或是乾脆給個關鍵字指出函數可不可以遞迴,用不同的模式檢查函數。
另外這個方法對於需要有 stuck application 的語言也很容易通過把 Recur
轉成 Rigid
值來產生合適的 neutral value
stuck application 意思就是不需要計算函數體套用之後的結果,例如
f x
不需要知道f
的內容,而是產生一個 neutral value 儲存本來的f x
的形式。這也是因為有時候就是不知道f
的內容,這個儲存用的 value 通常就叫做Rigid
。
4. 結論
還有一個常見的辦法是定義 f'
,然後把 f
定義成 f' f'
,但這對有型別的語言來說是否容易成功就有點未知。另外有一個文章談到的 recusive-environment2 似乎跟這裡是類似的作法。