Code Aquarium

minazoko's blog -*- 水底のブログ -*-

(Racket) トップレベルbeginの継続が捕捉できない

仕様?バグ?

#lang racket
(define cc '())
(begin
  (display 'A)
  (call/cc (lambda (k) (set! cc k)))
  (display 'B))
(cc) ;; なにも表示されない。

call/ccの継続は (display 'B) を実行してトップレベルへ戻る処理の筈。だけど保存しておいた継続を実行しても何もおこらない。r6rsモードで実行しても同じ。
他処理系 (gauche, chickin, guile) で確認したところ皆(cc)でちゃんとBが表示される。
あまり書くことはなさそうなケースなのでクリティカルではないけれど、少し気持ち悪いですね。一応記憶の片隅に置いときます。

原因はbeginの最適化?

(begin ...) を (let () ...) や ( (lambda () ...)) にすると期待どおり Bが表示される。
また (let () (begin ...)) のようにトップレベルでなければ、ちゃんと begin内の継続は捕捉されますが、(begin (begin ...)) のようにbeginを重ねるだけだと、やっぱり継続は捕捉されない。

begin と let の違いと言えばスコープを作るかどうか変数束縛があるかどうか。この差に何か原因がありそうかな、とぐーぐる先生に聞いてみたところ、stackoverflowにこんなQ&Aがありました。

曰く、こんな風にネストした beginは

(begin
  (begin
    e1)
  (begin
    e2
    e3)
  (begin
    e4))

こういう風に spliceされると。

(begin
  e1
  e2
  e3
  e4)

そして、さらに

The second kind (which is what you have) will splice
all of its arguments into the surrounding context.

という事なので、つまりトップレベルの beginはトップレベルのコンテキストに展開されてしまうということかな。
結局冒頭のコードは surrounding context に splice されて。

#lang racket
(define cc '())
(display 'A)
(call/cc (lambda (k) (set! cc k)))
(display 'B)

確かにこれじゃあ k はいきなりトップレベルに戻るだけの継続になってしまいますね。

マニュアルは読みましたか?

ドキュメントにあたってみると、確かにそんな事が書いてありました。
3.15 Sequencing: begin, begin0, and begin-for-syntax
beginの位置が top-level, module-level または internal-definition position の場合には、

the begin form is equivalent to splicing the forms
into the enclosing context.

しかしこれ、他処理系はそうならないので、racket特有の仕様ということになるのでしょうね。
うーん、なんだかな。

あれ? (追記)

Gaucheのドキュメントを見ると、
Gauche ユーザリファレンス: 4.7 順次実行

意味的には、beginはまるでform …が beginを囲むコンテクスト中に展開されているかのように振舞います。 
例えば、トップレベルに次のような式があった場合、それは2つのトップレベルのdefineと 同等です。

言ってることは racket と同じように見える。
ここで言う define がトップレベルにあるのと同等ならば、冒頭の例もracketと同様 call/ccが (display 'B)を継続として捕捉しないような気がするけどそうではない。何か理解が間違っているのかな。