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

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

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

(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 へのアクセスを可能にしてみると、この自由変数の存続期間内に収まってしまうため、正しく状態が保持される結果となってしまいます。

と言う訳で、エクステントの違いについての説明は、私には無理な様です。
多分、動的スコープな言語では無理なのではないでしょうか。(勝手に結論付けてしまいましたが)
静的スコープで無限エクステント*でない*実装があれば説明できるのではないか、と思うのですが。

とここまで書いて、結局何が判ったかと言いますと、

  • コードブロックをそのまま高階関数に渡す様なコードの取り回しに於いては、クロージャが使えないと言われる言語環境でも、同じ様に利用することが可能な場合がある。
  • 但し、それはあくまで、静的スコープと動的スコープ、無限エクステントと有限エクステントといった枠組みの違いが表面化しないから可能だというだけのことである。

ということになりましょうか。
ごめんなさい、自信はありません。
識者の方々の添削を希望…… (何とかならんのか、この他力本願振り……)