nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

closureには何が保存されるのか

closureとは関数にデータを保存するための仕組みです。
Lisp大好きな人はlambdaと一緒に多用します。
例えばこんな使い方をします。

(let ((x 0))
  (setq *call* (lambda () (incf x))))

(funcall *call*)
 -> 1
(funcall *call*)
 -> 2
(funcall *call*)
 -> 3
(funcall *call*)
 -> 4

上記の例では、lambdaにより生成された無名関数が、 変数xをclosureにて保存しています。 letで宣言された変数xのスコープはとっくに終わっているにもかかわらず、 funcallで関数を呼び出すと、変数xは立派に役目を果たしているというわけです。

では本題ですが、closureは何を対象にどんなものを保存するのでしょうか。
lexical変数だよ!と思った人は正解です。 問題はそれだけなのかということ。

典型的な回答としては下記の通り。

  • lexical変数
  • flet, labels関数

flet, labelsは局所関数を作成するためのものです。
変数ではなく関数です。
例えばこんな感じ。

(flet ((call () :hello))
  (setq *call* (lambda () (call))))

(funcall *call*)
 -> :HELLO

面倒な話題として、setf関数もclosureの対象となります。
説明すると長くなるので手短にしますが、 defsetfdefine-setf-expanderで定義されたマクロ形式の方ではなく、 flet, labelsで定義した(setf 名前)形式の関数がclosureの保存対象となります。 何が違うんだって思われるかもしれませんが、なんか結構違うんです。 そのうち詳しく説明します。

例をあげます。

(flet (((setf aaa) (value cons)
          (rplaca cons (* value value))
          value))
  (setq *call* (lambda (c v)
                 (setf (aaa c) v))))

(let ((c (cons 10 20)))
  (funcall *call* c 999)
  c)(998001 . 20)

ではdefun宣言による関数はclosureに保存されないのでしょうか?
実はされません。
下記に例をあげます。

(defun aaa ()
  :hello)

(setq *call* (lambda () (aaa)))

(funcall *call*)
  →:HELLO

(defun aaa ()
  :zzz)

(funcall *call*)
  →:ZZZ

defunによる大域関数はspecial変数みたいな扱いなんですね。

ここまではどのCommon Lispでも通用するでしょう。
ここからは処理系に依存する話になるかもしれません。

話は大きく変わり、tagbodyの実装をしたときの話です。
tagbodyのスコープを脱出したgoについて考えていました。

tagbodyは動的エクステントなので、スコープの外で呼び出されたgoは無効となります。 問題は「無効」という意味が、放っておいてもいいオブジェクト という意味では「ない」ということです。 無効になったtagは、呼び出された時点でエラーになります。 処理系は「エラーになる」という所まで面倒見てやらなければいけないのです。

例えば次の通り。

(tagbody
  (format t "Hello~%")
  again
  (format t "Tagbody~%")
  (setq *call* (lambda () (go again))))

(funcall *call*)
  →エラー、againはすでに閉じられている

どうすればいいのか。
goに結びつけられたtagを専用のオブジェクトにするのです。 tagには戻り先のtagbodyに加えて、有効無効を表すフラグを持たせます。 tagはtagbodygoで共有されます。 もしtagbodyがスコープを抜けたら、持っているすべてのtagに無効フラグを立たせます。 もしgoが無効のtagを呼び出したらエラーにします。

では、そのtagオブジェクトは誰がいくつ保有するのか。 最初の考えだと、tagオブジェクトはtagbodyのコードが唯一保有すること考えていました。 つまり、(tagbody ...)という表記が現れたら、 そのtagbodyがtagオブジェクトを保有するのだという考えです。 でもこれだと、再帰呼出か、スレッドによる同時実行により破綻します。 オブジェクトが一個しかないのですから。

解決方法は、tagオブジェクトはtagbodyを実行するたびに生成するしかなかったのです。 感覚としてはこんな感じ。

(let ((again (make-tagobject%)))
  (tagbody%
    (format t "Hello~%")
    again
    (format t "Tagbody~%")
    (setq *call* (lambda () (go% again)))
  (close-tagobject% again))

となると、当然tagオブジェクトもclosureに保存する必要が出てきます。
話が長くなりましたが、結局はtagbodyのtagもclosureに入れるんだよっていうことです。

全く同じ話がblock/return-fromにも当てはまります。 しかしcatch/throwは対象外。 catchspecial変数みたいな扱いなので必要ありません。

結論は、closureには下記のオブジェクトが格納される。

  • lexical変数
  • flet, labels関数
  • flet, labelssetf関数
  • tagbodyのtag
  • blocksymbol