読者です 読者をやめる 読者になる 読者になる

Code Aquarium

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

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

Racket Scheme

仕様?バグ?

#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)を継続として捕捉しないような気がするけどそうではない。何か理解が間違っているのかな。

(Racket) composeマクロ

Racket Scheme

composeマクロを作る

関数合成を行うcomposeは通常関数で提供されます。それをマクロで作るとどうなるかという話。昨日即興で書いた物が全然ダメダメだったので再挑戦です。使用するのは syntax-rules。

compose1 と compose

Racketには2つのcompose関数があります。違いは合成対象関数が多値を返す事を許可するかしないかです。

compose1

合成対象の関数間で多値を受け渡すことはできません。compose1の1は対象関数の返却値が1つであることを意味します。返却値が1つという事はその後に続くすべての関数は必ず1つの引数へ適用され1つの値を返す事になります。ただし最初に評価される関数だけは複数の引数を渡すことができ、最後に評価される関数だけは多値を返すことができます。

compose

合成対象の関数間で多値を受け渡すことができます。関数が多値を返すとその多値を可変長の引数として次の関数に渡します。call-with-valuesを挟むような動作になります。

書いたマクロ

https://gist.github.com/mnzk/6b785fd80c129c20b237#file-mycompose-rkt

それぞれ再帰部分をヘルパに切り出し、ヘルパの結果をラムダ式で包んでいます。ラムダの引数リストはリストのまま再帰の最深部まで送っています。

(define-syntax mycompose1
  (syntax-rules ()
    ((_) values)
    ((_ f1 f2 ...)
     (lambda arglist (mycompose1$ arglist f1 f2 ...)))))

;; helper
(define-syntax mycompose1$
  (syntax-rules ()
    ((_ arglist f) (apply f arglist))
    ((_ arglist f1 f2 ...)
     (f1 (mycompose1$ arglist f2 ...)))))
(define-syntax mycompose
  (syntax-rules ()
    ((_) values)
    ((_ f1 f2 ...)
     (lambda arglist (mycompose$ arglist f1 f2 ...)))))

;; helper
(define-syntax mycompose$
  (syntax-rules ()
    ((_ arglist f) (apply f arglist))
    ((_ arglist f1 f2 ...)
     (call-with-values (thunk (mycompose$ arglist f2 ...))
       f1))))

これを見ると何故二つに分けているかが分かる気がします。compose1で済む場合はこちらを使った方がcall-with-valuesのオーバーヘッドを省略できます。*1

lambdaの引数リスト

lambdaの引数はリストの形ではなく丸ごと仮引数にすることができるんですね。

((lambda lis lis) 42)  ;=> (42)

普通(?)可変長引数はドットリストの形で書くと思います。

(lambda (x . more)  <body>)

でも、これだと引数ゼロ個が表現できません。composeでは(実用上意味はありませんが)引数ゼロ個の関数へ適用することもできますのでこのドットリスト形式では都合が悪いのです。引数リストを丸ごとシンボルにすることでゼロ個を含む可変長を表現することができました。

マクロだからマクロに適用できる?

composeをマクロ化すると関数だけではなくマクロや特殊形式オペレータの合成にも使えるのではないかと期待しました。が、この実装ではapplyを使っているので関数しか対象にできません。
もっとも applyが適用されるのは最初に評価される関数だけなのでそれ以外の場所ならばマクロや特殊形式を挿入することは可能です。

(define-syntax inc
  (syntax-rules()  ((_ x) (+ 1 x))))

((mycompose1 inc inc inc identity) 1) ;=> 4

新たな制限が加わりますが一応はマクロの合成もできています。ただしこれはmycompose1だけ可能な事で、mycomposeのほうはcall-with-valuesを再帰の度に挿入しているのでそう簡単にはいきません。いろいろ試行錯誤してみると、今度は逆に lambdaの引数リストをひとまとめにしていることがネックになって行き詰まってしまいます。
こんな風にパターンマッチできればあるいは・・・

(define-syntax mycompose
  (syntax-rules ()
    ((_) values)
    ((_ f1 f2 ...)
     (lambda (a ...) (mycompose$ (a ...) f1 f2 ...)))))

そもそもこれが無理ですが。

*1:実際のRacketの実装は読み解いていませんが

(Scheme) syntax-rulesの引数が1つのパターンは省略できる?

Scheme

前からあやふやだったので確認してみた。
これでandマクロが作れるのは違和感がある。

(define-syntax myand
  (syntax-rules ()
    ((_) #t)
    ((_ e1 e2 ...) (if (not e1)
                       #f
                       (myand e2 ...)))))

(_ e1 e2 ...) が引数2つ以上を表すならば、上のマクロ定義では1引数の場合に対応できないはず。でもちゃんと動いてしまう。

(myand #f)  ;=> #f

衛生マクロはあまり詳しくないのだけど、これは正しい動作なのだろうか?

試した処理系は racket と gauche。どちらも通る。

追記

マクロ展開形を見ればわかることでしたね。

(myand #f)

をexpandすると

(if (not #f) #f (myand))

なんと無くそんな気はしていたんだけど、
上で書いた (_ e1 e2 ...) は引数2つ以上を表しているわけではないようです。
ここに違和感があるんだけど、そういうルールなのかな。

追記2

答えはこちら。

(_ e1 e2 ...) は引数2つ以上、ではなく、引数1つ以上をあらわす。引数が1つだけの場合、 e2 ... の部分はまるっきり存在しないものとして式展開されるということのようです。

意味的には「引数1のケースが未定義」ではなく、「引数1つ以上のケースを定義」していると考えるのが正しい。