with-exception-handler の仕様を正しく理解しよう その5

dynamic-wind へ。

Together, dynamic-wind and call/cc manage a list of winders.
A winder is a pair of in and out thunks established by a call to dynamic-wind.

dynamic-wind と call/cc は協調して winders というリストを管理する。
winder は dynamic-wind の呼び出しで指定される in と out の pairである。winder = (in . out)

Whenever dynamic-wind is invoked, the in thunk is invoked,
a new winder containing the in and out thunks is placed on the winders list,
the body thunk is invoked, the winder is removed from the winders list, 
and the out thunk is invoked.

dynamic-wind が呼び出されると

  • in が呼び出される
  • (in . out) が winders のリストに置かれる
  • body が呼び出される
  • (in . out) が winders のリストから除かれる
  • out が呼び出される

ということが起きる。

This ordering ensures that the winder is on the winders list
only when control has passed through in and not yet entered out.

winders リストの中に winder が存在するのは、この順序により in を通過し、out にまだ突入していないときだけであることが保証される。

Whenever a continuation is obtained, the winders list is saved,
and whenever the continuation is invoked,
the saved winders list is reinstated.

継続がキャプチャされれば、winders リストは保存され、継続が呼び出されれば winders リストは復帰する。

During reinstatement, the out thunk of each winder
on the current winders list that is not
also on the saved winders list is invoked,
followed by the in thunk of each winder
on the saved winders list that is not also on the current winders list.

継続の復旧の間、現在の winders リストには存在するが、保存された winders リストにはない winder の out がそれぞれ呼び出される。
そして続いて、保存された winders リストにはあるが現在の winders リストにはない in がそれぞれ呼び出される。

The test (not (eq? save winders)) performed in call/cc is not strictly necessary
but makes invoking a continuation less costly
whenever the saved winders list is the same as the current winders list. 

call/cc で実行されている(not (eq? save winders)) の判定は厳密には必ずしも必要な訳ではない。
保存されたものと現在の winders のリストが全く同じ場合に継続の呼び出しのコストを抑える効果がある。

検証

いまいちイメージがわかない。いくつかのケースで考えてみれば分かるかもしれない。


call/cc でキャプチャされない場合。これは winders に (in . out) が追加され削除されるだけなのでうまくいくのは理解できる。


dynamic-wind が1度だけ呼ばれて、さらにその中で call/cc が継続をキャプチャする。そしてその継続に戻ってくる場合。
コードで書くと

(let ([captured '()])
  (dynamic-wind
      (lambda () (print "before"))
      (lambda ()
        (if (call/cc
             (lambda (cont)
               (set! captured cont)))
            '()
            (set! captured #f)))
      (lambda () (print "after")))
  (if captured
      (captured #f)
      (print "done")))
; => before->after->before->after->done

確かに継続の復帰の際に before にあたる部分が現在のリストにはなくキャプチャされたリストにはあるので before が呼ばれる。


次に復帰をしようとする起点でも dynamic-wind があったらどうだろうか。

(let ([captured '()])
  (dynamic-wind
      (lambda () (print "before1"))
      (lambda ()
        (print "thunk1")
        (if (call/cc
             (lambda (cont)
               (set! captured cont)))
            '()
            (set! captured #f)))
      (lambda () (print "after1")))
  (dynamic-wind
      (lambda () (print "before2"))
      (lambda ()
        (print "thunk2")
        (if captured
            (captured #f)
            (print "done")))
      (lambda () (print "after2"))))
;=> before1->thunk1->after1->before2->thunk2->after2->before1->after1->before2->thunk2->done->after2

(captured #f) のタイミングで

  • 現在の winders リスト: (before2 . after2)
  • キャプチャされた winders リスト : (before1 . after1)

がある。

なのでルールに従えば after2 がよばれ before1 が呼ばれる。おおおお。すばらしい。


では入れ子はどうだろうか?

(let ([captured '()])
  (dynamic-wind
      (lambda () (print "before1"))
      (lambda ()
        (print "thunk1")
        (dynamic-wind
            (lambda () (print "before1-1"))
            (lambda ()
              (print "thunk1-1")
              (if (call/cc
                   (lambda (cont)
                     (set! captured cont)))
                  '()
                  (set! captured #f)))
            (lambda () (print "after1-1"))))
      (lambda () (print "after1")))
  (dynamic-wind
      (lambda () (print "before2"))
      (lambda ()
        (print "thunk2")
        (if captured
            (captured #f)
            (print "done")))
      (lambda () (print "after2"))))
;; => before1->thunk1->before1-1->thunk1-1->after1-1->after1
;;    ->before2->thunk2->after2->before1->before1-1->after1-1
;;     ->after1->before2->thunk2->done->after2

(captured #f) のタイミングで

  • 現在の winders リスト: (before2 . after2)
  • キャプチャされた winders リスト : (before1 . after1) (before1-1 . after1-1)

がある。

(captured #f) 再入するときには after2 -> before1 -> before1-1が呼ばれると。
ふむむ。良さげだ。