詞法作用域 vs 動態(tài)作用域
scheme是一門采用詞法作用域(lexical scoping)的lisp方言,這個設計是從alogol語言里借鑒過來的。現(xiàn)在,詞法作用域已經被許多l(xiāng)isp方言所吸收,實踐表明疤苹,這的確是一項正確的設計,避免了很多奇怪的錯誤敛腌,比較符合人類的思維習慣卧土。
但是凸椿,在某些場合下娄昆,動態(tài)作用域又是很有用的特性,比如emacs lisp里面就默認采用動態(tài)作用域囤官。
下面的程序演示了詞法作用域與動態(tài)作用域的不同
(define x 1)
(define y (lambda () x))
(let ([x 2]) (y))
如果是詞法作用域生棍,返回1颤霎,如果是動態(tài)作用域,返回2。
在scheme中模擬動態(tài)作用域
從一個簡單的例子出發(fā)友酱,演示如何在scheme里面實現(xiàn)動態(tài)作用域晴音,我們想要寫一個try catch宏來處理程序中的異常。其中一個函數(shù)叫做current-exception-handler缔杉,我們希望它是動態(tài)的锤躁,隨代碼運行位置而變化,永遠指向當前的異常處理器或详,下面是我們寫的第一個版本
(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)
這里就出現(xiàn)問題了椒振,按照我們上面的要求throw應該在try catch塊里面使用,而這里卻不會報錯梧乘,說明我們上面的代碼錯了杠人。
進入try catch塊時,我們把current-exception-handler設置為當前try catch塊的exception-handler宋下,但當運行出try catch塊的時候,exception-handler并沒有發(fā)生變化辑莫,我們希望恢復原來的exception-handler学歧,使得不論是正常退出或者是發(fā)生錯誤退出都能恢復原有的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就會恢復成原來的揭蜒,從而實現(xiàn)了動態(tài)作用域的效果横浑。
fluid-let語句
如果你記得fluid-let語句的話,你就會發(fā)現(xiàn)上面的代碼效果和fluid-let語句很類似屉更,沒錯徙融,fluid-let語句就是被設計用來實現(xiàn)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))))])))
具體的實現(xiàn)細節(jié)就不說了瑰谜,給一個例子:
(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函數(shù)称勋,就會得到1胸哥,這說明parameterize只對塊內生效,一旦出了parameterize塊(不論是通過何種方式)赡鲜,就會恢復成原來的樣子空厌。
scheme與其他lisp方言在作用域方面還有一個不同點,它的宏也是采用了詞法作用域银酬,而其他lisp方言的宏幾乎都是動態(tài)作用域嘲更。如果你想了解scheme的宏如何實現(xiàn)動態(tài)作用域的效果,請看這里:http://schemeworkshop.org/2011/slides/Barzilay2011.pdf