コルーチン3(コルーチンから脱出)

プログラミングGaucheのpp298-303。前回のコルーチンによるREPLは(exit)を与えてGaucheインタプリタを終わらせるまで無限に繰り返していたけど、今回はコルーチンから脱出してGaucheのREPLのトップレベルに戻ってくるように修正するらしい。

そのために定義を修正する必要があるのは構文define-coroutine。

;; コルーチンから抜けて処理系のREPLのトップレベルに
;; 戻ってこれるように修正したdefine-coroutine
(define-syntax define-coroutine
  (syntax-rules ()
    ;; 前回のdefine-coroutineと同じ部分
    ((_ (routine yield) body ...)
     (define (routine)
       (call/cc (lambda (return)
                  (define (yield)
                    (call/cc (lambda (cont)
                               (enqueue! *tasks* cont)
                               (return))))
                  body ...))
       ((dequeue! *tasks*))))
    ;; 新たに追加したブロック
    ((_ (routine yield exit) body ...)
     (define (routine)
       (call/cc (lambda (escape)  ; 処理系のREPLトップレベルへ
                  (call/cc (lambda (return) ; 次のコルーチンへ
                             (define (yield)
                               (call/cc (lambda (cont)
                                          (enqueue! *tasks* cont)
                                          (return))))
                             (define (exit)
                               (call/cc (lambda (cont)
                                          (enqueue! *tasks* cont)
                                          (escape))))
                             body ...))
                  ((dequeue! *tasks*))))))))

コメントの「次のコルーチンへ」の行で捕捉した継続returnは、トップレベルへ到達するまでの間に( (dequeue! *tasks*) )という仕事、つまり次のコルーチンを取り出して実行するという仕事があるのに対し、コメント「処理系のREPLトップレベルへ」の行で捕捉した継続escapeは何もせずにトップレベルへ直行するので、コルーチンの無限ループから抜けられるというわけね。
しかし、このマクロdefine-coroutine、

  • ローカルな手続きyieldとexitの定義がとても似ている
  • 後半部分はローカル手続きexitが挿入され、かつ、継続escapeを捕捉するためのcall/ccの皮が一枚被さっただけ

なので、もうちょっとなんとかならんのかな...。


で、上記のdefine-coroutineを使って、処理系のREPLトップレベルへ脱出するための手続きmanager定義すると

(define-coroutine (manager yield exit)
  (let lp ((count 1))
    (print #`"MANAGER(,count)")
    (exit)
    (lp (+ count 1))))

こうなる。もちろんyieldしない。yieldしないんだったら引数のyield書きたくない!だけど無理!仕方ないな。
上のmanagerでトップレベルまで抜けた後に、継続を登録したり、削除したりするための雑多な手続きも作っておくと

(define coroutine-add!
  (lambda (r) (enqueue! *tasks* r)))

(define coroutine-del!
  (lambda () (dequeue! *tasks*)))

(define coroutine-restart!
  (lambda () ((dequeue! *tasks*))))

(define coroutine-skip!
  (lambda () (coroutine-add! (coroutine-del!))))

こうなる、と。
これで、前回までのようにあらかじめcoroutine-init!で継続を登録しておかなくても、managerがトップレベルまで連れて行ってくれるので、そこでreaderやらevaluatorやらprinterを定義して、coroutine-add!で登録して、coroutine-restart!でコルーチンを再開して...、みたいなこともできる。
実際やった例は以下。見づらいけど一応書いとく。kahua (Kahua Project) · GitHubの19章のコード(chap19.scm)の通りやってるので、見比べれば何をやっているか分かると思う。「gosh>」が処理系のプロンプト、「coroutine>」がコルーチンreaderのプロンプト。

gosh> (manager)
MANAGER(1)
#<undef>
gosh> (coroutine-restart!)
MANAGER(2)
#<undef>
gosh> (define *val* #f)
*val*
gosh> reader ; 注
gosh> (coroutine-add! reader)
((#<subr continuation> . #0=(#<closure reader>)) . #0#)
gosh> (coroutine-skip!)
((#<closure reader> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-restart!)
coroutine> 123
READ(1): 123
MANAGER(3)
#<undef>
gosh> (coroutine-restart!)
coroutine> "HELLO"
READ(2): HELLO
MANAGER(4)
#<undef>
gosh> printer ; 注
gosh> (coroutine-skip!)
((#<subr continuation> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-add! printer)
((#<subr continuation> #<subr continuation> . #0=(#<closure printer>)) . #0#)
gosh> (coroutine-skip!)
((#<subr continuation> #<closure printer> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-restart!)
coroutine> 'hello
READ(3): 'hello
PRINT(1): 'hello
MANAGER(5)
#<undef>
gosh> evaluator ; 注
gosh> (coroutine-skip!)
((#<subr continuation> #<subr continuation> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-add! evaluator)
((#<subr continuation> #<subr continuation> #<subr continuation> . #0=(#<closure evaluator>)) . #0#)
gosh> (coroutine-skip!)
((#<subr continuation> #<subr continuation> #<closure evaluator> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-skip!)
((#<subr continuation> #<closure evaluator> #<subr continuation> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-restart!)
coroutine> (+ 1 2 3)
READ(4): (+ 1 2 3)
EVAL(1): 6
PRINT(2): 6
MANAGER(6)
#<undef>
gosh> (coroutine-skip!)
((#<subr continuation> #<subr continuation> #<subr continuation> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-skip!)
((#<subr continuation> #<subr continuation> #<subr continuation> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-skip!)
((#<subr continuation> #<subr continuation> #<subr continuation> . #0=(#<subr continuation>)) . #0#)
gosh> (coroutine-del!)
#<subr continuation>
gosh> (coroutine-restart!)
coroutine> (* 7 11 13)
READ(5): (* 7 11 13)
EVAL(2): 1001
PRINT(3): 1001
coroutine> (print "OK")
READ(6): (print OK)
OK
EVAL(3): #<undef>
PRINT(4): #<undef>
coroutine> (read)
(foo bar baz)
READ(7): (read)
EVAL(4): (foo bar baz)
PRINT(5): (foo bar baz)
coroutine> (define x 123)
READ(8): (define x 123)
EVAL(5): x
PRINT(6): x
coroutine> x
READ(9): x
EVAL(6): 123
PRINT(7): 123
coroutine> (exit)
READ(10): (exit)

Process scheme finished

コメント「注」の箇所は、emacsの別のバッファに書いたそれぞれの定義をC-xC-eで実行してる箇所(p26参照)。対話環境でタイポしてしまうといろいろ面倒なので。この実行結果からも分かるように、今現在*tasks*の先頭が何なのかをいちいち意識しつつ、coroutine-skip!でキューの順序を制御する必要があるので、少し面倒くさかった。


19章やっと読み終わった。とりあえず、プログラミングGaucheはこれでいったん中断して、今度はSICPを読もうと思う。SICPで挫折しない程度にSchemeを勉強しておくというのが元々の動機だったし。実用的なプログラミングのための解説(文字列処理とか、オブジェクト指向とか)はほとんど読み飛ばしたので、いつか気が向いたら戻ってこよう。
あと、valvallowさんとこのブログ読んでると、The Seasoned Schemerも気になる。The Little Schemerと同時に買って、絶賛積読中。


ということで、プログラミングGauche良い本だった。

プログラミングGauche

プログラミングGauche