討論如何用 racket 中的 continuation 實作 effect system 的概念
一個 effect system 大概做了什麼,我們從 effekt 語言 (https://effekt-lang.org/) 這個案例著手
- 一個簽名
effect yield(v : Int) : Bool - 一個 handler
try { ... } with yield { n => ...; resume(n < 10) } - 在
try區塊中調用的函數f可以用do yield(k)來調用 handler - 當
f執行到do yield(k),就會跳轉到with yield中繼續進行,並且n = k - 當程式執行到
resume就會跳回去f之後的do yield(k)的續延繼續執行
因此 exception 系統也可以被視為,沒有調用 resume 的 effect handler,讀者或許也想出數種使用方法了吧?這正是 effect system 的價值。而 racket 擁有比 effect handler 更複雜的機制(continuation with marks),不過 effect handler 機制具有保障使用者不容易出錯的好處,因此在 racket 中模擬一套 effect system 依然有意義,也是有趣的挑戰。
call/prompt [rkt-0000]
call/prompt [rkt-0000]
call/prompt 的主要功能,就如其名稱,是為被呼叫的函數提示一個標記,這個標記在 abort/cc 會被使用。完整的 call/prompt 調用是
(call/prompt f tag handler args ...)
f 是被調用的函數,中間的 tag 是標記,handler 是處理標記的函數,剩下的都是 f 的參數。讀者可以想像成
(with-handler [tag handler] (f args ...))
abort/cc [rkt-0001]
abort/cc [rkt-0001]
abort/cc 的調用是
(abort/cc tag vs ...)
tag 當然就是先前 prompt 寫進去的 tag,而 vs ... 則作為呼叫 handler 的引數
(handler vs ...)
Continuation prompt tag 的產生方式 [rkt-0002]
Continuation prompt tag 的產生方式 [rkt-0002]
調用 (make-continuation-prompt-tag) 產出,這個函數還可以接受一個 symbol 產生可讀的識別碼,並且每次調用這個函數,產生的 tag 都算是不同的 tag,勿必要儲存好這個實體。
要想實現 exception 目前的程式已經完全充分了,只要寫下
(define (f params ...)
(abort/cc tag "cannot read file ..."))
(call/prompt f
tag
(λ (err)
(printf "got err ~a~n" err))
args ...)
即可。事實上 racket 自身的的 exception 也是這樣實作的,要考慮的問題是,前面我們看過 resume 可以跳回去被呼叫的函數!這是如何做到的?
call/cc [rkt-0003]
call/cc [rkt-0003]
call/cc 這個函數是 call with current continuation 的意思,比如說在下面的程式碼中
(* 2 1)
1 的 continuation 就是把 1 挖掉留下的 (* 2 hole),故下列程式碼
(* 2 (call/cc (λ (k) (k 1))))
的結果是 2。讀者可以想像 k = λ (hole) (* 2 hole),這在實作上不太正確,但對理解很有幫助。
一般來說,racket 的函數會簡單的寫成
(define (f params ...) stmt1 ... stmtn)
如果我在其中安放一個 call/cc 會發生什麼事呢?
(define (f params ...) ... (call/cc (λ (k) ...)) stmtk ...)
答案是 k = λ () (begin stmtk ... stmtn)。由於 racket 自動把
stmt1 ... stmtn
包裝成 (begin stmt1 ... stmtn),而
(begin stmt1 stmt2 ...)
中 stmt1 的 continuation 是 (begin stmt2 ...),以此類推就很容易理解 k 等於什麼了。
我們正是需要使用 call/cc 來做出 resume 的效果。
實現 resume [cs-000H]
實現 resume [cs-000H]
在 f 中我們寫下
(define (f params ...) (call/cc (λ (k) (abort/cc tag k ...))))
在 call/prompt 的 handler 中新增一個 resume 參數,這樣就完成了。讀者可以填補下面的程式中的空白來檢驗結果,也充當練習
(define (f params ...)
(call/cc (λ (k) (abort/cc tag k ...)))
...)
(call/prompt f
tag
(λ (resume ...)
...
; 跳回 f 繼續
(resume))
args ...)到這裡我們已經有了運行的基礎,要改善有幾個方向可以參考
- 用 macro 包裝語法
- 放到 typed/racket 中加上 effect 類型檢查確保使用者沒有漏掉 handler:typed/racket eff
- 支持 finally handler,保證任何退出行為都會被這個 handler 攔截(考慮 racket 的函數
dynamic-wind)