クロージャとブロック (前編)
上のエントリでは、これまで考察してきたクロージャについての簡単なまとめを行ないました。そのエントリで最後に書いたブロックにも着目しながら、これまで考察してきたクロージャについて、コードを挙げて検証してみようと思います。
さて、上のエントリでは、クロージャの基盤となっている三つの仕様を挙げています。
しかし、これらのうちの幾つかが欠ける場合でも、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は「全ての環境は無限エクステント」の定義を導入することで「何でもクロージャ」で統一的に扱えるシンプルさの方を選択したわけです。
という指摘を頂いています。
これを以って、今の私は、概念的には特に区別しない、しかし、実装面の詳細などを考えるときには区別する局面もあり、と思う様になっています。そのため、このエントリではクロージャとブロックを区別し、対比してみました。