Scheme:generatorとdoとwhile
- 0.9.3に入ったgeneratorについてはGauche:Generatorにまとめます。
- 部分継続?をつかったジェネレータについては、Scheme:部分継続:イテレータの反転を参照してください。
Scheme:初心者の質問箱から移動
doとwhileの違い?
最近のEcmaScriptにはジェネレータという機能があります。それと同じような感覚で使えるものをSchemeで実装してみようとこんなものを書きました。
(define (make-generator proc)
(define next #f)
(lambda ()
(if (not next)
(call/cc
(lambda(break)
(proc
(lambda arg
(call/cc
(lambda(cc)
(set! next cc)
(apply break arg)))))
#f))
(next))))
で、使い方としてはこんなカンジです。
(define (fib)
(make-generator
(lambda (yield)
(let loop ((i 0)
(j 1))
(yield i)
(loop j (+ j i))))))
(let ((g (fib))
(i 0))
(while (< i 10)
(display (g))
(newline)
(inc! i)))
ここ↓のサイトにインスパイアされました。
http://developer.mozilla.org/en/docs/New_in_JavaScript_1.7
で、このコードの最後でwhileループでまわしているところをdoに置き換えると停止せずに突っ走ってしまいます。 こう↓するととまらない。
(let ((g (fib)))
(do ((i 0 (+ i 1)))
((>= i 10) i)
(display (g))))
私が考える限りではdoでもwhileでも同じようなものだと思ったのですが、浅はかでしょうか?いったいどこに問題があるのでしょう。
ミソはinc!?
ミソはdo/whileというより、inc!ですかね。
(let ((g (fib))
(i 0))
(while (< i 10)
#?="mae" #?=i
(print (g))
(inc! i)
#?="ato" #?=i))
(let ((g (fib)))
(do ((i 0 (+ i 1)))
((>= i 10) i)
#?="mae" #?=i
(print (g))
#?="ato" #?=i))
こんな風にデバッグプリントを入れてみれば、
なぜ無限ループになるかは判りますよね。
では何故、そうなるのか?
(g)が呼ばれた時にnextつまりcall/ccで切り出した継続が呼ばれますけど、
call/cc式で継続が切り出された時にiはどうだったんでしょう。
こんな風にジェネレータに観測用の引数を渡せる様に変更を入れて確認してみましょう。
(define (make-generator proc)
(define next #f)
(lambda (a)
#?="MAE" #?=a
(if (not next)
(call/cc
(lambda (break)
(proc
(lambda arg
#?="ATO" #?=a
(call/cc
(lambda (cc)
(set! next cc)
(apply break arg)))))
#f))
(next))))
(define (fib a)
(make-generator
(lambda (yield)
(let loop ((i 0)
(j 1))
(yield i)
(loop j (+ j i))))))
(let ((g (fib 0))
(i 0))
(while (< i 10)
#?="mae"
(print (g #?=i))
(inc! i)
#?="ato" #?=i))
(let ((g (fib 0)))
(do ((i 0 (+ i 1)))
((>= i 10) i)
#?="mae"
(print (g #?=i))
#?="ato" #?=i))
これでどうですかね。:-)cut-sea:2006/08/09 08:55:39 PDT
本当の理由
Shiro: doとwhileの違いについては、cut-seaさんの見解のとおり、 破壊的変更の有無にあります。doは再帰で本体を呼び出しているので、一度 束縛されたiは変更されません。この違いは、本体内でiをクローズしてみると わかります。
gosh> (define procs
(let ((ps '()))
(do ((i 0 (+ i 1)))
((>= i 5) (reverse ps))
(push! ps (lambda () i)))))
procs
gosh> ((ref procs 0))
0
gosh> ((ref procs 1))
1
gosh> ((ref procs 2))
2
procsには手続きのリストが束縛されます。doを使った場合、 それぞれの手続きはその時点でのiの値をクローズしているので、返す値が違います。
gosh> (define procs
(let ((ps '()) (i 0)) ;; <-- これ
(while (< i 5)
(push! ps (lambda () i))
(inc! i))
(reverse ps)))
procs
gosh> ((ref procs 0))
5
gosh> ((ref procs 1))
5
gosh> ((ref procs 2))
5
procsには手続きのリストが束縛されます。whileを使った場合、 全ての手続きは最初のletで導入される単一のiを束縛します。 したがって、どの手続きも呼ばれると、while終了後のiの値、5を返します。
さて、これがdoとwhileの違いが出た直接の理由なんですが、 そもそも最初の質問者の動機にたちかえると、「じゃあdoだとジェネレータ使えないの? 不便じゃん」って疑問が当然出てきますわな。
最初のジェネレータがdoで使えなかった理由は、ジェネレータの実装にあります。 もういちどオリジナルの実装を見てみます。
(define (make-generator proc)
(define next #f)
(lambda ()
(if (not next)
(call/cc
(lambda (break)
(proc
(lambda arg ;; <-- これがyieldの正体
(call/cc
(lambda (cc) ;; <-- このccは「yieldの後」への継続
(set! next cc)
(apply break arg))))) ;; <-- このbreakがポイント
#f))
(next))))
(define (fib)
(make-generator
(lambda (yield) ;; <-- これがproc
(let loop ((i 0)
(j 1))
(yield i)
(loop j (+ j i))))))
fibが返すジェネレータの動作を追ってみましょう。
最初に呼ばれた時:
- ジェネレータから戻るところの継続をbreakで取得
- yield部分をクロージャにパッケージ化してprocを呼ぶ
- proc本体ではloopに突入して、まず初期値でyieldを呼ぶ。
- yieldの正体に戻って来て、「yieldの後への継続」をnextで保存した後、 breakへと制御を渡す。
- breakはジェネレータから戻るところに飛ぶ。
2度目に呼ばれた時:
- 既に、「最初のyieldの後への継続」がnextに入っている。そこに飛ぶ。
- proc本体でloopを再帰呼出
- またyieldが呼ばれる。ここで、「2度目のyieldの後への継続」をnextに保存。 breakへと制御を渡す。
あれ、このbreakはいつ捕まえたものでしょう? そう、継続breakは 「最初にジェネレータが呼ばれた時の、ジェネレータから戻るところ」に 束縛されたままなんですね。だから、「2度目に呼ばれたg」から戻るのではなく、 「1度目に呼ばれたg」の方に戻っちゃう。
whileの場合、「1度目に呼ばれたg」に戻り続けても、iが破壊的に変更されているので いずれループを抜けます。しかしdoの場合は上の例でみたように、 「1度目の本体の環境」と「2度目の本体の環境」は別物で、1度目に戻るとiの値も1度目の 値のままなわけです。それが無限ループの理由です。
正しいgeneratorは?
ジェネレータに入る度に、その時点でのジェネレータから戻る継続を 捕まえればいいんですが、うまいぐあいにふたつの継続をマネージするのは ちょっとトリッキーです。おもしろいパズルなのでやってみて下さい。
やってみよう
こうかな。cut-sea:2006/08/09 17:17:02 PDT
(define (make-generator proc)
(define next #f)
(define return #f)
(lambda ()
(call/cc
;; break = generatorがcallされた後に相当する継続
(lambda (break)
(set! return break) ;; 環境を書き換え
(if (not next)
(proc
;; yield
(lambda arg
;; cc = yieldがcallされた後に相当する継続
(call/cc
(lambda (cc)
(set! next cc) ;; 環境を書き換え
(apply return arg))))) ;; breakの後に待ち構えている継続をcall
(next)))))) ;; yieldの後に待ち構えている継続をcall
び(2007/03/15 19:13:27 PDT): よさげに見えるのですが、with-*-to-port系の手続きと併用したときの挙動が何だか変です。
gosh> (define (hoge yield)
(while #t
#?=(current-output-port)
(yield)))
hoge
gosh> (define g (make-generator hoge))
g
gosh> (with-output-to-string g)
#?="(stdin)":77:(current-output-port)
#?- #<oport (output string port) 0x58b960>
""
gosh> (with-output-to-string g)
#?="(stdin)":77:(current-output-port)
#?- #<oport (stdout) 0x99b40>
""
gosh> (with-output-to-string g)
#?="(stdin)":77:(current-output-port)
#?- #<oport (stdout) 0x99b40>
""
gosh>
何でこうなるのか、ソースを読んでも理解できず...
Shiro(2007/03/15 19:28:30 PDT): あれ、dynamic handlerのbefore thunkが 呼ばれてなさげ。Gaucheのバグかも。
び(2007/03/19 00:33:48 PDT): before thunkになっていなかったのでしてみました。これでいいのかな?
--- src/port.c 2 Mar 2007 07:39:14 -0000 1.137
+++ src/port.c 19 Mar 2007 07:31:15 -0000
@@ -1522,29 +1522,35 @@
ScmObj Scm_WithPort(ScmPort *port[], ScmObj thunk, int mask, int closep)
{
+ ScmObj initializer;
ScmObj finalizer;
- struct with_port_packet *packet;
+ struct with_port_packet *packet_before;
+ struct with_port_packet *packet_after;
int pcnt = 0;
ScmVM *vm = Scm_VM();
- packet = SCM_NEW(struct with_port_packet);
+ packet_before = SCM_NEW(struct with_port_packet);
+ packet_after = SCM_NEW(struct with_port_packet);
if (mask & SCM_PORT_CURIN) {
- packet->origport[pcnt] = SCM_VM_CURRENT_INPUT_PORT(vm);
- SCM_VM_CURRENT_INPUT_PORT(vm) = port[pcnt++];
+ packet_before->origport[pcnt] = port[pcnt];
+ packet_after->origport[pcnt++] = SCM_VM_CURRENT_INPUT_PORT(vm);
}
if (mask & SCM_PORT_CUROUT) {
- packet->origport[pcnt] = SCM_VM_CURRENT_OUTPUT_PORT(vm);
- SCM_VM_CURRENT_OUTPUT_PORT(vm) = port[pcnt++];
+ packet_before->origport[pcnt] = port[pcnt];
+ packet_after->origport[pcnt++] = SCM_VM_CURRENT_OUTPUT_PORT(vm);
}
if (mask & SCM_PORT_CURERR) {
- packet->origport[pcnt] = SCM_VM_CURRENT_ERROR_PORT(vm);
- SCM_VM_CURRENT_ERROR_PORT(vm) = port[pcnt++];
+ packet_before->origport[pcnt] = port[pcnt];
+ packet_after->origport[pcnt++] = SCM_VM_CURRENT_ERROR_PORT(vm);
}
- packet->mask = mask;
- packet->closep = closep;
- finalizer = Scm_MakeSubr(port_restorer, (void*)packet,
+ packet_before->mask = packet_after->mask = mask;
+ packet_before->closep = FALSE;
+ packet_after->closep = closep;
+ initializer = Scm_MakeSubr(port_restorer, (void*)packet_before,
+ 0, 0, SCM_FALSE);
+ finalizer = Scm_MakeSubr(port_restorer, (void*)packet_after,
0, 0, SCM_FALSE);
- return Scm_VMDynamicWind(Scm_NullProc(), SCM_OBJ(thunk), finalizer);
+ return Scm_VMDynamicWind(initializer, SCM_OBJ(thunk), finalizer);
}
/*===============================================================
- Shiro(2007/04/16 04:43:12 PDT): 0.8.10では、with-output-to-string etc. をSchemeで 実装する方向でこの問題を解決しました。
この例は意図通りに動いているようなのですが...
gosh> (define g (make-generator hoge)) g gosh> (with-output-to-string g) #?="(stdin)":22:(current-output-port) #?- #<oport (output string port) 0x595960> "" gosh> (with-output-to-string g) #?="(stdin)":22:(current-output-port) #?- #<oport (output string port) 0x595960> "" gosh> (with-output-to-string g) #?="(stdin)":22:(current-output-port) #?- #<oport (output string port) 0x595960> "" gosh>
び(2007/03/19 01:01:26 PDT): やっぱり変。
gosh> (define g (make-generator hoge)) g gosh> (g) #?="(stdin)":16:(current-output-port) #?- #<oport (stdout) 0x99b40> #<undef> gosh> (with-output-to-string g) #?="(stdin)":16:(current-output-port) #?- #<oport (stdout) 0x99b40> "" gosh> (with-output-to-string g) #?="(stdin)":16:(current-output-port) #?- #<oport (stdout) 0x99b40> "" gosh>
だめぽ...
び(2007/03/19 20:16:37 PDT): よく考えてみると、dynamic-windを使ってwith-*port系を実装している限り、これが正しい挙動のような気がしますね。問題は、じゃあ上記のジェネレータはどうあるべきか、ということなんですが、うーん、よくわからない...
継続とdynamic-wind?
び(2007/03/20 01:37:56 PDT): 上記のパッチは、with-{input-from|output-to}-port をdynamic-wind で実装しようとしたものだ。なので、dynamic-windを使って実験してみた。
gosh> (define (hoge yield)
(while #t
#?='hoge
(yield)))
hoge
gosh> (define g (make-generator hoge))
g
gosh> (define (foo n)
(dynamic-wind
(lambda () #?=(format "before: ~d" n))
(lambda ()
#?=(format "main-a: ~d" n)
(g)
#?=(format "main-b: ~d" n))
(lambda () #?=(format "after: ~d" n))))
foo
gosh> (foo 1)
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 1"
#?="(stdin)":26:(format "main-a: ~d" n)
#?- "main-a: 1"
#?="(stdin)":17:'hoge
#?- hoge
#?="(stdin)":28:(format "main-b: ~d" n)
#?- "main-b: 1"
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 1"
"main-b: 1"
gosh> (foo 2)
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 2"
#?="(stdin)":26:(format "main-a: ~d" n)
#?- "main-a: 2"
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 2"
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 1"
#?="(stdin)":17:'hoge
#?- hoge
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 1"
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 2"
#?="(stdin)":28:(format "main-b: ~d" n)
#?- "main-b: 2"
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 2"
"main-b: 2"
gosh> (foo 3)
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 3"
#?="(stdin)":26:(format "main-a: ~d" n)
#?- "main-a: 3"
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 3"
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 1"
#?="(stdin)":17:'hoge
#?- hoge
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 1"
#?="(stdin)":24:(format "before: ~d" n)
#?- "before: 3"
#?="(stdin)":28:(format "main-b: ~d" n)
#?- "main-b: 3"
#?="(stdin)":29:(format "after: ~d" n)
#?- "after: 3"
"main-b: 3"
gosh>
なるほど納得。gは常に一番最初に呼び出した dynamic-wind のmain thunkの中の継続を 保存していることになっているわけだ。
Tags: 継続, dynamic-wind, generator, ジェネレータ