Scheme:継続のarity
Scheme:多値から移って来た話題
継続のarityとCommon Lisp的動作
Rui: 「継続が期待する値の数」とはすなわち継続のarityですよね。これが正しくarity (GaucheRefj:arity)で取得できるとしたらどうでしょうか。たとえば2値が期待されているときだけhash-table-getが2値を返す(2つめはキーが発見できたときに#tになる)ことにすれば、Common Lisp的な「余分な戻り値が捨てられる」という前提なしに、GETHASHの振る舞いを真似することができますよね。これはあくまで例ですが、Common Lisp的な振る舞いを真似できれば実用上役立つこともあるはずです。
Shiroさんの書かれた意味論の観点から見ても、継続のarityを取得するという発想は自然なように思えます。なぜなら関数呼び出しと関数からのリターンは同じことであり、関数からのリターンとは現在の継続に値を渡すことと同じことになるからです。そして実際に、Schemeでは現在の継続を手続きとして取り出すことができるので、(call/cc (lambda (k) (arity k)))のようにして継続のarityを取得しようとすることは可能なわけです。Gaucheでは常に#<arity-at-least 0>が返るようですけど。
Schemeとしていかがなものかという面は想像つきますが、論理的にはきれいにつじつまがあうと思います。それともそもそもarityというのがおかしいのかしら。みなさんどう思います? (2004/06/01 08:05:18 PDT)
hira: arityってR5RSに無いんですよね。R6RSでは追加されるのかなぁ。何をもめてるんだろう。
Shiro: 多分RnRSには入らないんじゃないかなあ。もめるもめないではなくて、 Schemeの仕様策定ってそっちの方向は向いていないような気がします。 SRFIならあり得るかも。
Ruiさんのアイディアに関しては、面白いと思います。Gaucheではご指摘のように 今はサボってますけど、callerの期待する戻り値の数はVMレベルではわかって いるので、うまくやればわりと効率良く継続のarityをサポートできるかもしれません。
Rui: 以下の方針で継続のarityを取得できるようにしてみました。
- 継続フレームにreqargs, restargを追加。
- コンパイル時のコンテキストとしてreqargs, restargを渡す。これらはふつうは(0, 1)だが、receiveのproducerをコンパイルするときだけ(必須の返り値の数, オプションの返り値が許される場合は1、許されないなら0)が渡される。これらの値はPRE-CALL命令に詰め込まれる。
- PRE-CALL命令は、新しく作成した継続フレームのreqargs, restargに適切な値を設定する。それ以外のPUSH_CONTは(0, 1)を設定する。
- call/cc手続きは、それが呼び出された時点の継続フレームからreqargs, restargを取り出し、それをもとに継続手続きを作成する。
ためしに「継続が2値をとるときはエラーを返さないhash-table-get」を書いてみました。キーが見つからなかったときは2値目に#fを返します。
(define ht (hash-table 'eq? '(a . 1))) (define hash-table-get* (let1 default (gensym) (lambda (ht key) (let/cc cont (if (eqv? (arity cont) 2) (let1 val (hash-table-get ht key default) (if (eq? val default) (values (undefined) #f) (values val #t))) (hash-table-get ht key)))))) (hash-table-get* ht 'b) ;; => *** ERROR: hash table doesn't have an entry for key b (receive (val success?) (hash-table-get* ht 'b) (list val success?)) ;; => (#<undef> #f) ;; 上記のようにcall/ccで継続を取り出す方法でも下記のようにCPSで書く方法でも ;; 同じことを同様に実現でき、対称性は高い (define hash-table-get*/CPS (let1 default (gensym) (lambda (ht key cont) (if (eqv? (arity cont) 2) (let1 val (hash-table-get ht key default) (if (eq? val default) (cont (undefined) #f) (cont val #t))) (hash-table-get ht key))))) (hash-table-get*/CPS ht 'b (lambda x x)) ;; => *** ERROR: hash table doesn't have an entry for key b (hash-table-get*/CPS ht 'b (lambda (val success?) (list val success?))) ;; => (#<undef> #f)
coLinuxの上でコードを書いているのでベンチマークがいまいち信用ならないのですが、効率はそんなに悪くないようです。起動されない継続をいちいち取り出す効率の悪さは、call-with-current-continuation-arity手続きを用意して継続を取り出さないですむようにすることで、とりあえず逃げることができそうです。diffはこちら(Rui)においておきます。上記以外のよい実装方法があれば(あると思いますが)教えてください。(2004/06/03 11:03:57 PDT)
Shiro: ああそうか、ここまでしないとダメですね… スタックフレームのスロットを増やすと性能への影響が少し心配ですが、 sizeをu_shortにして1 wordに押し込んでしまえばいいかな。せこいけど。
それと、現在の実装では色んな箇所で多値をlistにパックしているところがあり (call-with-valuesとかcomposeとか)、そのためにコンテキストによって 継続のarityに依存するコードの振るまいが変わってしまう可能性があります。 例えばreceiveを使ってたのをcall-with-valuesに変えたら動作が変わるとか。 こりゃもうSchemeである限り仕方ないですが…
Rui: listにパックする振る舞いは変わらないはずです。現在の振る舞いから変更されるのは、継続手続きに渡す引数の数が足りないときに、継続を呼び出した時点でエラーが返ることだけだと思います。いままではこういうコードは継続が呼び出された直後にエラーになっていたはずなので、結果はまあ同じですね。
ホントの問題は継続のarityによって振る舞いがかわるコードが作法としてよろしいのかというところなんですが、これは微妙ですね。なくて困るものでもないし。移植性がなくなるので使わないことにすると、はじめからそんな一般化はいらないという……。そういうバランスはShiroさんにおまかせします。(2004/06/03 22:54:21 PDT)
Shiro: 実装の中身を覗いていじくれるという機能は、少々危険な香りが しても好きなんで、おもしろそうだなとは思います。 「listにパック」の問題とは、例えば (receive (x y) (hash-table-get* ht key) ...) と、 (call-with-values (hash-table-get* ht key) (lambda (x y) ...)) で ふるまいが変わってしまうんじゃないかってことです。
Rui: ああなるほど、 (receive (x y) f1 (f2 x y)) と (receive x f1 (apply f2 x)) で、f1の振る舞いが違ってくるってことですね。これは (f1 (lambda (x y) (f2 x y))) と (f1 (lambda x (apply f2 x))) が違うのと同根なんでしょうけど、確かに困ってしまうし、簡単にどうにかすることもできなさそうです。やはりarityを使うという自体いまいちなのかも。
Shiro(2012/03/10 11:49:03 UTC): まあ、いまいちという結論が出てるんですが、 http://chaton.practical-scheme.net/gauche/a/2012/03/10#entry-4f5b3bb4-1c94e のようないきさつで今のVMでやってみたらあっさり出来ちゃったので一応パッチをば。 https://gist.github.com/2011209
これ自体ではあんま役に立たなさそうだけど、書いてるうちに多値の扱いについていくつかアイディアが 得られたので、そっちの方向でちょいとまたいじってみるかも。
しかし上の議論は8年も前なのか。