R6RS:変更点

R6RS:変更点

R6RS文書のAppendix Eにも"language changes"として変更点のリストがあるけど、 抜けがあるし、ただずらずらとリストされてるだけで実用上のインパクトがわかりづらいので、 ここではトピックごとにまとめます。

なお、参照箇所を示すために次のような略記を用いることにします。


プログラム構成上の変更点

R5RSでは、「プログラムは、式、定義、構文定義のならびによって構成される。(5:5.1)」 とだけ規定されており、定義の順序が実行に与える影響とか、 同じ変数が複数回定義されたらどうすんの、とかは未定義。 「外に」あるライブラリを読み込む手段としてはload (5:6.6.4)があるけれど、 コンパイルと実行がわかれている処理系でコンパイル時に読まれるのか 実行時に読まれるのか規定されてないし、ファイル名を与えることになってるけど そのファイル名をどう解釈すべきか (適当なサーチパスから探すのかカレントディレクトリ から相対で探すのか、拡張子の扱いは、等)も規定されてないし、 まあポータビリティという点ではでっかい穴になっていました。

R6RSでは、「プログラムは、ひとつのトップレベルプログラムと、いくつかのライブラリから構成される。(6:5.1)」とされました。

「どの」ライブラリを「いつ(コンパイル時か、実行時か…)」使うかという指定は importフォームで行います (6:7)。ライブラリをどこから持ってくるかという話は 処理系依存ですが、字面上ではポータブルなソースを書くのに必要十分なセマンティクスが 規定されています。なお、loadは無くなりました

また、ローカルでない束縛 (トップレベルプログラムのボディ、もしくはライブラリボディ) について次の制限を設けることにより、 コンパイル時にプログラムの意味を確定することが出来るようになっています。(6:7.1)

再定義の禁止は、call/ccにより発生するものにも適用されます。 R5RSでは次のような操作が可能でした(処理系依存だが、明確に禁止されてはいない)。

(define x (call/cc (lambda (c) c)))

x => #<subr continuation>

(x 3)  ;; ここで (define x <...>) の <...> に戻るのでxが再定義される

x => 3

このようなプログラムはR6RS的には不正なプログラムとなります。 R6RSでは(x 3)を呼んでxが再定義されようとしたところで 処理系はエラーを検出して&assertion例外を投げることが強く推奨されています (mustではなくshouldなので努力目標ですが)。

トップレベルの意味の明確化

R5RSはトップレベルについてあまりきちんと定義していなくて、色々な ダークコーナーがありました。

R6RSでは、従来の「トップレベル」に相当する トップレベルプログラムの本体およびライブラリの本体のセマンティクスは、 lambda式の本体と基本的に同じです。 つまり、internal defineとトップレベルのdefineとの差はスコープだけです。 どちらも、同じexpansion process (6:10)を経てletrec*フォームへと 展開されます。 (そもそも"internal define"という概念自体が無くなりました。 これに伴い、lambda式の本体内でもdefine-syntaxを使ってローカルマクロを 定義できます)。

トップレベルプログラムについては、定義と式をまぜこぜに書けるのが 他の「本体」と違いますが、これについては式の方を擬似的に定義とみなして 解釈する方法が取られます (6:8.2)。

ただ、この方法だと実行時に定義や式がインクリメンタルに追加されてゆく 状況で意味を確定できません。そのため、R6RSではインタラクティブな REPLが仕様の範囲外になりました。(R5RSではsection 5.1で インタラクティブREPLについて触れられています (5:5.1))。

なおこれは、処理系が独自拡張としてREPLを提供することを妨げるものではありません。 R6RSの主目的が「ポータブルなライブラリを書くための基盤作り」にある以上、 意味が明確に合意できる最大公約数のところで仕様を止めておくのが良いだろうという 判断です。

エラー処理の明確な定義

R5RSまでは、「エラーである」という文言は「そのプログラムはR5RS仕様上不正である」 というだけの意味でした。そのエラーをどう処理するかは処理系に任されていて、 処理系は独自の例外を投げても良いし、 処理系が操作の定義を拡張して意味のある値を返しても良いことになっていました (5:1.3.2)。

