Common Lispのダイナミック・スコープについて勉強したのでまとめてみた.まず,要点は以下の通り.
-
Lispのスコープは,レキシカル・スコープとダイナミック・スコープの2種類ある.
-
ダイナミック・スコープに従う変数をスペシャル変数という.
-
スペシャル変数は
defvar
かdefparameter
によって作成される. -
スペシャル変数と同名の変数が作成されると,スコープを超えて同名のスペシャル変数全てが新しい変数を参照する.つまり,スコープを超えて同名の変数がシャドウイングされる.
以下,順を追って見ていこう.
レキシカル・スコープとダイナミック・スコープ
レキシカル・スコープでは,評価器がコードを読み込んだ時点で変数のスコープが決定する.つまり,書かれた通り(レキシカル)に変数のスコープが決まる.それに対し,ダイナミック・スコープでは実行時に変数の有効範囲が変わる.スコープが動的に決まるのだ.昨今のほとんどのプログラミング言語はレキシカル・スコープを採用している.
Common Lispでも 通常の変数 はレキシカル・スコープに従う.それではどんな変数がダイナミック・スコープになるかというと,通常じゃないスペシャルな 変数だ.その名も スペシャル変数 である.
defvar
か defparameter
によって作られた変数は全てスペシャル変数になる.
レキシカル・スコープの場合
まず,JavaScriptを使って,レキシカル・スコープの例を見てみよう.そのあと,同じコードをCommon Lispで記述し,ダイナミック・スコープとの違いを明らかにする.
var glvar = "Global";
var inner = function () {
console.log("In inner: " + glvar); // グローバル変数
}
var shadowing = function () {
var glvar = "overriden!";
console.log("In shadowing: " + glvar);
inner(); // この中のglvarには上のローカルglvarは影響を及ぼせない.
}
inner(); // -> In inner: Global
shadowing(); // -> In shadowing: overriden!
// In inner: Global
shadowing()
関数内でグローバル変数と同名の glvar
を定義してグローバル変数をシャドーイングしている(コード1の8行目).これによって9行目の実行結果(15行目のコメント)はローカル変数の値で上書きされる.しかし,shadowing()
関数から呼び出される inner()
関数内の glvar
には上書きの効果は及ばない(16行目のコメント).これは,inner()
関数の定義が評価器によって読み込まれた時点でレキシカル(書かれた通り)にスコープが決まるからだ.トップレベルで inner()
関数が呼び出されても,shadowing()
関数内から呼び出されても,inner()
関数内の glvar
のスコープは変化しない.
Common Lispでは glvar
がスペシャル変数なら,shadowing()
関数内で新たに作成された glvar
は,同名の変数を全てシャドウイングし,inner()
関数内の glvar
も新しく作成された変数を参照することになる.
Common Lispのダイナミック・スコープ
コード1をLispに書き換えたものがコード2である.1行目で *glvar*
に "Global"
文字列を設定している.トップレベルで作成された *glvar*
はグローバル変数である.また,defvar
で作成されたのでスペシャル変数でもある.Lispではスペシャル変数をアスタリスク(イヤーマフ)で囲む習慣がある.
(defvar *glvar* "Global")
(defun inner ()
(format t "In inner: ~s~%" *glvar*))
(defun shadowing ()
(let ((*glvar* "overriden!"))
(format t "In shadowig: ~s~%" *glvar*)
(inner)))
(inner) ; -> In inner: "Global"
(shadowing) ; -> In shadowig: "overriden!"
; In inner: "overriden!"
inner
関数内の *glvar*
(4行目)の参照先は実行時に変化する.6行目の let
で同名のローカル変数が作成されると,JavaScriptと同様,同じブロック内の *glvar*
(8行目)もローカル変数によってシャドウイングされる(13行目のコメント内に実行結果).ダイナミック・スコープのCommon Lispでは,このローカル変数の有効期間内に参照される全ての *glvar*
は,たとえ他の関数内であっても全てこのローカル変数によってシャドウイングされる.実際,9行目で呼び出される inner
関数の実行結果(14行目)を見ると確かに値が上書きされている.これがダイナミック・スコープに従うスペシャル変数の特徴である.
グローバルだがスペシャル変数ではないとどうなるか?
Common Lispの規格ではレキシカル・スコープに従うグローバル変数は未定義である.トップレベルで defvar
と defparameter
を使わずに変数を作成できるかは実装系に依存する.SBCLではトップレベルでも setf
によって変数を作成できる.以下のコード3では,レキシカル・スコープのグローバル変数 glvar
を作成しコード2と同じ処理を実行した結果である.
(setf glvar "Global")
(defun inner2 ()
(format t "In inner: ~s~%" glvar))
(defun shadowing2 ()
(let ((glvar "overriden!"))
(format t "In shadowig: ~s~%" glvar)
(inner)))
(inner2) ; -> In inner: "Global"
(shadowing2) ; -> In shadowig: "overriden!"
; In inner: "Global"
14行目コメント欄の実行結果を見ると,JavaScriptと同様,inner
関数には let
で作成された変数の影響が及んでいないのが分かる.
スペシャル変数の使い道は?
デフォルト値のオーバーライド
スペシャル変数はアプリケーションのデフォルト値の設定に使うと便利だ.例えば,テキストをファイルに書き出すアプリケーションがあったとしよう.デフォルトの出力先は output/
ディレクトリで,ユーザがオプションで出力先を変えられるようにしたいとする.
この場合,出力先のパスを *outdir*
スペシャル変数に保存しておく.
(defparameter *outdir* "output/")
アプリケーション内の全てのコードは,出力先ディレクトリとして *outdir*
スペシャル変数を参照する.
ユーザの出力先指定はアプリケーションの起動関数で処理する.出力先が与えられていたら *outdir*
をオーバーライドする.これでアプリケーション内の全べての関数で,*outdir*
の値がオーバーライドされる.
(defun main (&optional outdir)
(let ((*outdir* (if outdir outdir *outdir*))) ; (1)
(format t "~a~%" *outdir*)))
1 | outdir が設定されていれば outdir を そうでなければデフォルトの *outdir* を設定する. |
以下が実行例だ.
> (main)
output/
NIL
> (main "my-output/")
my-output/
NIL
入出力のコントロール
Lispのテキストで最もよく出てくるスペシャル変数の例は入出力先のコントロールだ.通常スペシャル変数はグローバル変数として使う.グローバル変数の利点はどこからでも参照できるため,関数に渡す引数を省略できる.Common Lispでは標準入出力のストリームがスペシャル変数として予め提供されているので,入出力系の関数内で入出力先のストリームを引数で渡す必要がない.しかし,ファイルに出力したい時もある.この時,新たにファイル出力用の関数を作成しなくても,同名のスペシャル変数をローカルで作成し,そこにファイル出力のストリームを代入すれば,そのローカル変数が有効な間,入出力をリダイレクトできる.既存の入出力系の関数をそのまま再利用できるのだ.
コード2では (format t …
で標準出力に結果を表示した.試しに標準出力のストリーム型オブジェクトを格納したスペシャル変数 *standard-output*
をファイルに結びつけて出力をリダイレクトしてみよう.
(defun print->file ()
(let ((*standard-output* (open "shadow.txt" ;; スペシャル変数をファイルストリームで一時的に上書き
:direction :output
:if-exists :supersede)))
(shadowing) ;; shadowing関数内のスペシャル変数もシャドウイングされる
(close *standard-output*)))
(prnt->file)
まず,2行目の open
関数が指定されたファイルへの出力ストリームを返す.それを let
を用いて標準出力のストリーム型オブジェクトを指す *standard-output*
に代入する.これでカレントディレクトリのshadow.txtファイルに2行出力される.上の例は自分でファイルストリームをオープンしローカル変数に束縛しているが,通常は with-open-file
マクロを使って次のように書く.with-open-file
マクロを使うとストリームを自分で閉じなくて良い.
(defun print->file ()
(with-open-file (*standard-output* "shadow.txt"
:direction :output
:if-exists :supersede)
(shadowing)))
(prnt->file)
すっきりとしていて美しい….