クロージャについて: 一旦、簡単にまとめてみます。

これまで考察してきたクロージャの件ですが、まだまだ頭の中では発散しているのですけど、一旦、簡単にまとめておきたいと思います。
ただ書き散らかしているだけでは意味ない (いや、意味はあると思ってるんですけど。自己矛盾だ。) ですからね。

Rui さん、shiro さんのコメントに助けられて、今では、

  1. 手続きをファーストクラスオブジェクトとして扱える。
  2. 静的スコープを持つ。
  3. 無限のエクステントが保証されている。

の三つの基盤が揃うことで、クロージャが実現されていると考えるに至りました。
Scheme 以外ではどうか判りませんが、こと Scheme に於いては、上の仕様に立脚する形でクロージャが実現されていると考えて差し支えないと思っています。

ただ、言語設計の推移としては、shiro さんから頂いたコメントで示唆されている様に、動的スコープに於ける環境問題 (FUNARG problem) が先にあり、その解決策として function特殊形式や、Scheme が選択した定義時からの環境の閉じ込めがあり、その帰結として Scheme では静的スコープが導入されるに至ったということの様です。
つまり、クロージャ (と言って良いと思うのですが) を実現するために静的スコープが導入されたということになりますか。

ということで、私が最初に書いた、「クロージャは、(上に挙げた様な) 幾つかの言語仕様によって副次的に利用可能となったものではないか」という仮説 (?) については、否定されることとなりました。
ただ、どちらが先か、という問題は抜きにして、クロージャを形作る土台となっているという意味では、考えていた通りだと思っています。

また、ここで言うクロージャとは行かずとも、有効に活用可能な形態もあると思っています。以前に言及した `ブロック' に相当するものですね。
これについては、また別のエントリにて。

クロージャとブロック (前編)

上のエントリでは、これまで考察してきたクロージャについての簡単なまとめを行ないました。そのエントリで最後に書いたブロックにも着目しながら、これまで考察してきたクロージャについて、コードを挙げて検証してみようと思います。

さて、上のエントリでは、クロージャの基盤となっている三つの仕様を挙げています。
しかし、これらのうちの幾つかが欠ける場合でも、Scheme で言うところのクロージャではないかもしれませんが、有効に利用可能な手続きを活用することは可能であると、私は考えています。
これは、上で挙げた三つの仕様が揃わない言語環境に於いて、クロージャの様な仕組みを活用することが可能か、という視点に置き換えることができます。例えば、Emacs LispJava などでクロージャの様なコードを書けるか否か、ということですね。
;; 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は「全ての環境は無限エクステント」の定義を導入することで「何でもクロージャ」で統一的に扱えるシンプルさの方を選択したわけです。

という指摘を頂いています。
これを以って、今の私は、概念的には特に区別しない、しかし、実装面の詳細などを考えるときには区別する局面もあり、と思う様になっています。そのため、このエントリではクロージャとブロックを区別し、対比してみました。

クロージャとブロック (後編)

前編では、クロージャと、私なりにクロージャと区別している `ブロック' についての比較を、高階関数にブロックをそのまま渡す様なコードについて試みました。そこではクロージャとブロックに明確な差異はありませんでした。

しかし、手続きを無名関数として定義し、それにシンボルを束縛して利用する様な形態になると様相が変わってきます。名前を付けるからではなく、定義と評価 (実行) の字句的な位置が変わってくる (可能性がある) からです。

(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 の中では `環境' という概念が際立っていると感じます。そしてその環境という概念が最も明確なものが `継続' ですよね。
クロージャに関する考察を続けるうち、やはり `継続' についても更に理解を深める必要性を感じました。
ということで、今度は継続についてちょっと考えてみたいと思っています。

`継続' も、クロージャと同様、自分では何となく判っているつもりでいて、しかし実際には明確な理解が乏しく……というものだと感じています。ここでもう一踏ん張りして理解を深めてみたいところです。

また、近頃流行りの Web 公開学習ではないですが、この様に自分の意見や思考過程を公開して反応を頂くことで、自分の考えの妥当性を判断したり、ヒントを頂いて先に進むことができたりと、とても有効な場にできてきているという感触を得ました。
これも私のエントリに反応して頂けた Rui さん、shiro さんのおかげです。改めて感謝させて頂くと共に、今後も宜しくお願いします、と図々しくも更に先回りしてお願いさせて頂いときます。;-D