しかしこれでは異常系の処理をちゃんとやるポータブルなプログラムが書けないし、 処理系独自の戻り値にうっかり依存したコードを書いてしまう危険も高いので、 R6RSでは異常系の動作も規定しています。具体的には、

たとえば、R5RSでは()のcarを取るのは「エラーである」と記されています (5:6.3.2)。

  Note that it is an error to take car of an empty list.
 
  (car '())  => error

R5RS準拠の処理系でも、ここでcarの意味を拡張して(car '()) => () とすることが 許されていました。R6RSでは、この場合は&assertion例外をあげなければならないと されています。

  (car '())  => &assertion exception

R6RSの初期のドラフトではこの点が非常に厳格で処理系の解釈のぶれの余地を 極力減らしてあり、例えばset!などの戻り値が規定されない手続きでも、 処理系が選んだ適当な値を返すんではなくて"unspecified value"という オブジェクトを返さなくちゃならない、とか、引数のドメインエラーは全て 検出しなければならない、とかなっていたんですが、 静的型無しでそれをやるのはさすがに厳しすぎるだろうってことで、 最終形ではかなり緩くなっています。(例えば多くの場所で、ドメインエラーの 検出は"must"ではなく"should"、つまり努力目標になりました)。

Unicodeの採用

これもだいぶ揉めたんですが、現実的な観点で言えば(1)ascii文字セットだけでは 実用的なプログラムはもはや書けない、(2)十分に定義されて広く使われている 大文字集合規格の代替案が他に無い、ってことで採用。

これは、Scheme処理系が他の文字セットやエンコーディングをサポートすることを 妨げるものではありません。あくまで「R6RS準拠プログラム」と銘打った ポータブルなコードは、Unicodeを使うこと、っていうだけです。

これに伴い、charはUnicode scalar valueを表現するオブジェクトであると 規定されました(6:11.11)。scalar valueの中にはcombining character とかvariation selectorとか混じってますが、ざっくり全部文字として 扱っちゃいましょうと。これも現実路線ですね。処理系が独自の文字オブジェクトを 定義することは妨げませんが、R6RS準拠モードではcharはunicode scalar valueである、 ということです。

文字列のcase conversionではß <-> SSなどの対応も考慮されます(6lib:1)。

I/Oの強化

R5RSのI/Oはポートへの文字の書きだし(write-char)とポートからの文字の読み込み (read-char)がプリミティブになっていましたが、大文字集合の導入に伴い、 (1)文字入出力とバイナリ入出力とを区別する必要がある、 (2)文字エンコーディング変換などを一般的な形で仕様に取り込む必要がある、 という要求が出てきました。

R6RSのI/Oは大幅に強化されています(6lib:8)。 R6RS:標準ライブラリのI/Oの項も参照してください。

ただし、ノンブロッキングI/Oは入っていません (char-ready?も無くなったし)。

マクロ展開の厳密な定義

マクロはR5RSでSchemeに正式に導入されたのですが、マクロがどのタイミングで 展開されるのかがちゃんと規定されてなかったために、仕様的には結構大きな穴が 空いていました。

R6RSでは定義とマクロ展開の解釈方法が厳密に規定されました(6:10)。 簡単に言えば、最初に全部展開が行われてから定義の右辺式が解釈(コンパイル)されます。 なので上の例では(foo x)はマクロ呼び出しになります。

この動作をうまく説明するために、letrecのように再帰的なスコープを持ち、 評価順序は上から下に固定されている、letrec*というフォームが新たに導入され ました。

(ところで個人的な感慨ですが、R5RSにおけるinternal defineとマクロの事例は、 「言語に機能を矛盾無く追加する」ということがいかに難しいかを 教えてくれる例だと思います。Schemeのベテラン達が何年も議論してて気づかなかったわけで。)

低レベルマクロとphase

syntax-rulesはパターンマッチに基づいたSchemeとは別の言語を定義していたために legacy macroに比べて制限が多かったわけですが、R6RSでは syntax-caseを導入し、健全性を保ったままScheme自身を使ってマクロを書くことが できるようになりました(6lib:12)。

しかし、これは意味論上大きな問題を提起します。例えば次の例のように、 マクロmy-macroの展開の本体を別に定義したaux-procにやらせたいと思ったとしましょう。

(define (aux-proc . args)
   ...)

(define-syntax my-macro
  (lambda (form)
    (syntax-case form ()
      [(_ arg ...) (aux-proc arg ...)])))

しかし上で述べたように、曖昧性を排除するためにマクロ展開は全ての定義の 右辺値の評価に先だって行われることになっています。my-macroの展開には aux-procの呼び出しが必要ですが、aux-procの本体はmy-macroの展開が終わるまでは 使えません。

R6RSはSchemeコードの解釈を次のように規定しました。

つまり、上で示したようなaux-procとmy-macroをひとつのライブラリ本体に 共存させることはできません。しかし、aux-procをlibraryAに、 my-macroをlibraryBに置き、libraryBの方で (import (for (libraryA) expand)) とexpand時にlibraryAを必要としていることを示せば、 libraryBのexpand時にlibraryA内の手続きを実行することができます。

libraryA [expand] -> [run]
                       ^
                       | macro call
                       |
libraryB            [expand] -> [run]

R6RSの標準ライブラリを使うには (import (rnrs (6))) としますが、この時 デフォルトでR6RSの構文と手続きはexpandとrunフェーズの両方にimportされます。 従ってsyntax-caseを使った低レベルマクロ内でR6RSライブラリの手続きは 自由に使えます。

このフェーズの概念はさらに拡張できるんですが、詳しくはR6RSを直接読んで下さい。

なお、R5RSの範囲では、トップレベルフォームをひとつづつ読んで解釈する インタプリタが許されていましたが、R6RSではこの規定によりインタプリタであっても ライブラリひとつをまるごと読み込んで展開を行い、その後で実行する必要があります。 SCMのようにlazyにマクロ展開とコンパイルを行う処理系とは非常に相性が悪い仕様 となっています。

無くなった、もしくは無くなりつつある手続き

無くなった手続き

互換ライブラリで提供される手続き

以下の手続きはR5RS向けに書かれたコードをR6RSに移植するのを楽にするために、 R5RS互換ライブラリとして提供されます。R6RSで新規に書く場合はR6RSで提供される APIを使うことが推奨されます。

なんとなく隅に追いやられた手続き

一応R6RSの標準ライブラリに入ってはいるが、(import (rnrs (6))) ではインポートされず、 個別に指定してインポートしなければならないという点でちょっと差別されてる 手続き達です。

追加された構文と手続き

標準ライブラリについてはR6RS:標準ライブラリ参照。

コア言語について以下にまとめます。

実装者の落とし穴

ユーザにも関係するんですが、どっちかというと実装者を悩ませるかな、という話。

equal? の停止性

R5RSでは、引数に循環のある構造が渡された場合、equal?は停止しないかもしれない と規定されていました。R6RSでは、equal?はその場合でも停止しなければなりません (6:11.5)。Cf. x:equal?

mapとcall/cc

(map proc lis) の proc 実行中に継続が捕捉されて、 後からその継続が再起動された場合、mapも途中から結果のリストを作り直すことになります。 この時、以前に返した結果のリストをmapは変更してはいけません。 この性質は、ambのようなcall/ccを使った非決定性計算とmapを安全に組み合わせるために 必要ですが、実装側にmapの戻り値とするリスト以外に入力リストの大きさに比例する バッファを必要とします。詳しくはR6RS:変更点:mapとcall/ccにて。

数値のサポートの厳格化

複素数まで規定されているSchemeの数値ですが、 R5RSまではそのうちの一部分だけをサポートしても良いことになっていました。 R6RSでは全ての数値型をサポートすることが要求されています。特に、 正確な整数と正確な有理数はメモリが許す限りの大きさをサポートしなければなりません

また、R6RSでは、実数とは虚部が正確な0である数と規定されています (6:11.7.4)。 従って、虚部が不正確な0である 1.0+0.0i は実数ではないことになります。

  (real? 1.0+0.0i)  => #f
  (real? 1.0+0i)    => #t

実装はこのふたつを区別しなくてはなりません。

その他の雑多な変更

雑多、と言っても重要度が低いわけじゃなくて、他にカテゴライズできないって ことですが。

プログラムがCase-sensitiveになった

Lispの長年の伝統をついに破って、識別子がcase-sensitiveになりました。 もっともこれは現状追認とも言えます (参考:x:Concept:CaseSensitivity)。

処理系はオプショナルなcase-insensitive modeを用意しても 良いことになっています (6app:B)。

コメント形式が増えた

こちらも現状追認。従来のセミコロンによる行コメント以外に、 #|...|#のブロックコメント (x:SRFI-30) と #; による式コメント (x:SRFI-62) が認識されます。 また、ソースがR6RS準拠であることを示す特殊トークン #!r6rs も コメント扱いになります。

数値構文のちょっとした変更

|p 表記はあくまで参考で、実装は都合の良いビット数で仮数部を表現しても 構いません。仮数部の精度が問題になるようなアプリケーションを扱う実装「も」許すのが この記法の目的です。例えばIEEE浮動小数点数を使っていても、非正規化領域では 正規化領域よりも精度が落ちてしまいますが、|p記法によって処理系は 精度が落ちたデータであることを陽に示すことができます。 そのような情報は精度にセンシティブなアプリケーションにとって重要かもしれません。

文字列リテラル内のエスケープ記法の拡張

実はR5RSでは、文字列リテラル内で使えるエスケープ記法はダブルクオートを埋め込む\" とバックスラッシュ自体を埋め込む\\ だけで、 改行記号などを埋め込むことは出来なかったのです。

R6RSになって改行\nやタブ\tなども使えるようになったほか、 Unicodeのコードポイント記法なども追加されました。ちょっと便利なのは 長い文字列を複数行に渡って書けるようになったことでしょうか。文字列リテラル内で バックスラッシュに空白文字の連続と改行が続いた場合、その空白文字と改行、 および次の行の先頭の空白文字全てが無視されます。例えば、

(define *long-string*
  "寿限無寿限無\
   五劫の擦り切れず\
   海砂利水魚の水行末雲来末風来末\
   食う寝る処に住む処\
   やぶら小路の藪柑子")

は、次のように書いたのと同じことになります。

(define *long-string*
  "寿限無寿限無五劫の擦り切れず海砂利水魚の水行末雲来末風来末食う寝る処に住む処やぶら小路の藪柑子")

charとstringの比較手続き

char=?、char<?、string=?、string<=? などが3つ以上の引数を取れるようになっています。 数値の <、= などとの一貫性のためでしょう。

quasiquoteのちょっとした拡張

unquote、unquote-splicingが不定長のフォームを取れるようになりました。

[]の採用

R5RSまでは拡張用に予約されていた角括弧[]が、 丸括弧()と同じ意味で使えるようになっています。

この記法は昨日今日出てきたものではなくて、論文などではずいぶん昔から 使われており、サポートしている処理系も少なくありませんでした(Gaucheもサポートしてます)。 そういう点では現状追認とも言えます。

ただ、処理系独自の拡張の余地が減ったという点で賛否両論ありました。

言語マニアしか気にしないような、しかし重要な変更

構文がS式により規定された

R5RSではformal syntaxの定義(5:7.1)において、プログラムの上位構文が readの結果として得られるS式に基づいていると明示されていなかったために、 Scheme:マクロ:CommonLispとの比較:意味論で示したような議論がありました。

R6RSではプログラムの構文は次の3つのレイヤで規定されます(6:4)。

これで晴れてSchemeもLispの仲間入りだぞ。

セマンティクスの定義が表示意味論から操作意味論になった。

R5RSまではformal semanticsが表示意味論に基づいて規定されていました(5:7.2)が、 結構穴が多かったらしいです。らしい、というのは私も全部理解してるわけじゃないんで。 R6RSのセマンティクスはSchemeコア言語のより広い範囲をカバーしています。 他にも、手続きの引数の評価順が不定であることについてちゃんと意味論で表記したとか いろいろあるみたいですが、全部はよくわかりません。

More ...