Scope, block and extent in Common Lisp.

Meta note

対象読者

Introduction.

Common Lispという言語でもスコープという言葉は使われます。 ですがブロックという言葉はCommon Lispでは異なる意味を持ちます。 Common Lispという言語でBLOCKは特殊形式というオペレータです。 他言語でいうブロックとは少々趣が異なるかと思われます。

代わりといってはなんですがCommon Lispではextentという言葉がscopeと絡めて語られます。 本稿では自分の言葉でこれらを解説してみたいと思います。

PROGN

PROGNはS式を複数受け取り、左から順に評価していき、最後のS式の評価結果をPROGN全体の評価結果として返します。 他言語でいうブロックはCommon LispではPROGNになるかと思われます。

* (progn (print :a) (print :b) (print :c))

:A      ; <--- Side effect.
:B      ; <--- Side effect.
:C      ; <--- Side effect.
:C      ; <--- Return value of PROGN.

PROGNには暗黙理のものがあります。 代表がLETの本体です。

Scope.

スコープとは見える範囲を指す言葉です。

例えばLETは局所変数を定義しますが、そのスコープ(見える範囲)はそのボディに限られます。 LETのボディは暗黙理のPROGNとなります。

* (let ((a 0))
    (print a)
    (1+ a))

0       ; <--- Side effect.
1       ; <--- Return value of LET.

* a
; Error

Type of scopes.

スコープには二種類あります。 レキシカルなスコープとダイナミックなスコープです。

Lexical scope.

LETは通常レキシカルなスコープを持ちます。 レキシカルとはこの場合「文字列上の」くらいの意味です。

以下の図ではLETのボディでAを参照するBOTTOMを呼び出していますが、BOTTOMからは変数Aが見えないのを表しています。 なぜなら文字列としては(LET ((A 0)) (BOTTOM))で閉じているからです。 変数Aはその文字列としての範囲内でのみ有効です。

image of lexical scope.

Dynamic scope.

動的なスコープを持つ変数は通常DEFVARないしDEFPARAMETERで定義します。

* (defvar *a* 0)
*A*

動的スコープの場合、スコープの解決(=変数の参照)を実行時に動的に行います。

上の図におけるBOTTOMAに対する参照の解決がコンパイル時に確定できないので、多くの処理系でコンパイルエラーとなります。(コンディションは処理系に依存します。)

参照すべき変数が動的な変数だと分かっている場合はコンパイルエラーとなることなく、変数参照を実行時に行います。

* (defun bottom2 ()
    *a*)
BOTTOM2

* (defun bottom3 ()
    (declare (special a))
    a)
BOTTOM3

上の例ではBOTTOM2は事前に動的であるとDEFVARにより宣言されている変数への参照なのでエラーとなりません。

また、BOTTOM3では変数Aは動的な参照であることをDECLAREにより宣言してあるので、これもエラーとはなりません。 ただしこの場合も以下のコードはエラーとなります。

* (let ((a 0))
    (bottom3))
; Error

なぜならLETが作る変数Aはあくまでレキシカルなスコープを持つ変数だからです。 BOTTOM3が実行時に参照する変数Aはあくまで動的なものです。 LETの変数Aはレキシカルなのでそのスコープはもう閉じてあるのでBOTTOM3からは見えません。

見えるようにするためにはLETの側にも宣言が必要となります。

* (let ((a 0))
    (declare (special a))
    (bottom3))
0

Extent

エクステントとは値が生きている時間軸的な長さを表す言葉です。

エクステントには2種類あります。 おのおの、動的なエクステントと無限のエクステントとになります。

indefinite extent.

Common Lispのオブジェクトは通常無限エクステントを持ちます。

そのためレキシカルなスコープを抜けた後も変数への参照が残り続けるということが起こります。

* (let ((a 0))
    (lambda () a))
#<FUNCTION (LAMBDA ()) {...}>

* (funcall *)
0

上のコード例ではまずLETが変数Aを作り無名関数を返します。 無名関数の中ではAへの参照が保持されます。

変数Aのレキシカルなスコープはもう閉じています。 ですが返り値であるLAMBDALETのレキシカルな(文字列としての)スコープ内で作られたものでレキシカルなスコープ内で変数Aへの参照を保持しています。 この参照はレキシカルなスコープが閉じた後にも残り続けるので、関数呼び出しを行うと正しく0を返します。

このレキシカルな変数への参照を閉じ込めた関数のことをクロージャといいます。

Dynamic extent.

動的なエクステントとはいうなれば一時的なエクステントです。

WITH-OPEN-FILEで開いたストリームはWITH-OPEN-FILEのレキシカルなスコープ内でのみOPENな状態であり、スコープを抜けると同時にストリームは閉じられます。 ストリームに束縛される変数は、クロージャで包めばスコープを抜けた後でも参照はできます。 ですがそのストリームは閉じられた後のストリームになります。

また変数に宣言をすることで変数を動的エクステントであると宣言することができます。

* (let ((a 0))
    (declare (dynamic-extent a))
    (lambda () a))
#<FUNCTION (LAMBDA ()) {...}>

上の例ではLETが作る変数Aに動的エクステントであるという宣言がされています。 返された無名関数は変数Aへの参照を保持し続けますが、その変数Aが実行時に有効であるかどうかはわかりません。 動的エクステントであると宣言されているので、GCが値を回収し、実行されるその瞬間には全く関係ない値がそのアドレスに置かれているかもしれません。

BLOCK

Common Lispという言語でBLOCKは特殊形式です。 BLOCKの中からはRETURN-FROMで値を返すことができます。

* (block :a
    (return-from :a 3))
3

BLOCKはレキシカルなスコープを持ちます。

* (defun test () (return-from :a 0))
; Error

上記コードはコンパイル時のレキシカルな環境下に:Aという名前のBLOCKがないので多くの処理系でエラーとなります。

レキシカルな環境はクロージャで包むことで渡すことが可能となります。

* (defun bottom4 (returner)
    (funcall returner))
BOTTOM4

* (block :a
    (bottom4 (lambda () (return-from :a 0)))
    (print :never))
0

上記コードではBLOCK:Aを無名関数にクロージャとして包んでBOTTOM4に渡しています。 BOTTOM4は無名関数を呼び出すだけのものなので、PRINTにはけしてたどり着くことはありません。

Implicit BLOCK.

BLOCKには暗黙理に作られるものもあります。

defun

関数を定義すると、暗黙理に関数名と同じ名前を持つBLOCKが作られます。

* (defun test () (return-from test 0) (print 0))
TEST

* (test)
0

DO family and LOOP.

DOのファミリーとLOOPマクロは暗黙理にNILという名前のBLOCKを形成します。

NILを名前に持つブロックからはRETURNで帰れます。 RETURNは単に(return-from nil ...)へ展開されるマクロでしかありません。

* (dotimes (x 5) (if (oddp x) (return :return) (print x)))

0
:RETURN