Island Life

< Lispでメモリに直接触る | ピアノレッスン73回目 >

2012/12/07

ローカルスコープ内からグローバル定義

twitterでのやりとりがきっかけで思い出したので書いておく。

Common Lispのdefunはどこに書いてあってもグローバル(トップレベル)に関数を定義する。レキシカルな束縛の中からdefunすれば、その束縛変数をクローズすることができる。

;; Common Lisp
(let ((count 0))
  (defun inc () (incf count))
  (defun dec () (decf count)))

countincdecによってしかアクセスされない、クローズドされた変数となる。

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で、incdecにローカルな環境をクローズしたい場合は、 環境を共有するクロージャを作って、それをトップレベルで束縛する。

;; 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

Past comment(s)

tavi (2012/12/12 00:48:43):

> bind-foo を実行すると突如としてトップレベルにfoo という関数が 現れることになる。この動作を気持ち悪いと思うかそういうもんでしょと思うかで あなたはSchemerタイプかCLerタイプかが判別できるぞ!

JavaScriptにおける「varをつけないで宣言するとグローバル変数になる」を初めて知ったときと似たような感覚を覚えました。僕は「気持ち悪い」派ですね(笑)

Common Lispはやったことないですが、Schemeとの違いが面白そうです。

Post a comment

Name: