表題

#, 外部フォーム

著者

Oleg Kiselyov

状態

この SRFI は現在「確定」の状態である。 SRFI の各状態の説明については ここ を参照せよ。

関連する SRFI

ここで提案する拡張可能な外部表現 #,() は、 新しい型の Scheme 値を導入する他の SRFI において、 具体的に定義されることを意図している。 たとえば、SRFI-4 「一様数値ベクタ型」で定義されるデータ型の外部表現を規定するために使うことができる。 浮動ベクタの外部表現は「標準 Scheme に対する些細な違反」であったが、 この SRFI ではこの違反を解消することができる。 また、SRFI-4 の議論アーカイブで述べられているような、 要素数が 0 である次元をもつ2次元の行列を表現することができないという問題を解消することができる。 ちなみに、本提案は SRFI-4 における議論に触発された。

この SRFI では、 Common Lisp のリーダーマクロと特殊な # 記号フォームについて議論し、それをお手本としている。

[訳注] 上記はリンク切れ。代替として次の URL を示しておく: Common Lisp

概要

この SRFI では、Scheme 値の拡張可能な外部表現を提案する。 この外部表現は将来の SRFI で使われることを想定している。 この SRFI では、新しいトークン #,( を追加し、 Scheme リーダーの文法生成規則を拡張する。 #,() フォームは、 たとえば、 簡便な印字表現をもたない値を記述する場合や、 コードを条件コンパイルするために使うことができる。 将来の SRFI で新しい値の読み取り構文を導入する場合、 この #,() 記法と適切なタグ シンボルを使用することを提案する。

#,() 記法の特定の応用例と参照実装を示すために、 この SRFI では #,() 外部フォームを 「読み取り時適用」として解釈した説明を行う。

課題

なし。

論拠

ブール値、数値、リスト、ベクタ、文字列の外部表現は、 RnRS で定義されている。 どのような Scheme リーダーでも、 外部表現を解析して Scheme 値を構築する方法を知っているが、 構築できるのは組み込み型に限られており、 その仕組みを拡張することはできない。 この SRFI では、この制限をなくし、ポート、構造体、ウィル、セマフォや、 多くの Scheme システムで実装されている他のデータ型、 および、将来の SRFI で導入されるデータ型に対して、 印字表現とシリアル化を可能にする。 しかし、ここで提案する外部フォームは、構造化されたデータや複雑なデータだけでなく、 標準 Scheme の値に対しても、使用することができることを強調しておく。 標準 Scheme 値の標準の外部表現は扱いにくく、外的な状況に依存し、不適切である。

#,() 記法は、read 手続きで処理される任意の文字ストリームの中で使用できる。 たとえば、ストリームの中で #,() フォームを使用してデータを読み取らせ、 何らかのアルゴリズムの初期値やパラメータとして使用することができる。 一方、文字ストリームから Scheme コードを読み取って評価するような場合は、 #,() フォームにより、 リテラル値や条件コンパイルされるコードを記述することができる。

将来、SRFI-X が Scheme の新しい型を導入し、 その値を書き込んだり読み込んだりすることを想定するかも知れない。 そのような SRFI は、Scheme の形式構文を拡張する必要が出てくる (R5RS の Section 7.1.1 と 7.1.2 を参照)。 そのような拡張を行うための最も一般的な方法は、次のような形式構文を導入することである。

        "#" <discriminator> <other-char>*
ここで <discriminator> は '(', '\', 'i', 'e', 'b', 'o', 'x', 'd' あるいは 'f', '!', 't' 以外の1文字である。 しかし、これでは <discriminator> の選択肢はそれほどないことになる。 覚えやすくすることもできない。

SRFI-10 では、Scheme 値の新しい外部表現を追加するための、 もっと系統立った、表現力のある方法を導入する。 この方法では #,(<tag> <datum>*) という形式で記述する。 新しいデータ型を導入する SRFI-X は、適切な <tag> (シンボル) と <datum> 引数を決めるだけでよい。 新たな文法生成規則を追加する必要はないし、 <discriminator> に残されている文字を選択するために思案する必要もない。 SRFI-X の実装では、新しいデータ型の値を #,() フォームに書き込んだり、そこから読み取ったりする機能を提供しなければならない。 この読み書きを行うための方法は、SRFI-X あるいはその実装に任されている。

#,() という記法は、新しい型に対してのみ有用なのではない。 既存の Scheme 型に対しても有用なものとなる。 たとえば、循環リストや循環依存をもつ他のデータ構造体、 大きなキャッシュやメモ化テーブルをもつデータ構造体、 などである。 別の可能性としては、次のような外部表現を導入してもよい。

        #,(pi) #,(epsilon) #,(Infinity) #,(NaN)
これらは (非正確な) 数値を表す。

別の応用として「変更可能な定数」が定義できる。 たとえば OS の種類を表す #,(os-type) という定数である。 Scheme リーダーはすべての #,(os-type)"Solaris""HP-UX" といった適切なリテラルに置換する。 この「束縛」は非常に早い段階で行われるため、 対応する文字列はマクロや構文やその他の特殊フォームにより解析することができる。 これとよく似た別の応用として、 Scheme 処理系がサポートしているフィーチャ識別子のリストを返す #,(srfi-features) という記法が考えられる。

#,(foo) とそれと類似した (macro-expand-foo)(force (delay (compute-foo)) との違いを認識することが重要である。 #,(foo) は読み取り時に処理されるので、 入力ストリームにおいてのみ意味を持ち、 read で読み取られた跡は、単なるデータになってしまう。 (macro-expand-foo)(force (delay (compute-foo)) は、入力ストリームをスルーして、 コンパイラや評価機にそのまま渡される。 評価機やマクロ エクスパンダは #,(foo) フォームを扱うことはない。 入力ストリームに現れる #,(foo) フォームは、 コンパイラやインタプリタに渡される段階では、すでに Scheme 値に置換されている。 このことは、R5RS 7.1.2 節にある 'bar のような <省略形> (abbreviations) の処理と同じである。 評価機はトークンとしてのアポストロフィに遭遇することはない。 評価機は 2 つの要素を持つリストに遭遇することになり、 そのリストの最初の要素は quote である。 特殊フォーム内に #,(foo) が存在する場合、この構文は #,(foo) という構文を含むわけではない。 それに対応するリテラルを含むだけである。 それと反対に、特殊フォームへの引数として (force (delay (compute-foo))(macro-expand-foo) を渡すと、これらの評価値やマクロ展開結果ではなく、 これらの引数がとしてそのまま渡されることになる。

仕様

この SRFI では、R5RS 7.1.2 節「外部表現」で定義されている外部表現の文法を、次のように拡張する。

<datum> ---> <simple datum> | <compound datum> | <hash-comma-datum>

<hash-comma-datum> ---> "#,(" <tag> <datum>* ")"
<tag> ---> <symbol> | <hash-comma-datum>
さらに、R5RS 7.1.1 節の <token> の生成規則を、次のよう修正する。
<token> ---> <identifier> | <boolean> | <number> | <character> | <string>
           | ( | ) | #( | ' | ` | , | ,@ | . | #,(

この SRFI では、Scheme リーダーが特定の <hash-comma-datum> を解析して対応する値を構築する方法については規定しない。 その方法は、その値に関する特定の SRFI で定義するべきである。 #,() の解析に失敗した場合の Scheme リーダーの振る舞いも規定しない。

実装

「Scheme に関する報告書」(RnRS) では、 入力ストリームが外部表現の文法に従っていない場合の リーダーの振る舞いについては規定されていない (R5RS 7.1.2 節)。 特に、リーダーがエラーを報告することは要求されていない。 つまり、絶対に報告しなければならないエラーが発生しない入力ストリームの集合を考えると、 SRFI-10 を実装するかしないかに関わらず、その集合は変化しないのである。 この意味において、SRFI-10 は Scheme 言語を変更しておらず、 したがって、SRFI-10 は既存の Scheme システムにすでに「実装されている」と言うことができる。

読み取り時適用

