詞法作用域 vs 動態作用域
詞法作用域 vs 動態作用域
連結:https://www.jianshu.com/p/cdebb5965000
scheme是一門採用詞法作用域(lexical scoping)的lisp方言,這個設計是從alogol語言裡借鑑過來的。現在,詞法作用域已經被許多lisp方言所吸收,實踐表明,這的確是一項正確的設計,避免了很多奇怪的錯誤,比較符合人類的思維習慣。
但是,在某些場合下,動態作用域又是很有用的特性,比如emacs lisp裡面就預設採用動態作用域。
下面的程式演示了詞法作用域與動態作用域的不同
(define x 1)
(define y (lambda () x))
(let ([x 2]) (y))
如果是詞法作用域,返回1,如果是動態作用域,返回2。
在scheme中模擬動態作用域
從一個簡單的例子出發,演示如何在scheme裡面實現動態作用域,我們想要寫一個try catch巨集來處理程式中的異常。其中一個函式叫做current-exception-handler,我們希望它是動態的,隨程式碼執行位置而變化,永遠指向當前的異常處理器,下面是我們寫的第一個版本
(define current-exception-handler
(lambda (msg) (error "No Top Level Try")))
(define-syntax try
(syntax-rules (catch)
[(_ expr ... (catch msg expr* ...))
(call/cc (lambda (k)
(let ([msg (call/cc (lambda (k1)
(set! current-exception-handler k1)
(let ([result (begin expr ...)])
(k result)
)))])
expr* ...)))]))
(define (throw msg) (current-exception-handler msg))
其中,涉及throw的程式碼必須被包含在try裡面,否則會導致錯誤。先來測試一下
(try 1
(throw 'foo)
(catch msg (display "catch ") (display msg)))
但是如果我們在後面再加上一行程式碼
(throw 'test)
這裡就出現問題了,按照我們上面的要求throw應該在try catch塊裡面使用,而這裡卻不會報錯,說明我們上面的程式碼錯了。
進入try catch塊時,我們把current-exception-handler設定為當前try catch塊的exception-handler,但當執行出try catch塊的時候,exception-handler並沒有發生變化,我們希望恢復原來的exception-handler,使得不論是正常退出或者是發生錯誤退出都能恢復原有的exception-handler,所以修改一下程式碼,就是:
(define current-exception-handler
(lambda (msg) (error "No Top Level Try")))
(define-syntax try
(syntax-rules (catch)
[(_ expr ... (catch msg expr* ...))
(call/cc (lambda (k)
(let ([msg (call/cc (let ([preserved current-exception-handler])
(lambda (k1)
(set! current-exception-handler (lambda (msg)
(set! current-exception-handler preserved)
(k1 msg)))
(let ([result (begin expr ...)])
(set! current-exception-handler preserved)
(k result)
))))])
expr* ...)))]))
(define (throw msg) (current-exception-handler msg))
這樣,只要程式碼出了try catch塊,current-exception-handler就會恢復成原來的,從而實現了動態作用域的效果。
fluid-let語句
如果你記得fluid-let語句的話,你就會發現上面的程式碼效果和fluid-let語句很類似,沒錯,fluid-let語句就是被設計用來實現dynamic scoping效果的,fluid-let語句的定義如下(摘自chez scheme user guide 8):
(define-syntax fluid-let
(lambda (x)
(syntax-case x ()
[(_ () b1 b2 ...) #'(let () b1 b2 ...)]
[(_ ((x e) ...) b1 b2 ...)
(andmap identifier? #'(x ...))
(with-syntax ([(y ...) (generate-temporaries #'(x ...))])
#'(let ([y e] ...)
(let ([swap (lambda ()
(let ([t x]) (set! x y) (set! y t))
...)])
(dynamic-wind swap (lambda () b1 b2 ...) swap))))])))
具體的實現細節就不說了,給一個例子:
(define x 1)
(define y (lambda () x))
(fluid-let ([x 2]) (y))
這和第一段程式碼一模一樣,只不過用了fluid-let語句,就在scheme裡面模擬出了dynamic scoping的效果。
這樣,try catch巨集就可以寫的很簡單了:
(define-syntax try
(syntax-rules (catch)
[(_ expr ... (catch msg expr* ...))
(call/cc (lambda (k)
(let ([msg (call/cc (lambda (k1)
(fluid-let ([current-exception-handler k1])
(let ([result (begin expr ...)])
(k result)
))))])
expr* ...)))]))
parameterize語句-更好的選擇
但是出於某些原因(效率,賦值,標準庫裡面沒有提供。。),我們並不想用fluid-let語句來解決問題,恰好rnrs標準庫裡面就提供了一個類似的,parameterize語句,同樣可以完成任務。
(define x (make-parameter 1))
(define y (lambda () (x)))
(parameterize ([x 2]) (y))
如果我們在parameterize塊外面嘗試呼叫y函式,就會得到1,這說明parameterize只對塊內生效,一旦出了parameterize塊(不論是通過何種方式),就會恢復成原來的樣子。
scheme與其他lisp方言在作用域方面還有一個不同點,它的巨集也是採用了詞法作用域,而其他lisp方言的巨集幾乎都是動態作用域。如果你想了解scheme的巨集如何實現動態作用域的效果,請看這裡:http://schemeworkshop.org/2011/slides/Barzilay2011.pdf
=============== End