さよならcall/ccとwith-exception-handlerやguardへの置き換えの検討

yuniではfull-continuationの提供を止めることにした。単純な理由はKawaがこれを提供していないことだが、じゃあこれに代えるプリミティブを何にするのかという点がちょっと悩ましい。
call/ccに対する批判はOlegのページが詳しい( http://okmij.org/ftp/continuations/against-callcc.html )。要するに現実的なプログラムで使うのは難しいという話で、実際継続を介したポートI/OやFFIは難しい。yuniやその前身で書かれたSchemeプログラムはそれなりの量が有るが、全てone-shotな継続しか使用しておらず、full-continuationが真に必要なケースは無い。
Rubyはcall/ccを1.9でContinuationライブラリに移し、

非推奨になりました。代わりにfiberを使ってください。

と地獄のようなことが書かれている(Fiberって継続の替わりになるの?)。
yuniも同様にcall/ccを(yuni continuation)あたりのライブラリに追い出してオプショナルにすることを考える。ただ、今のところ、yuniがサポートするScheme処理系で"call/ccが無いことを前提にパフォーマンスメリットを提供している処理系"は存在しないようだ。

with-exception-handlerとguardのセマンティクス

当のKawaはcall/ccをJavaの例外で実装している。

Kawa continuations are implemented using Java exceptions, and can be used to prematurely exit (throw), but not to implement co-routines (which should use threads anyway).

また、KawaはguardなどのSchemeの例外機構をJavaの例外機構にマップしている。

The Scheme exception model is implemented on top of the Java VM’s native exception model where the only objects that can be thrown are instances of java.lang.Throwable. Kawa also provides direct access to this native model, as well as older Scheme exception models.

実際、C++JavaScriptのように言語のコア機能として例外機構を実装しているケースは多いため、これらと良くマッチするならばSchemeの例外機構はプリミティブとして適切だろう。ただちょっと難しいのは、Schemeの例外機構のセマンティクスをより理解して進めないと危ない気がしている。...つまりyuniの場合10近くあるScheme処理系でちゃんと挙動が一致するのかどうかを比較的真面目に気にする必要が出てくることになる。
(正直、Schemeの例外手続きの使いかたを覚えるのが面倒だからcall/ccで書かれているコードがそれなりにあるので、それらをguardなりwith-exception-handlerなりに書き換えたときに挙動を変えない自信が現状あんまり無い。)
Kawaの場合、guardはwith-excpetion-handlerより制約のあるプリミティブで、guardの方が効率的としている:

Performance note: Using guard is moderately efficient: there is some overhead compared to using native exception handling, but both the body and the handlers in the cond-clause are inlined.

(with-exception-handlerはlambdaの分のコストがある -- はずなんだけど上手いこと逆コンパイルできなかったので調査できていない。ToDo。)
ただ、通常の処理系ではwith-exception-handlerをプリミティブとして使用している。例えば、Gaucheのpull request https://github.com/shirok/Gauche/pull/335 は興味深い調整で、Gaucheに元から有ったSRFI-18の with-exception-handler とR7RSの同名手続きの挙動差への言及がある。
guardとwith-exception-handlerはどちらも例外ハンドラを表現するが、その差については最終的なコミットのドキュメントに要約されている:

(SRFI-18のwith-exception-handlerについて、)
例外がraiseやerrorで通知されると、投げられたコンディションを引数としてhandlerが呼び出し元と全く同じ動的環境で呼び出されます。つまり、handler中でraiseを呼び出すと、再びhandlerが呼ばれます。また、handlerから戻ると、制御はraiseの呼び出し元に戻ります。

この振る舞いはSRFI-18により定義されました。これは、この手続きが例外制御の最もプリミティブな構成要素になることを意図しています。例外処理中にアクティブなハンドラを切り替えたければ、自分でそう書く必要があります。

通常、例外処理中に例外が発生したら、それは「外側の」ハンドラで処理したいでしょう。そういった典型的な使い方には、guardを使ってください。この手続きはあくまで、例外処理の最も低層にアクセスしたい時のみ使うとよいでしょう。

R7RSにも同名の手続きがありますが、一つだけ違いがあります。R7RS版はhandlerを呼び出す前に現在の例外ハンドラを一つ「外側」の例外ハンドラに置き換えます。R7RSのwith-exception-handlerの説明はR7RS base libraryを参照してください。

このR7RS(= SRFI-34)とSRFI-18の差は https://github.com/shirok/Gauche/commit/234d0ef154ade1983283ece93f52d462d7833ca4#diff-fb36b06efa4fdb8b419faa6d294f0e0aR350 のコードでSRFI-18 with-exception-handlerを元にr7rs:with-exception-handlerを定義することで吸収している。
Gaucheでは例外ハンドラを戻す挙動をwith-exception-handler内に実装しているが、SRFI-34ではraiseの内部に実装しているという微妙な違いがある。

;; (注: raiseの内部で呼ばれているのはwith-exception-handler"s"であることに注意。
;;      参照実装はwith-exception-handlerを実装するために、例外スタックを直接
;;      設定できるwith-exception-handlersを定義して使っている)
(define (raise obj)
  (let ((handlers *current-exception-handlers*))
    (with-exception-handlers (cdr handlers)  ;; ★ 例外スタックから現在のハンドラを除く
      (lambda ()
        ((car handlers) obj)
        (error "handler returned"
               (car handlers)
               obj)))))

guardの実装にcall/ccが必要な問題

では処理系はwith-exception-handlerさえ提供すれば十分なのかと言うと、実はそうでもないという問題がある。guardの実装にはcall/ccが必要になる

(define-syntax guard
  (syntax-rules ()
    ((guard (var clause ...) e1 e2 ...)
     ((call/cc ;; ★ guardの動的環境のキャプチャ
        (lambda (guard-k)
          (with-exception-handler
            (lambda (condition)
              ((call/cc ;; ★ re-raise用にexception-handler用の動的環境をキャプチャ
                 (lambda (handler-k)
                   (guard-k
                     (lambda ()
                       (let ((var condition))
                         (guard-aux
                           (handler-k
                             (lambda ()
                               (raise-continuable condition)))
                           clause ...))))))))
            (lambda () ;; with-exception-handlerで囲まれたbody部分
              (call-with-values
                (lambda () e1 e2 ...)
                (lambda args
                  (guard-k
                    (lambda ()
                      (apply values args)))))))))))))

この"動的環境のキャプチャ"が曲者で、基本的に他でcall/ccが使われることを想定する限りはcall/ccをここでも使わざるを得ない。例えば、Chezはcall/ccの他にone-shot continuationのためにcall/1ccを持つ( http://cisco.github.io/ChezScheme/csug9.4/control.html#./control:h3 )が、guardの実装には通常のcall/ccを使っている https://github.com/cisco/ChezScheme/blob/75107ee73f3619e1afcf043822cf5fbf675522e3/s/exceptions.ss#L254
通常のシチュエーションではguardやその各clauseの継続から複数回返ることは無いが、guardの内部でキャプチャされた継続から複数回返る場合に問題になる。
yuniでは一部の処理系で自前のexpanderを実装している関係上、移植レイヤに構文を使わないのが原則なので、guardの内部で使用されるcall/ccも何らかの形で抽象化したい。。もちろん、guardが構文である所以は単に変数のバインディングを行っているという特徴だけなので、define-record-typeのように特別扱いしても良いのかもしれない。(R7RS small(= SRFI-9)のrecordには手続きレイヤが無いため、expanderはdefine-record-typeの知識を持っていないと実装できない。つまり、letとかlambdaのようなコンパイラプリミティブと同等の扱いがdefine-record-typeには必要となってしまっている。)
逆に言うと、guardはR7RSの標準構文で唯一call/ccを内包する構文になるため、これさえ特別扱いすれば移植性要求からcall/ccを一掃できる。
ちょっと気になるのは、では、guardやwith-exception-handlerの存在を仮定した場合、内部的にはネィティブ例外で実装されているKawaの継続キャプチャを表現することは可能なのかという点。つまり、call/1ccはguardやwith-exception-handler、dynamic-wind等の自然な例外プリミティブで実装可能か?という問題が考えられる。まだあまり真面目に考えていないが、C++とかJavaScriptのようなネィティブ例外機構を持つ言語ランタイムの上にSchemeを実装する場合にcall/1ccが自然に実装できると言えるならば、call/1ccは良いプリミティブになり得る気がしている。