クロージャの動作と展望(?)

クロージャについての言及があったので、ちょっと見解を述べてみる。
On lispに書いてある通りに説明しようとしたら、上手く動かなかった*1ので、xyzzy上で確認できたことを書く。


私がクロージャといっているのは、主にレキシカルクロージャを指している。
lexcalというのは、語彙とか辞書という意味だが、これは、定義された関数が、引数にない変数(フリー変数)を参照しているときに、実際に何を参照するべきかを関数が持っている辞書に記録していることを示している。
関数は、その関数が定義されたクロージャ内の変数を覚えている。


xyzzyクロージャが意識されるのは、主にlet周りである。
トップレベルで定義された関数は、letで同じ名前の変数が束縛されていても、トップレベルの変数を参照する。
また、let内で定義された変数は、トップレベルや別のlet内で束縛された変数とは無関係でいられる。この場合、もとのletは2度と呼ばれない(同じコードを実行しても、それは新しいクロージャである)から、その変数にさわれるのは、その関数だけということになる。


日常言語で説明しようとしたら、書くのに随分時間がかかってしまった。ちょっとややこしいが、次の例を見てもらおう。
トップレベルのクロージャで定義した関数と、letのクロージャ内で定義した関数を比べている。
次に、それぞれの関数が参照している変数をスペシャル変数にして、挙動の変化を見てみる。
全体の状態が変わる式があるので、上から実行していると思ってもらいたい。

;トップレベル
(defun foo () fizz)  ;>foo

(foo)  ;>変数が定義されていません: fizz

(let ((fizz t))
  (foo))  ;>変数が定義されていません: fizz

buzz  ;>変数が定義されていません: buzz

;letクロージャ
(let ((buzz nil))
  (defun bar () buzz))  ;>bar

(bar)  ;>nil

(let ((buzz t))
  (bar))  ;>nil

;スペシャル関数にしてみる
(defvar fizz 1)  ;>fizz

fizz  ;>1

(foo)  ;>1

(let ((fizz t))
  (foo))  ;>t

(defvar buzz 2)  ;>buzz

buzz  ;>2

(bar)  ;>2

(let ((buzz t))
  (bar))  ;>t

変数をdefvarでスペシャル変数にする前が、標準の挙動である。
これをみると、buzzをスペシャル変数にすると都合が悪いことが分かる。
スペシャル変数になる前は、安全にbarだけがbuzzを参照できているのだが、スペシャル関数にしてしまうと、トップレベルで定義したのと差がなくなってしまう。
まあ、安易にスペシャル関数にしなければいいのだが。

応用?

クロージャを使ったコマンド - 象徴ヶ淵
のように、内部に変数を持つ関数を作りたいという考えは、突き詰めていくとオブジェクト指向になる。
ちょっと考えると、こうした内部変数を持つ関数に、キーワード指定で変数を読み書きするようにすると、丁度オブジェクトと同じ構造になることが分かる。

;疑似コード うろ覚えのJavaっぽくしてみる。
(class object (fizz buzz)
  (const (n) (setq fizz n))
  (private mem (n) (setq buzz n))
  (public put (n) (incf fizz n))
  (public cut (m) (setq fizz (/ fizz (mem m))))
  (public show (&optional (fp t)) (format fp "~A個" fizz)))

(new obj (make object 100)
(obj :show)
> 100(obj :cut 4)
> 25

(obj :mem 1)
> Error: アクセスできないメソッドです。

defmacroとlabelsを駆使すると、大体こんな感じでオブジェクト?を作れるように実装できると思う。
実際に作ってみようと思ったが、面倒だし、その割りに実用的ではないので止めておく。
継承とか面倒そうだし。必要性を感じたら、CLOSを勉強すればよい。


まだ、クロージャの決定的な利点は知らないけど、現段階では、見やすさや解り易さを考えて使うべきじゃないかと思っている。
.xyzzyで、何気なく使っているのを見つけた(自分で書いたのだが忘れている)。

;paren
(let ((fn (lambda()
            (series 2 (set-syntax-match (syntax-table))
                    #\( #\)
                    #\{ #\}
                    #\[ #\]))))
  (series 2 add-hook
          'katex-mode-hook fn
          '*fundamental-mode-hook* fn))

seriesは自分で適当に作ったマクロで、第2引数に第1引数個の引数を渡してprognで繰り返すものだ。
mapcarでもできる気がするが、実際に書いてみると妙に繁雑になってしまう。
そういうものと、クロージャによる変数で見た目の重複を避けている。
まあ、これは書き直しておこう。今はもっと単純な書き方を知っているので。

(series 2 add-hook
  'katex-mode-hook
  #1=(lambda()
        (series 2 (set-syntax-match (syntax-table))
          #\( #\)
          #\{ #\}
          #\[ #\]))
  '*fundamental-mode-hook* #1#)

ちょっと統一感はないけど、みにくいletを外せた。大抵の場合はマクロの方がすっきりするものだ。
(段々適当になるいつものパターンだな)

*1:要は、理解が足りてないんだ。