「読み取り時適用」(read-time application) は、SRFI-10 の実装の一つである。 ここでの「実装」という言葉はより構成的な意味をもつ。 読み取り時適用は、Smalltalk のオブジェクトのシリアル化や Common Lisp の #.#S() リーダーマクロに類似している。 読み取り時適用では、修正された文法の <hash-comma-datum> を、外部表現として解釈する。
"#,(" <tag> <datum>* ")"
ここで、tag は識別子 (識別子の外部表現) であり、 datum は何らかの値の外部表現である (これも読み取り時適用であってよい)。 read 手続きは tag に関連付けられたリーダー コンストラクタ (reader-constructor) を検索し、引数となる datum... を読み取り、その引数にコンストラクタを適用する。 そのコンストラクタの戻り値が #,() 外部フォームに対応する値となる。 tag に関連付けられたリーダー コンストラクタが存在しなければ、エラーである。

シンボルタグとコンストラクタ手続きを関連付けるための方法はいくつかある。 たとえば、以下のような方法がある。

この SRFI-10 の参照実装では define-reader-ctor 手続きを実装している。 以下の使用例は、この選択をした場合の例を示している。

比較

「読み取り時適用」という仕組みは、Lisp リーダーのマクロ機能に非常に近い。 しかし、Common Lisp とは異なり、 リーダー コンストラクタ は、それ自身の入力ストリームから読み取ることは許されていない。 すでに読み取られて初期化された他の値からのみ、新しい値を構築することができる。 reader-constructor は常に 1 つの値を返さねばならない。 「何も返さない」ということは許されない。 しかし、例外をスローしたり、 #f のような「不適切な」値を返してもよい。 「コンパイル時の適用」(マクロ展開) とは異なり、 「読み取り時適用」では、セカンド パス (二次処理) がない。 #,() フォームは、 リーダー コンストラクタが呼び出されたときに完全に読み取られるので、 Common Lisp が禁止しているコンストラクタにおける副作用を、緩和することができる。 Common Lisp のリーダー マクロ機能とは異なり、 ネストしていない1個の #,() フォームを読み取るときには、 reader-constructor が何度も呼び出されることはない それに加えて、副作用を禁止することは、とても強制し難いことである。 しかし、その制限を解除したとしても、 リーダー コンストラクタで副作用を推奨しているわけではない。

Common Lisp には #.obj という外部フォームがある。 このフォームは Lisp リーダーがこれを構文解析した直後に obj を評価しろという指令である。 #. は汎用の読み取り時評価である:
      #.obj === (eval obj)
それに対して #, は単なる適用である:
      #,obj === (apply (lookup (car obj)) (cdr obj))
このとき obj はリストの外部表現でなければならない。 「読み取り時適用」(read-time application) は汎用の「読み取り時評価」(read-time evaluation) よりも低機能である。 後述の「使用例 5」がこの違いを示している。

「読み取り時適用」ではさらに、 この目的のためだけに (define-reader-ctor) を用いて) 特別に定義された手続きにのみ制限されている。 そのため、読み取り時にどの手続きが適用されるかについて、 きめの細かい制御ができる。 読み取り時に任意の計算を行うことには、セキュリティ上の問題がある。 たとえば、アプリケーション サーバーが リクエストを受け取るパイプからデータを読み取る場合、 副作用を持つ潜在的に危険な手続きを実行したくはない。 Common Lisp では、*read-eval* グローバル変数を nil に設定することで、このような危険な振る舞いを回避することができる。 しかし、これは「全か無か」の回避方法であり、 安全で適切な手続きも含めて、 すべての #. フォームの評価をグローバルに無効にしてしまう。 そのため、この参照実装のように リーダー コンストラクタに制限を課すことは、 読み取り時計算に対してきめの細かい制御を可能にする。 さらに、この参照実装では、 リーダー コンストラクタを読み取りテーブルに登録する。 リーダーを読み出すたびに、 実際の読み取りテーブルに応じて、 「読み取り時適用」を実行することになる。

