スペシャル変数とLET
Common Lispではdefvarを使ってグローバル変数を作ると、
その変数はスペシャル変数となり、無限スコープと動的エクステントを持つようになります。
(defvar *special* 99) (defun f () (let ((*special* 1)) (g *special*))) (defun g (x) (+ x *special*))
上のプログラムから分かるように、*special*はどこからでも参照できます(無限スコープ)。
関数fのletにて、*special*に新たな値を設定すると、そのletを抜けるまでの間、
*special*の値は1に変わります(動的エクステント)。
よって、fのletがgにも影響して、 (f) => 2 となります。
しかし、これが正しく動作するのは*special*がスペシャル変数であるということを
処理系が知っているからです。関数fの定義だけを取り出してみると、
letで束縛している変数*special*が通常の変数なのかスペシャル変数なのか分かりません。
と、いうことでプログラムを次の様に書き換えてみました。
;;; test.lisp (defun f () (let ((v 1)) ; この時点ではvがスペシャル変数であることが分からない (g v))) (defun f1 () (let ((v 1)) (declare (special v)) ; vはスペシャル変数だと明示的に宣言 (g v))) (defvar v 99) ; ここでvがスペシャル変数であることが分かる (defun g (x) (+ x v)) ; このvはスペシャル変数である (defun h () (let ((v 1)) ; このvはスペシャル変数である (g v)))
このプログラムをREPLで次の様に読み込ませて実行させます。
> (load "test.lisp") => T > (f) => ??? > (f1) => ??? > (h) => ???
CLISP, CMUCL, SBCLでの結果は以下の通りでした。
– | (f) | (f1) | (h) |
---|---|---|---|
CLISP | 2 | 2 | 2 |
CMUCL | 2 | 2 | 2 |
SBCL | 100 | 2 | 2 |
SBCLの (f) だけ値が他と違います。
これは、SCBLがロードとともに式をコンパイルするため、
関数fをコンパイルする時点で最適化により、vという名前が消失してしまうからでしょう。
(すると、fは単にgに1を適用することになり、gはスペシャル変数vに1を足し、結果は100となります)
CLISP, CMUCLでもcompile-fileしたものをロードすると (f)の値は100となりました。
ちなみに、変数の名前を*special*からvに変更しましたが、
*special*のままにして、SBCLでプログラムをロードすると次のような警告がでます。
; caught STYLE-WARNING: ; using the lexical binding of the symbol (*SPECIAL*), not the ; dynamic binding, even though the name follows ; the usual naming convention (names like *FOO*) for special variables
「レキシカル変数のくせして、目立つんじゃねーよ!」ということですね。
このメッセージを読むと、スペシャル変数を誤ってレキシカル変数として扱う可能性は減るでしょう。
Let Over Lambdaではスペシャル変数をアスタリスクで囲わない方針をとっていますが、
誤ってdefvarの登場場所より前でその変数を使ってしまうことを考えると、
スペシャル変数はアスタリスクで囲ったほうがいいとおもいます。