ふう、疲れました。
ここまで色々とクロージャについての考察を行なって来て、識者の方々にもコメントを頂けましたし、それらを踏まえて一旦は整理しておかねば、と一気に書いてしまいましたが……
「簡単にまとめてみました」とか書いてますがとんでもない、私としてはもうとんでもない大作となってしまいました。疲れました。もう今日は何もできないでしょう。
ここまでの過程で、やはり Scheme の中では `環境' という概念が際立っていると感じます。そしてその環境という概念が最も明確なものが `継続' ですよね。
クロージャに関する考察を続けるうち、やはり `継続' についても更に理解を深める必要性を感じました。
ということで、今度は継続についてちょっと考えてみたいと思っています。
`継続' も、クロージャと同様、自分では何となく判っているつもりでいて、しかし実際には明確な理解が乏しく……というものだと感じています。ここでもう一踏ん張りして理解を深めてみたいところです。
また、近頃流行りの Web 公開学習ではないですが、この様に自分の意見や思考過程を公開して反応を頂くことで、自分の考えの妥当性を判断したり、ヒントを頂いて先に進むことができたりと、とても有効な場にできてきているという感触を得ました。
これも私のエントリに反応して頂けた Rui さん、shiro さんのおかげです。改めて感謝させて頂くと共に、今後も宜しくお願いします、と図々しくも更に先回りしてお願いさせて頂いときます。;-D
クロージャとブロック (後編)
前編では、クロージャと、私なりにクロージャと区別している `ブロック' についての比較を、高階関数にブロックをそのまま渡す様なコードについて試みました。そこではクロージャとブロックに明確な差異はありませんでした。
しかし、手続きを無名関数として定義し、それにシンボルを束縛して利用する様な形態になると様相が変わってきます。名前を付けるからではなく、定義と評価 (実行) の字句的な位置が変わってくる (可能性がある) からです。
(let ((y 5) (ar #f) (f #f)) (set! ar '(1 2 3 4 5)) (set! f (let ((y 2)) (lambda (x) (* x (set! y (+ y 1)))))) (let () (map f ar))) => (3 8 15 24 35)
ここでは、最上位のブロックで f というシンボルを束縛しておき、更にコードブロックに束縛しています。通常は let でそのまま束縛するでしょうが、ここでは説明を判り易くするために、二段階の束縛にしています。
これまでの例示と異なり、このコードでは、コードブロックの定義と評価 (実行) のスコープが異なり、レキシカルスコープとダイナミックスコープの相違が発生することになります。
また、この違いを明確にするため、自由変数 `y' も、二段階に束縛されています。(Elisp だと error になってしまうので)
このケースを Elisp で表現してみると、
(let ((y 5) (ar nil) (f nil)) (setq ar '(1 2 3 4 5)) (setq f (let ((y 2)) (lambda (x) (* x (setq y (+ y 1)))))) (let () (mapcar f ar))) => (6 14 24 36 50)
と、遂に Scheme (Gauche) と Emacs Lisp の間で結果が異なることになりました。
Elisp の場合では、f を定義した時点で束縛されている y ではなく、評価の時点で参照可能な、最上位のブロックでの y の束縛が適用されます。
これがレキシカルスコープとダイナミックスコープの違いということになります。
また、この様な予め手続きを用意しておく様な場合、手続きを返す手続き (これもまた高階関数ですね) を用意するのが一般的ではないでしょうか。すると、
(let ((y 5) (ar #f)) (define (make-f y) (lambda (x) (* x (set! y (+ y 1))))) (set! ar '(1 2 3 4 5)) (let () (define f (make-f 2)) (map f ar))) => (3 8 15 24 35)
の様になります。これを Elisp に置き換えた場合も、
(let ((y 5) (ar nil)) (defun make-f (y) (function (lambda (x) (* x (setq y (+ y 1)))))) (setq ar '(1 2 3 4 5)) (let () (fset 'f (make-f 2)) (mapcar 'f ar))) => (6 14 24 36 50)
と、やはり結果は異なる様です。やってることは一つ前に挙げたコードと同じですからね。
ここで、表面的には判別不能ですが、これらのケースに於ける Elisp の場合、f の評価 (実行) 時点では、f を定義、または生成したときの束縛である 「2 に束縛された y」 は存在すらしていません。存続期間切れですね。(GC によって回収されていなければ空間上に残ってはいるでしょうが、論理的には存在しないのと同じですよね)
このエクステントの違いについても何とか説明を試みたんですが…… 例えば以下の様に。
(let ((a1 #f) (a2 #f) (f #f)) (set! a1 '(1 2 3)) (set! a2 '(4 5)) (let () (set! f (let ((y 2)) (lambda (x) (* x (set! y (+ y 1)))))) (cons (map f a1) (map f a2)))) => ((3 8 15) 24 35)
これは、二回に分けた map に、それぞれ f を適用することで、最初の適用時にインクリメントした自由変数 y の内容がそのまま保持され、二回目の map で f を適用するときに保持されている値が引き継がれて評価されていることを示すコードです。
フレームを跨がって状態が保持されており、継続して利用可能であるということですね。
これを Elisp で実装してみると、
(let (a1 a2 y f) (setq a1 '(1 2 3)) (setq a2 '(4 5)) (setq f (let ((y 2)) (lambda (x) (* x (setq y (+ y 1)))))) (let () (cons (mapcar f a1) (mapcar f a2)))) => Wrong type argument: number-char-or-marker-p, nil
となって、自由変数 y が nil なために error となります。
しかし、これはスコープによるものと解釈する方が妥当でしょう。
それならば、と、
(let (a1 a2 (y 5) f) (setq a1 '(1 2 3)) (setq a2 '(4 5)) (setq f (let ((y 2)) (lambda (x) (* x (setq y (+ y 1)))))) (let () (cons (mapcar f a1) (mapcar f a2)))) => ((6 14 24) 36 50)
として最上位のブロックで束縛しておき、評価時に y へのアクセスを可能にしてみると、この自由変数の存続期間内に収まってしまうため、正しく状態が保持される結果となってしまいます。
と言う訳で、エクステントの違いについての説明は、私には無理な様です。
多分、動的スコープな言語では無理なのではないでしょうか。(勝手に結論付けてしまいましたが)
静的スコープで無限エクステント*でない*実装があれば説明できるのではないか、と思うのですが。
とここまで書いて、結局何が判ったかと言いますと、
- コードブロックをそのまま高階関数に渡す様なコードの取り回しに於いては、クロージャが使えないと言われる言語環境でも、同じ様に利用することが可能な場合がある。
- 但し、それはあくまで、静的スコープと動的スコープ、無限エクステントと有限エクステントといった枠組みの違いが表面化しないから可能だというだけのことである。
ということになりましょうか。
ごめんなさい、自信はありません。
識者の方々の添削を希望…… (何とかならんのか、この他力本願振り……)
クロージャとブロック (前編)
上のエントリでは、これまで考察してきたクロージャについての簡単なまとめを行ないました。そのエントリで最後に書いたブロックにも着目しながら、これまで考察してきたクロージャについて、コードを挙げて検証してみようと思います。
さて、上のエントリでは、クロージャの基盤となっている三つの仕様を挙げています。
しかし、これらのうちの幾つかが欠ける場合でも、Scheme で言うところのクロージャではないかもしれませんが、有効に利用可能な手続きを活用することは可能であると、私は考えています。
これは、上で挙げた三つの仕様が揃わない言語環境に於いて、クロージャの様な仕組みを活用することが可能か、という視点に置き換えることができます。例えば、Emacs Lisp や Java などでクロージャの様なコードを書けるか否か、ということですね。
;; Java にもクロージャを導入可能に (?) といった動きもあるみたいですが。(すみません、英文を読み違えている可能性があるので、そういう話でなかったらごめんなさい)
さて、その様な活用方法について、(これまでコードを全然挙げられてなかったので) 少しコードを挙げての説明を試みてみようかと思います。
;; コードを挙げると途端にエントリが長くなってしまうので、前後編に分けます。(余り意味はないかもしれませんが)
クロージャとブロックを対比するため、Scheme (Gauche) のコードと Emacs Lisp のコードを挙げて行きます。他の言語の場合はまた色々とあるでしょうが、それぞれ Lisp の方言ということで、対比し易いと思いますので。
また、挙げるコードはいたって単純なものだけにしています。なので、`有用な' と書いていても余りそう思っては頂けないかもしれませんが、一応、検証を目的としているので、明かにしたい部分を明確にするためにもシンプルなコードを、ということで。
先ず、手続きがファーストクラスオブジェクトでありさえすれば良いという形態。これは、先日のエントリで、私自身はクロージャというより `ブロック' と考えていると書いていた形態の最も簡素な一形態になります。
(let ((ar #f)) (set! ar '(1 2 3 4 5)) (map (lambda (x) (* x 2)) ar)) => (2 4 6 8 10)
手続きとリストを引数として受け取る高階手続きである map を利用して、リストの各要素に手続きを適用する形態です。
上に書いたコードは、1 から 5 までの数値の集合の各要素に、定数である 2 を乗じ、その結果の値のリストを返すだけのコードです。
これは、クロージャを持たない (動的スコープ、有限エクステントなため) とされる Emacs Lisp でも同じ様に書くことができます。
(let ((ar nil)) (setq ar '(1 2 3 4 5)) (let () (mapcar (lambda (x) (* x 2)) ar))) => (2 4 6 8 10)
Emacs Lisp で動作させるために、set! を setq に、map を mapcar に、nil を #f に置き換えています。
このコードは、
- 高階関数に渡しているコードブロック (lambda式) の中に自由変数が存在しないため、レキシカルスコープでもダイナミックスコープでも相違が無い。
- 同じく自由変数が存在しないため、変数の存続期間 (エクステント) に影響を受けない。
コードです。ですから、Scheme (Gauche) でも Elisp でも同じ結果が得られます。
続いて、定数としていた 2 を (lambda式から見た) 自由変数にしてみます。
(let ((ar #f)) (set! ar '(1 2 3 4 5)) (let ((y 2)) (map (lambda (x) (* x y)) ar))) => (2 4 6 8 10)
このコードを Elisp で書くと以下になります。
(let ((ar #f)) (set! ar '(1 2 3 4 5)) (let ((y 2)) (map (lambda (x) (* x y)) ar))) => (2 4 6 8 10)
定数を利用していたコードと変わりなく利用可能です。結果も同じですね。
このコードでは、
- 高階関数に渡しているコードブロック (lambda式) の中に自由変数が存在しているが、コードブロックの定義が評価 (実行) 時に行なわれているため、レキシカルスコープでもダイナミックスコープでも相違が無い。
- この自由変数の存続期間 (エクステント) は、実行時のフレーム内に収まっているため、エクステント (が無限か/有限か) に影響を受けない。
ことになり、やはり Scheme (Gauche) でも Elisp でも同じ結果が得られます。
さて、ここで更に、この自由変数を更新する処理を持ち込み、状態の保持を意識しなければならないコードにしてみます。
(let ((ar #f)) (set! ar '(1 2 3 4 5)) (let ((y 2)) (map (lambda (x) (* x (set! y (+ y 1)))) ar))) => (3 8 15 24 35)
リストの各要素に lambda 式を適用する度に、自由変数を y をインクリメントしています。map によるコレクションへの lambda の適用を繰り返す間、自由変数 y の値の変化を保持しています。
これにより、これまでのコードとは得られる結果が変わりました。
これを Elisp で実装すると、
(let ((ar)) (setq ar '(1 2 3 4 5)) (let ((y 2)) (mapcar (lambda (x) (* x (setq y (+ y 1)))) ar))) => (3 8 15 24 35)
となり、ここでも Scheme (Gauche) と結果は同じです。このコードは、
- 高階関数に渡しているコードブロック (lambda式) の中に自由変数が存在しており、その内容に破壊的な操作 (単純な更新ですが) が加えられているが、コードブロックの定義が評価 (実行) 時に行なわれているため、レキシカルスコープでもダイナミックスコープでも相違が無い。
- この自由変数の存続期間 (エクステント) は、実行時のフレーム内に収まっているため、エクステント (が無限か/有限か) に影響を受けない。
一応、状態の変化が発生している訳ですが、一つ前の例と同じ理由で、大勢に影響はなく、状態の変化は保持されています。
こうしてみると、高階関数にブロックをそのまま渡している場合には、クロージャが存在しないと言われる言語環境でも、ブロックによってクロージャと同様の事ができ、特に問題無く活用可能と言えそうです。
この辺りまでが、これまでに私が書いていた、`十分に有用なブロック' の活用です。
ここで、私はクロージャとブロックという書き分けをしていますが、実はこの書き分けに厳密な定義はありません。
いえ、正当な定義というものがあるのかもしれませんが、私が勝手にクロージャとブロックと使い分けているだけです。判り難かったらすみません。と同時に、どなたか区別なんか無いとか、正当な区別があってそれは何によるものだ、とか、ご存じでしたらお知らせ下さい。(またも他力本願ですみません……)
因みに、以前のエントリで私は、
個人的には、この様なケースではクロージャではなく単なるブロックと考えているのですが、特に区別する必要は無いのでしょうね。
と書いていて、感覚的には区別して考えてはいるけれど、特に区別しなければならないことはないのでしょう、という主旨のことを書いています。これに対して、shiro さんより、
いわゆる下向きfunarg、つまりクロージャの生存期間が親環境の関数の実行期間の中に収まる場合は、特別に状態の保持に気を使わなくても良いですね (環境をスタックに置いたままにしておける、とか)。上向きfunarg (作成したクロージャを返すなど、親環境の関数の実行が終了しても環境を残しておく必要がある場合) とは実装戦略が異なってくるので、言語としてそれらを区別する方針もありでしょう。Schemeは「全ての環境は無限エクステント」の定義を導入することで「何でもクロージャ」で統一的に扱えるシンプルさの方を選択したわけです。
という指摘を頂いています。
これを以って、今の私は、概念的には特に区別しない、しかし、実装面の詳細などを考えるときには区別する局面もあり、と思う様になっています。そのため、このエントリではクロージャとブロックを区別し、対比してみました。
クロージャについて: 一旦、簡単にまとめてみます。
これまで考察してきたクロージャの件ですが、まだまだ頭の中では発散しているのですけど、一旦、簡単にまとめておきたいと思います。
ただ書き散らかしているだけでは意味ない (いや、意味はあると思ってるんですけど。自己矛盾だ。) ですからね。
Rui さん、shiro さんのコメントに助けられて、今では、
- 手続きをファーストクラスオブジェクトとして扱える。
- 静的スコープを持つ。
- 無限のエクステントが保証されている。
の三つの基盤が揃うことで、クロージャが実現されていると考えるに至りました。
Scheme 以外ではどうか判りませんが、こと Scheme に於いては、上の仕様に立脚する形でクロージャが実現されていると考えて差し支えないと思っています。
ただ、言語設計の推移としては、shiro さんから頂いたコメントで示唆されている様に、動的スコープに於ける環境問題 (FUNARG problem) が先にあり、その解決策として function特殊形式や、Scheme が選択した定義時からの環境の閉じ込めがあり、その帰結として Scheme では静的スコープが導入されるに至ったということの様です。
つまり、クロージャ (と言って良いと思うのですが) を実現するために静的スコープが導入されたということになりますか。
ということで、私が最初に書いた、「クロージャは、(上に挙げた様な) 幾つかの言語仕様によって副次的に利用可能となったものではないか」という仮説 (?) については、否定されることとなりました。
ただ、どちらが先か、という問題は抜きにして、クロージャを形作る土台となっているという意味では、考えていた通りだと思っています。
また、ここで言うクロージャとは行かずとも、有効に活用可能な形態もあると思っています。以前に言及した `ブロック' に相当するものですね。
これについては、また別のエントリにて。
クロージャについて: コメントありがとうございます。
勝手にプレッシャーを感じながら、これまた勝手に識者の方々にコメントをお願いしていたところ、Rui さんに続いて shiro さんからもコメントを頂きました。
私の発散した記事を読んで頂いた上で、非常に示唆に富んだコメントを頂きまして、本当にありがとうございます。勉強になります。
shiro さんから頂いたコメントは、おおまかに、
という具合に分けられるかと思います。
(1)、(2) については、先の私のエントリの記述内容を、強力に後押しして頂けた様に感じました。(違っていたらご指摘下さい)
また、ご紹介頂いたポインタ先の論文を、大変興味深く読ませて頂きました。この論文は、ダイナミックスコープである Lisp 実装に於いて、`環境' を扱う場合に認識すべき問題を考察した論文になっており、これも私の発散した思考と重なる部分の多いものでした。
私の考えていた事柄が、大筋では外れていなさそうであることが確認でき、一歩進めた気がするとともに、何だかとてもホッとした気分です。
クロージャと環境の関係は、1960年代後半にいろいろ考察されて理解されるようになったようです。
そして、ご紹介頂いた論文が 1970年に書かれたものということで、現在の状況は様変わりしているのでしょうし、ようやく 35年前に追い付いただけなんですけれど……
(3) はクロージャと継続で着目しているリソースが異なることが説明されていると理解しました。
「束縛環境」が環境フレーム、「アクティベーションレコード」(活動レコードとか活性レコードとか言われるものですよね) がコンティニュエーション (継続) フレームを指していると認識しています。
実際、Gauche では、スタックフレームを、環境フレーム、引数フレーム (束縛変数のフレームのことだと認識しています)、継続 (コンティニュエーション) フレームの三つに分けていて、状況に応じて環境フレームだけヒープに移動 (クロージャの場合) したり、継続フレームだけヒープに移動 (継続の場合) したりしている様ですね。
そして、これらのフレームは線形のリストではなく、ツリー構造として管理されることになるというのが、コメントの主旨かと。
このツリーとして管理する必要があることについては、ご紹介頂いた論文の中でも触れられていました。
また、括弧書きでクロージャとオブジェクトの類似性についてもコメント頂きましたが、`Actor = Closure (mod Syntax)' というのは聞いたことがあります。検索して調べてみたところ、大意としては「オブジェクト? クロージャと変わりないよ。」ということらしいですね。
Lambda: The Ultimate Imperative がご指摘のドキュメントであることは突き止められましたが、本当に情けない話ですが、英語ドキュメントだと読めるのはいつになることやら、というか不可能かもしれません……
トラックバックありがとうございます。
何と、結城さんからトラックバックを頂いてしまいました。
メモ。
ということなんですが、これはきっと、higepon さんと同じで、
と言うことなのでしょう。
なので、既にコメント頂いている Rui さん以外の識者の方々、何卒宜しくお願い致します。;-p
クロージャについて、どんどん発散して行くなあ。
色々考えていることを整理してみたいと思ったのですが、意に反して、どんどん発散してしまいます。こんなときはまだ整理できる段階ではないということで、発散するままに書き連ねてしまうことにしてみました。
先のエントリで少し触れましたが、私は、クロージャの本質を「状態の保持」と捉えています。
この「状態」とは、Scheme 的に言えば、`環境' と言うことになりましょうか。
スコープ
そう言う意味では、自由変数へのアクセスをクロージャの定義 (生成) 時点のスコープで行なうのか、動作 (評価) 時点のスコープで行うのかという話については、枝葉でしかないのでは、と思っています。
いえ、自由変数へのアクセスがクロージャの定義 (生成) 時点のスコープで行なわれることには大きな意味があるのですが、それは「定義時点のものか、動作 (評価) 時点のものか」という視点で考えるものではなく、「一つの環境に特定できるかどうか」という視点で考えるべきものじゃないのだろうかと思うのです。
つまり、ダイナミックスコープな環境でクロージャが生成できないのは、クロージャが評価される文脈で、クロージャから見える環境が異なってしまうことに問題があるからだと思う訳です。
逆を言えば、ダイナミックスコープであっても、生成されたクロージャが存在している間、特定の環境にのみ結び付けられる仕組みがありさえすれば良いと言えるのかもしれません。
また、コードブロックの上位階層に遡って自由変数にアクセスすることについても、必ずしも必要なものではないのでは? とも思っています。そう考えると、自由変数は抜きにして、束縛変数だけで環境を構成するというのもアリかと思いました。クロージャの作り易さという面をスポイルすることにはなってしまうかもしれませんが。
オブジェクトとの類似
この、状態 (環境) の保持という面では、`継続' も同じかと思っています。しかし、継続の場合は、その目的、役割がプログラムの実行制御 (状態遷移と言った方が良いのかな) のためのものであり、クロージャとは異なってくるのでしょう。
あ、クロージャも、コールバック手続きとして使われる様なケースでは、継続と似た役割を持つことになると言えるのかもしれませんが。
そして、状態 (環境) の保持が本質であると考えると、やはりオブジェクト指向で言うところのオブジェクトやインスタンスの単純な一形態と言えるのではないかと思うのです。
インスタンスごとに状態 (環境) として変数群を保持し、それらに作用する、或いはそれらに基く処理を行なう手続きを持つ、立派なオブジェクトと言えると思います。
内部状態となる変数に手続きを保持して適用することも可能な訳ですから、インスタンスごとに手続き自体の振る舞いを変えることも可能ですね。
クロージャ自身は一つの手続きとなるため、メソッドを複数持てるオブジェクトに比べて貧弱に思われるかもしれませんが、同じ状態 (環境) に対して複数のクロージャを生成することができるので、複数のメソッドを持つオブジェクトと同等の振る舞いを持たせることも可能ですし。複数クロージャの管理には工夫が必要になるでしょうが。
所謂オブジェクト指向言語による様々なサポートを全て実現できるというつもりはありません (工夫すれば何とかなるでしょうが、その工夫のコストを掛けることが妥当でないという判断はありでしょう) が、仰々しいオブジェクト設計とは別に、ある局面でのみ簡単に独立したオブジェクトを導入したい、といったときには十分に有効かと思います。
クロージャについてもう少し考えてみました。
昨日のエントリに Rui さんからコメントを頂きました。ありがとうございます。
私もなんだか同じイメージを持ってます。静的スコープでオブジェクトの寿命が永遠(無限エクステント)というのがまず初めにあって、その結果クロージャがあるという印象。
私が漠然と感じているクロージャへの解釈を、一言で表現されている様に思いました。
ここで無限エクステントに触れられていますが、実は私も、先のエントリを書いたとき、クロージャを副次的に作り出している仕様の一つとして、無限 (実装上は有限になるのでしょうが) エクステントを挙げようとは思ったのです。
しかし、レキシカルスコープとブロックの階層化 (手続きをトップレベル以外でも定義可能とか、lambda はどこでも書けるとか) を挙げることで、必然的に無限エクステントが求められることになると考えて、そちらを優先したのでした。
そして、その考えが、
例えば、レキシカルスコープであって、トップレベル以外で手続きが定義可能 (まあ、lambda 式が書ければ良い訳ですが) なことから、定義部分から可視範囲にある自由変数へのアクセスが可能なところとか。
それを実現するために、手続き生成時点のスタックフレームを、生成された手続きが利用可能な期間中は保持しておくところとか。
に現われています。
ここで、特に「生成された手続きが利用可能な期間中は」と限定しているのは、Schemeの実装におけるスタックフレーム(Draft) に書かれている内容に影響されています。更に Gauche ではスタックフレームをヒープに移動したりもしている様ですね。
ただ、これは私の思考の流れがそういう向きを向いていただけで、無限エクステントを挙げた方がスッキリする様に思います。
この部分は、クロージャの本質である (と私が勝手に考えている) 状態の保持に密接な関係があるところですから、先ず、
- フレーム内のオブジェクトの存続期間が無限であって、フレームを参照しているクロージャが全て回収されるまでその存在が保証されている。
ことが明確になる考え方を土台とした方が筋が良いと考え直したからです。
フレームの扱いに関しては、`環境' として、`継続' やクロージャに大きく影響するところですし、もっと明確にイメージを掴んでおきたいと感じるところですね。
クロージャについて少し考えてみました。
クロージャについて色々と考えてみました。
以前にも書いたのですが、そもそも Gauche をインストールした目的の一つに、クロージャの動作をもっと理解したいという目的があったので。
で、色々と shiro さんが公開されているドキュメントを読み返したり、Gauche 上で試行錯誤しながら、これまでの私の解釈には誤りがあったと思う様になりました。
これまでの私の理解では、クロージャとは、
- クロージャは、ファーストクラスのオブジェクトである。なので、名前も付けられるし、関数の引数や戻り値としても利用できる。
- クロージャには、(そのクロージャ生成時の) 環境が閉じ込められている。
- クロージャ実行時に参照される環境は、実行時点のものではなく、クロージャ自身に閉じ込めてある環境になる。
というものでした。
これらの認識自体には誤りはないと思っている (誤りがあったらツッコミをお願いしたいです。> 識者の方々) のですが、これらが「クロージャという機構を実現するために用意されたもの」、という解釈をしていたところが何か違うと思う様になったのです。
この解釈は、特別意識していた訳ではないのですが、漠然と感じていたものです。
ひょっとすると、この感覚は、elisp でクロージャが使えず、cl パッケージを require してクロージャを使っていたりしたことが関係してるのかもしれません。つまり cl パッケージによってクロージャ機構を持ち込んだ、と感じていたのではないかと。
しかし、Scheme に触れていて、scheme に於けるクロージャは、幾つかの言語仕様による副作用の様なものだと感じる様になりました。その幾つかの言語仕様とは、
- 手続き (lambda) は第一級計算要素 (ファーストクラスオブジェクト) である。
- レキシカルスコープである。
- 手続きの定義はトップレベルだけでなく、式の内部 (ボディ部) の冒頭でも可能である。(lambda はどこにでも書けますし)
といった辺りでしょうか。
これらの仕様は、特にクロージャを導入するためのものではない筈です。
しかし、これらの仕様が組み合わさると、あら不思議、手続きはクロージャとして利用できるものになっているよ、という具合でしょうか。
例えば、レキシカルスコープであって、トップレベル以外で手続きが定義可能 (まあ、lambda 式が書ければ良い訳ですが) なことから、定義部分から可視範囲にある自由変数へのアクセスが可能なところとか。
それを実現するために、手続き生成時点のスタックフレームを、生成された手続きが利用可能な期間中は保持しておくところとか。
こんな諸々のことからクロージャっていう機構が実現されているんだなあ、と勝手に解釈しています。
何かおかしなことを言っている様であれば識者の方々からツッコんで頂けるととても有難いです。