さかもとのブログ

つらつらと

継続:コルーチン(1)

SICPと平行して,プログラミングGaucheに取り組む.
今回は19.8のコルーチン.SICP前にこれに取り組んだときは手も足も出なかった...
今回は理解できた気がする.

コルーチン機構を作り出すマクロは以下のもの.

(use util.queue)

(define *task* (make-queue))

(define-syntax define-coroutine
  (syntax-rules ()
    [(_ (routine yield) body ...)
     (define (routine)                                        ;(1)
       (call/cc (lambda (return)                          ;(2)
                  (define (yield)                               ;(3)
                    (call/cc (lambda (cont)                ;(4)
                               (enqueue! *task* cont)
                               (return))))
                  body ...))
       ((dequeue! *task*)))]))

(define (coroutine-init! . rs)
  (set! *task* (make-queue))
  (for-each (lambda (r)
              (enqueue! *task* r))
            rs))

coroutine-init!はとりあえず置いておいて, define-coroutineのすごーく簡単な解説

  1. "routine"に相当する名前の関数を定義する(1)
  2. "routine"関数内で,"yield"に相当する名前の関数を定義する(3)
  3. "yield"を定義し終えると,bodyの評価に入る
  4. bodyの評価を終えると(これは実際には起こらないが), (dequeue! *task*)を評価する

例題1

プログラム例は

(define-coroutine (three yield)
  (let lp ()
    (print 'one)
    (yield)
    (print 'two)
    (yield)
    (print 'three)
    (yield)
    (lp)))
(three)

であって, 実行すると,

one
two
three
・
・

となり無限ループになる.
なぜこうなるのかを説明する.
プログラム例の(let lp () ...)がdefine-coroutineのbody...になる.
重要なのは, (let lp ()...)が(yield)を呼び出すこと.このyieldは,当然(3)の関数を指している.
これを実行するとどうなるかというと, 

  1. (yield)を呼び出したところを"cont"とする
  2. "cont"を*task*に入れ,(return)を呼び出す
  3. "return"が呼び出されると, "return"の継続地点に戻る
  4. (dequeue!...)によって,*task*に入っているもの取り出し,評価する
    • ここでは,1の"cont"が評価される
  5. 評価された結果,"cont"は継続なので,継続地点の先が評価される
  6. 再び(yield)が呼び出され,1〜4が行われる

今回のサンプルプログラムでは,(yield)を実行することで,自分への継続地点(cont)を*task*に入れるんだけど,(return)によって,dequeueされ,すぐに継続地点に戻ってしまう.
簡単に言えば,yieldでenqueueして,(return)でdequeueをする.

例題2

つぎのプログラム例は

(define-coroutine (Go next)
  (let loop ()
    (print 'ichi1)
    (next)
    (print 'ni2)
    (next)
    (print 'san3)
    (next)
    (print 'yon4)
    (next)
    (print 'Go!5)
    (next)
    (loop)))
(coroutine-init! Go)

ここで初めて,coroutine-init!が使われる.
coroutine-init!は何をする関数かというと,引数で渡された関数を*task*にあらかじめ関数を入れておくものである.
あらかじめ関数を入れておいて(three)を実行するとどうなるか...以下がこのプログラムの実行

gosh>(three)
gosh> one
ichi1
two
ni2
three
san3
one
yon4
two
Go!5
three
ichi1
 :
 :

無限ループとなる.さきほどはone,two,threeだけだったが,coroutine-init!をしたおかげで,結果はこうなる.
なぜこうなるかの説明をする.基本的には例題1と変わらないが,今回はすでに*task*にある関数が入っていることと*task*がキュー(先入れ先だし)であることが重要

  1. "three"において,(yield)を呼び出したところを"cont"とする
  2. "cont"を*task*に入れ,(return)を呼び出す
  3. "return"が呼び出されると, "return"の継続地点に戻る
  4. (dequeue!...)によって,*task*に入っているもの取り出し,評価する
    • 1回目は,*task*に入っているGoが評価される
  5. "Go"の評価に入る
  6. (next)を呼び出したところを"cont"とする
  7. "cont"を*task*に入れ,(return)を呼び出す
  8. "return"が呼び出されると, "return"の継続地点に戻る
  9. (dequeue!...)によって,*task*に入っているもの取り出し,評価する
    • ここでは,*task*の先頭に入っている, "three"での(yield)による継続が評価される
  10. 再び(yield)が呼び出され,"cont"が*task*に追加され,*task*の先頭に入っている"cont"が呼び出される
    • ここでは,*task*の先頭に入っている, "Go"での(next)による継続が評価される

こんな感じ.わかりにくい説明になってしまった....
つまり,(yield) -> "Go"の評価 -> (next) -> {*task*の先頭にある"three"の継続 -> (yield) -> *task*の先頭にある"Go"の継続 -> (next) -> …}
{}の中が無限ループになる.(もちろんletのなかで(lp)を呼び出しているからだけど)

継続ってすごいなぁ...