Scheme:継続のarity

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を取得できるようにしてみました。

ためしに「継続が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年も前なのか。

Tags: 多値, 継続, Arity

More ...