Common Lisp には #S(typename . data) という記法があり、 これは読み取り時に値を構築するが、コンストラクタをあらかじめ登録しておかなければならない。 結果として、データ構造体に対してだけ使われている。 この SRFI はこの記法を一般化し、 「読み取り時適用」によって任意の値を計算できるようにし、 S-式がコンパイラやインタプリタに入力できるようにしている。

使用例

使用例 1

Scheme の標準的なデータ型の印字表現。

   (define-reader-ctor 'list list)
   (with-input-from-string "#,(list 1 2 #f \"4 5\")" read) ==> (1 2 #f "4 5")

   (define-reader-ctor '+ +)
   (with-input-from-string "#,(+ 1 2)" read) ==> 3

使用例 2

構造体 (レコード) の読み取り可能な表現。
   (define-reader-ctor 'my-vector
     (lambda x (apply vector (cons 'my-vector-tag x))))
   (with-input-from-string "#,(my-vector (my-vector 1 2))" read) ==>
       2つめの要素が、シンボル my-vector、1、2、のリストであるベクタ。
   (with-input-from-string "#,(my-vector #,(my-vector 1 2))" read) ==>
       2つめの要素が、1 と 2 で構成された my-vector であるベクタ。
   (with-input-from-string "#,(my-vector #,(my-vector #,(+ 9 -4)))" read) ==>
       '#(my-vector-tag #(my-vector-tag 5))

使用例 3

一様ベクタ (SRFI-4) を #, 記法で表現したもの。
   (define-reader-ctor 'f32 f32vector)
   (with-input-from-string "#,(f32 1.0 2.0 3.0)" read) ==>
       3 つの要素を持つ f32 一様ベクタ。

使用例 4

ポートの外部表現。
   (define-reader-ctor 'file open-input-file)
   (with-input-from-string "#,(file \"/tmp/a\")"
     (lambda () (read-char (read)))
これはファイル"/tmp/a" の最初の文字を返す。

使用例 5

Common Lisp の #. とこの SRFI の #, との違いを示す。
   (with-input-from-string "#,(+ 1 (+ 2 3))" read) ==>
       エラー:数値 1 とリスト '(+ 2 3) を加算することはできません。
   (with-input-from-string "#,(+ 1 #,(+ 2 3))" read) ==> 6
Common Lisp では次のようになる。
   (with-input-from-string (is "#.(+ 1 (+ 2 3))") (read is))  ==> 6

使用例 6

読み取り時適用を含んでいるファイルをロードする場合: "foo.scm" というファイルが次の内容を含んでいるとする。
   (define (temp-proc)
     (let ((v '#,(f32 1.0 2.0 3.0))) (f32vector-ref v 1)))
このとき、次のようになる。
   (define-reader-ctor 'f32 f32vector)
   (load "foo.scm")
   (pp temp-proc) ==>
       (lambda () (let ((v '#f32(1. 2. 3.))) (f32vector-ref v 1)))
   (temp-proc) ==> 2.0

完全な実装と検証コードは、次から入手できる。

read-apply.scm
vread-apply.scm
検証コードの冒頭のコメントに、このコードの起動方法が記載されている。

SRFI-0 の cond-expand フォームを読み取り時適用で実装したものとその検証コードが、 次から入手できる。

cond-expand.scm
vcond-expand.scm

この実装で羽 SRFI-0 に記載されている文法に直接的に従っている。 有効化されているフィーチャ識別子が ALL-FEATURES というリストに含まれていることを前提にしている。 このリストは Scheme 処理系内で定義済みであるか、あるいは、 このコードを読み取る前に何らかの方法で定義されていなければならない。 この実装は、ユーザー コードを評価する前に Gambit Scheme インタプリタをカスタマイズする方法のデモをしている。 SRFI-10 と SRFI-0 を有効化し、フィーチャのリストを構築している。 このリーダー コンストラクタのコードは、 SRFI-0 が想定していたことと反して、 cond-expand を実装するために必要な リーダーの変更は小さく簡単であることを示している。

著作権

Copyright (C) Oleg Kiselyov (1999). All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Author: Oleg Kiselyov oleg@pobox.com, oleg@acm.org, oleg@computer.org

Editor: Shriram Krishnamurthi