2012/12/07
ローカルスコープ内からグローバル定義
twitterでのやりとりがきっかけで思い出したので書いておく。
Common Lispのdefun
はどこに書いてあってもグローバル(トップレベル)に関数を定義する。レキシカルな束縛の中からdefun
すれば、その束縛変数をクローズすることができる。
;; Common Lisp (let ((count 0)) (defun inc () (incf count)) (defun dec () (decf count)))
count
はinc
とdec
によってしかアクセスされない、クローズドされた変数となる。
cl-user> (inc) 1 cl-user> (inc) 2 cl-user> (dec) 1 cl-user> (dec) 0
Common Lispではありふれたテクニックなのだが、CLのコードをそのままSchemeに移植しようとするとはまる。
;; Scheme - 動かない (let ((count 0)) (define (inc) (inc! count)) (define (dec) (dec! count)))
Schemeではlet
内のdefine
はそのスコープだけのローカルな定義(internal define)になってしまうので、let
の外に影響を及ぼすことはできない。
(さらに厳密に言えば、Schemeの仕様ではlet
の本体には(defineによる定義以外に)ひとつ以上の式がないとまずいので、上の例は正しいプログラムでさえない。)
これは言語の方針の違いだ。SchemeはCommon Lispよりも少し静的寄りで、プログラムの実行開始時までにトップレベル束縛がなるべく決まっていて欲しいと思う傾向がある。ところが内側のスコープからのグローバル定義を許すと、例えばこんなコードが書けてしまう。
;; Common Lisp (defun bind-foo (x) (defun foo () x))
このコードでは、bind-foo
を実行するまで foo
という関数は定義されない。
bind-foo
を実行すると突如としてトップレベルにfoo
という関数が
現れることになる。この動作を気持ち悪いと思うかそういうもんでしょと思うかで
あなたはSchemerタイプかCLerタイプかが判別できるぞ!
Schemeで、inc
とdec
にローカルな環境をクローズしたい場合は、
環境を共有するクロージャを作って、それをトップレベルで束縛する。
;; Scheme (define-values (inc dec) (let ((count 0)) (values (lambda () (inc! count)) (lambda () (dec! count)))))
(define-values
はR6RSまでにはないけど多くの処理系に備わっている。syntax-rules
ですぐ書ける。Gaucheの実装はこれ。
R7RSには入りそう。)
まあでも、CL版に比べるとかなりまどろっこしい。
で、何年か前にCL風に書きたいなと思ったことがあってこんなマクロを書いた。
これを使うと、上のinc/decの例はこんなふうに書ける。
;; Scheme (toplevel-let ((count 0)) (define-toplevel (inc) (inc! count)) (define-toplevel (dec) (dec! count)))
toplevel-let
のbody部に直接現れるdefine-toplevel
はトップレベル定義になる。body部には普通のdefineも書けて、そっちはinternal defineになる。完全にCLと等価な動作ではないけど (define-toplevel
をさらにネストした式の中に書くことはできないとか、body部に式を書けるけどその式の実行時にはまだdefine-toplevel
してる変数が見えないとか)、普通使うパターンはだいたいカバーできると思う。
これ、当時はそのうちGauche本体に足そうと思ってたんだけど、しばらく使ってみたらそこまで便利でもないなあと思ったので結局入れてない。CL風に発想してると便利なんだけど、最初からScheme風に発想してるとあんまりこういうコードが出てこないんだよね。
でも便利だから欲しいって声があれば入れるかも。
Tags: CommonLisp, Scheme, Gauche, Programming
tavi (2012/12/12 00:48:43):