Title

R6RS Syntax-Case Macros

Authors

Kent Dybvig

Status

This SRFI is currently in ``withdrawn'' status. To see an explanation of each status that a SRFI can hold, see here. To provide input on this SRFI, please mailto:srfi-93@srfi.schemers.org. See instructions here to subscribe to the list. You can access previous messages via the archive of the mailing list. You can access post-withdrawal messages via the archive of the mailing list.

This SRFI is being submitted by a member of the Scheme Language Editor's Committee as part of the R6RS Scheme standardization process. The purpose of such "R6RS SRFIs" is to inform the Scheme community of features and design ideas under consideration by the editors and to allow the community to give the editors some direct feedback that will be considered during the design process.

At the end of the discussion period, this SRFI will be withdrawn. When the R6RS specification is finalized, the SRFI may be revised to conform to the R6RS specification and then resubmitted with the intent to finalize it. This procedure aims to avoid the situation where this SRFI is inconsistent with R6RS. An inconsistency between R6RS and this SRFI could confuse some users. Moreover it could pose implementation problems for R6RS compliant Scheme systems that aim to support this SRFI. Note that departures from the SRFI specification by the Scheme Language Editor's Committee may occur due to other design constraints, such as design consistency with other features that are not under discussion as SRFIs.

目次

1. 概要

本文書は R5RS のマクロシステムを拡張する構文抽象システムについて述べる。 これにより低レベルのマクロを高レベルの形式で書けるようになり、構文チェック、入力の分解、出力の再構成、静的スコープと参照透過性(健全性)の管理が自動化され、明示的な識別子の捕捉が可能になり、展開のオーバーヘッドが一定になる。 このシステムではクォートされたリストやベクタのようなリテラルを複製・走査する必要がないため、プログラム中の定数内・定数間の共有構造や循環構造を保存することができる。 また、ソースコードとオブジェクトの相互関係もサポートする。 すなわち、もとのソースコードと展開された出力のつながりを保持し、実装系がソースレベルのデバッガ等のツールを提供することを認めている。

2. 動機

多くの構文抽象は高レベルの syntax-rules を使えば書くことができるが、その他の、入力に明示的にはあらわれない識別子への参照や束縛を導入するものや、状態を持つもの、ファイルシステムの読み込みをするもの、新たな識別子を構成するようなものは記述が困難であるか記述が不可能である。 ここで述べる syntax-case システム [5] では、もともと R6RS の syntax-rules で提供されていた健全性の強制やテンプレートベースのマッチング・出力生成を犠牲にすることなく、プログラマがこの種の変換子を書くことが可能にしている。

3. 仕様

構文抽象はたいてい (keyword subform ...) のような形式をしている。 ここで keyword は構文抽象の名前をあらわす識別子である。 各 subform の構文は構文抽象ごとに異なる。 非真正リストや単一の識別子をとることもある(3.6 節参照)が、 あまり一般的ではない。

構文抽象はキーワードを変換子とむすびつけることにより新たに定義される。 キーワードの束縛は define-syntaxlet-syntaxletrec-syntax をつかって作成される。 変換子は syntax-rules や、syntax-casesyntax で作成され、変換規則をパターンマッチングとテンプレートの再構成で記述することができる。

3.1 展開処理

構文抽象は評価の開始時に(コンパイルや解釈の前に)構文展開器により基本形式に展開される(何が基本形式がであるかは実装系依存である。展開器の出力での基本形式の表現も実装系依存である)。 展開器はプログラムのトップレベルのフォームひとつにつき一回起動する。 展開器は構文抽象を見つけると対応する変換子を起動し、構文抽象を展開して変換子の返したフォームに対して展開処理を継続する。 基本形式を見つけた場合には、その下位フォームを処理し、必要ならばその展開結果からフォームを再構成する。 展開中は識別子の束縛に関する情報を保存し、変数やキーワードの静的スコープを実現する。

展開器は、内部定義をあつかうために librarylambda の最初の部分のフォームを左から右に処理していき、フォームの種類にしたがって各フォームを処理する。

構文抽象
対応する変換子を起動して構文抽象を変換し、結果のフォームに対してこの規則を再帰的に適用する。
define-syntax フォーム
右辺式を展開・評価し、結果の変換子をキーワードに束縛する。
define フォーム
定義された識別子が変数であることを記録するが、右辺式の展開は定義がすべて処理されるまで保留する。
begin フォーム
下位フォームを現在処理中の本体フォームに挿入する。
let-syntaxletrec-syntax フォーム
内側の本体フォームを処理中の(外側の)本体フォームに挿入し、let-syntaxletrec-syntax で束縛されたキーワードが内側の本体フォームからだけ参照できるようにする。
式(定義以外)
保留していた右辺式の展開と、現在の式、本体中の残りの式の展開を実行する。

本体部分のひとつが定義(派生形式でも基本形式でも)であるキーワードが、同一本体部分の同じ定義、ないしは後続する定義で再定義された場合エラーが通知される。 このエラーを認識するために、展開器は構文抽象、define-syntaxdefinebeginlet-syntaxletrec-syntax それぞれの処理中に遭遇した定義についてキーワードを記録し、(definedefine-syntax の左辺側で)新たに定義されたキーワードを、記録したキーワードと bound-identifier=?3.7 節参照)と同様の方法でチェックする。 たとえば、以下のフォームはエラーになる。

(let ()
  (define define 17)
  define) 

(let-syntax ([def0 (syntax-rules ()
                     [(_ x) (define x 0)])])
  (let ()
    (def0 z)
    (define def0 '(def 0))
    (list z def0)))

各変数定義の右辺の展開は、定義をすべて確認し、右辺側で参照されているキーワードや変数があれば、それを局所束縛に帰着させるまで保留される。

このアルゴリズムは直接的にフォームを再処理することはしない。 必要なのは、左から右へ定義群を走査するパスと、それに続く本体部分を処理するパス(順序は不定)と保留していた右辺側を走査するパスである。

たとえば、

(lambda (x)
  (define-syntax defun
    (syntax-rules () [(_ (x . a) e) (define x (lambda a e))]))
  (defun (even? n) (or (= n 0) (odd? (- n 1))))
  (define-syntax odd? (syntax-rules () [(_ n) (not (even? n))]))
  (odd? (if (odd? x) (* x x) x)))

では、まず最初に defun の定義を見つけ、キーワード defun が対応する右辺を展開・評価した結果の変換子に対応づけられる。 次に defun の使用に遭遇すると、それを define フォームに展開する。 この define フォームの右辺の展開は保留される。 次の odd? の定義では、右辺を展開・評価した結果の変換子をキーワード odd? に対応づける。 次の部分の odd? の使用は展開され、結果としてあらわれる not は式と認識される。 これは not が変数として束縛されているからである。 この時点で、展開器は現在の式(not の呼び出し)と even? の展開を完了する。 式中の odd? の使用は、キーワード odd? に対応づけられた変換子によって展開された。 最終的な出力は以下と等価である。

(lambda (x)
  (letrec* ([even? (lambda (n)
                     (or (= n 0) (not (even? (- n 1)))))])
    (not (even? (if (not (even? x)) (* x x) x)))))

ただし出力の形式は実装系依存である。

3.2. 健全性の保持

Barendregt のラムダ計算の 健全性条件 [1] とは、別の式 M に代入される式 N があったとき、その中の自由変数が、期せずして M 中の束縛により捕捉されないことを要求する非形式的な概念である。 Kohlbecker らは、これに対応して、明示的な捕捉のない場合すべてに適用できる マクロ展開の健全性条件 を提案した[7]。 曰く、「自動生成された識別子で、展開後のプログラムで実際の束縛になるものは、同一の転写段階で生成された変数だけを束縛しなければならない。」 本文書の用語では、「生成された識別子」は変換子に渡されたフォーム中に存在せず変換子により挿入されたものを指し、「マクロ転写段階」は展開器による変換子呼び出しを指すものとする。 また、健全性条件は変数束縛だけでなく、すべての束縛に適用される。

挿入された識別子が同一の変換子呼び出しで挿入された束縛の外側に現れた場合の動作は保留されている。 このような識別子は、実際は、変換子の本体部分や、変換子の呼び出した補助手続きの(syntaxtemplate のなかの — 3.6 節参照)静的スコープを参照する。 これは、本質的には Clinger と Rees の言うところの参照透過性である [2]。

したがって、健全性条件は以下のように言い換えられる。

展開器による変換子呼び出しにより出力に挿入された識別子の束縛は同一の変換子呼び出しにより挿入された識別子の参照だけを捕捉しなければならない。 出力に挿入された識別子への参照は、挿入された識別子を取り囲むもっとも近い束縛を参照し、挿入された識別子の外側の束縛のまったくない場所にあらわれた場合には、変換子の本体部分か変換子の呼び出した補助手続きのなかの(syntaxtemplate 内部の)、もっとも近い束縛を参照する。

明示的な捕捉は datum->syntax でおこなわれる。 3.8 節参照。

展開器はマーク置換を使って健全性を保持する。 マークは変換子の出力に対して展開器が選択的に適用し、置換は束縛変数のスコープ内にあるものとして各束縛フォームに適用される。 マークは別々の段階で挿入された同名の識別子(入力にもともとあったものや、特定の変換子の呼び出しで出力に挿入されたもの)を区別するのに使われ、 置換は識別子を展開時の値に対応させるのに使われる。

展開器は構文抽象に遭遇し対応する変換子を起動すると、新規にマークを作成し、出力のうち変換子の挿入した部分にはこのマークをつけ、入力に由来する部分にはマークをつけずにおく(入力に逆マークをつけ、出力に新規のマークをつけることもある。逆マークのついた入力にマークをつけると、マークが取り消され、出力のその部分は実際にはマークのついてない入力に由来したものとしてあつかわれる)。

展開器は束縛フォームに遭遇すると一群の置換を作成し、各置換で(マークのついた)束縛識別子をその束縛情報に対応づける (lambda 式に対しては、展開器は各束縛識別子を、展開器の出力の仮引き数に対応させ、 let-syntax フォームに対しては、各束縛識別子を対応する変換子と対応させる)。 この置換は入力のうち、その束縛が参照可能な部分に対しておこなわれる。

マークと置換は展開器に処理されるフォームを層をなしてラップし、それが葉の部分にむかって必要に応じて折り重なっていく。 ラップされたフォームはラップされた構文オブジェクトとして参照される。 最終的にはこの層は識別子を表す葉の部分までのこってい、この場合ラップされた構文オブジェクトはより正確には識別子として参照される。 識別子はこのラップに加えて名前を格納している(名前は一般にシンボルで表される)。

ある識別子と展開時の値に対する置換データを作成するとき、展開時の値に加えて、識別子の名前とその識別子に適用されたマークも記録される。 展開器はもっとも最近に識別子に適用された置換 — すなわち、ラップ中の置換でいちばん外側にあり名前とマークの一致するもの — を探索して識別子の参照を解決する。 名前はそれが同一の名前である場合(シンボルを使っている場合は eq? であれば)一致し、マークは置換とともに記録されているマークが下層に現れたものと同一である場合、すなわち、そのマークが置換よりも前に適用された場合に一致する。 置換よりあとにほどこされたマーク、すなわち置換のおこなわれたのより上の層にあらわれたマークは適切なものとは見做されず無視される。

マークと置換の動作のより正確な代数的定義は Oscar Waddell の博士論文の 2.4 節を参照[9]。

3.3. キーワードの束縛

キーワードの束縛は define-syntaxlet-syntaxletrec-syntax でおこなわれる。

define-syntax定義であり、定義のあらわれるところにはどこにもあらわれることができる。

(define-syntax keyword transformer-expr)

は、展開時に transformer-expr を評価した値に束縛される。 transformer-expr の評価結果は変換子にならなければならない(3.4 節参照)。

下の例では let* を、syntax-rules をつかった変換子で構文抽象として定義する(3.10 節参照)。

(define-syntax let*
  (syntax-rules ()
    [(_ () e1 e2 ...) (let () e1 e2 ...)]
    [(_ ([i1 v1] [i2 v2] ...) e1 e2 ...)
     (let ([i1 v1])
       (let* ([i2 v2] ...) e1 e2 ...))]))

define-syntax によるキーワード束縛は、define による変数束縛と同様に、ほかの束縛で隠蔽されなければ、その現れた本体部分で参照可能である。 内部定義による束縛は、キーワード、変数を問わず、その定義群から参照可能である。 たとえば次の式

(let ()
  (define even?
    (lambda (x)
      (or (= x 0) (odd? (- x 1)))))
  (define-syntax odd?
    (syntax-rules ()
      [(_ x) (not (even? x))]))
  (even? 10))

は正当で、#t を返す。

処理が左から右に進む(3.1 節参照)ことにより、内部定義が後続のフォームが定義になるかどうかに影響を与えることがある。 例として、次の式

(let ()
  (define-syntax bind-to-zero
    (syntax-rules ()
      [(_ id) (define id 0)]))
  (bind-to-zero x)
  x)

は、let の外側の bind-to-zero の束縛によらず 0 に評価される。

let-syntaxletrec-syntaxletletrec と同等であるが、変数の代わりにキーワードを束縛する。 begin と同様、let-syntaxletrec-syntax は定義の文脈に現れることがあり、その場合このふたつは定義として扱われ、その本体部分のフォームも定義でなければならない。 また、式の文脈に現れることもでき、この場合には本体部分のフォームは式でなければならない。

は、キーワード keyword ... を、展開時に transformer-expr ... の評価結果に束縛する。 transformer-expr ... の評価結果は変換子でなければならない(3.4 節参照)。

let-syntax によるキーワード束縛は隠蔽されないかぎり let-syntax の本体部分で参照可能である。

も同様であるが、let-syntax による束縛が transformer-expr ... のなかからも参照できる。

let-syntaxletrec-syntax 中のフォームは、定義の文脈であっても式の文脈であっても、暗黙の begin で囲まれているかのように扱われる。

以下は let-syntaxletrec-syntax が異なる場合の例である。

(let ([f (lambda (x) (+ x 1))])
  (let-syntax ([f (syntax-rules ()
                    [(_ x) x])]
               [g (syntax-rules ()
                    [(_ x) (f x)])])
    (list (f 1) (g 1)))) ⇒ (1 2) 

(let ([f (lambda (x) (+ x 1))])
  (letrec-syntax ([f (syntax-rules ()
                       [(_ x) x])]
                  [g (syntax-rules ()
                       [(_ x) (f x)])])
    (list (f 1) (g 1)))) ⇒ (1 1)

違いはひとつめの式の let-syntax がふたつめでは letrec-syntax になっているだけである。 最初の式では g 中の flet で束縛された変数 f を参照しているが、ふたつめでは letrec-syntax で束縛されたキーワード f を参照しているのである。

キーワードは変数と同一の名前空間に格納される。 したがって、同一のスコープでは識別子は変数ないしキーワードに束縛され、またそのどちもにも束縛されないこともあるが、その両方であることはない。

3.4. 変換子

変換子には変換手続き変数変換子とがある。 変換手続きは入力にあたるラップされた構文オブジェクト(3.5 節参照)をひとつ引き数として受け取り、出力をあらわす構文オブジェクト3.5 節参照)を返す。 この変換手続きは対応するキーワードへの参照があると展開器から呼び出される。 キーワードがリスト形式の入力フォームの先頭部分にあらわれた場合、変換手続きはリスト構造全体を引き数として受け取り、その出力でフォーム全体を置き換える。 キーワードがほかの定義の文脈や式の文脈であらわれた場合、変換子はそのキーワードの参照だけをあらわすラップされた構文オブジェクトを受け取り、出力はその参照だけを置き換える。 キーワードが set! 式の左辺に現れた場合はエラーが通知される。

変数変換子もほとんど同じである。 ただし、変数変換子に対応するキーワードが set! の左辺にあらわれてもエラーは通知されない。 代わりに、変換子は set! 式全体をあらわすラップされた構文オブジェクトを引き数として受け取り、出力で set! 式全体を置換する。 変数変換子は、変換手続きを make-variable-transformer に渡して作成する。

(make-variable-transformer procedure)

make-variable-transformer は変換手続きを処理系依存の方法でカプセル化し、展開器がそれを変数変換子と認識できるかたちにしたものを返す。

3.5. 構文オブジェクト

構文オブジェクトは、構造に加えて文脈情報を保持した Scheme のフォームを表現したものである。 文脈情報は展開器が静的スコープを保持するのに使ったり、処理系によってはソースコードとオブジェクトの対応関係を管理するのに使われたりする。

構文オブジェクトにはラップされたものとラップされていないものがある。 ラップされた構文オブジェクト(3.2 節)は、ラップ3.2 節)と Scheme のフォームの内部表現から成る(内部表現については規定されていない。典型的には Scheme の S 式や S 式にソースコードの情報を付加したものである)。 ラップされた構文オブジェクトで識別子をあらわすもの自体を識別子と呼ぶことがある。 したがって識別子という用語は実際の構文要素(シンボル、変数、キーワード)を指すこともあるし、構文オブジェクトとしての実際の構文要素の具象表現を指すこともある。 ラップされた構文オブジェクトは他の値の型と明確に区別できる。

ラップされていない構文オブジェクトは、全体がラップされていないか、部分的にラップされていないかのどちらかである。 すなわち、その外側の層はリストとベクタから成り、葉の部分はラップされた構文オブジェクトかシンボル以外の値である。

本文書では構文オブジェクトという用語はラップされたものとラップされていないもの両方を指す。 より形式的に言うと構文オブジェクトとは、

である。 「構文オブジェクト」と「ラップされた構文オブジェクト」の区別は重要である。 たとえば、変換器により起動されたとき、変換子(3.4 節参照)はラップされた構文オブジェクトを受け取り、構文オブジェクト(ラップされていないものも含む)を返す。

3.6. 入力の構文解析と出力の生成

変換子は入力を syntax-case で分解し、出力を syntax で再構成する。

syntax-case の構文は以下の通りである。

(syntax-case expr (literal ...) clause ...)

literal は識別子であり、 clause は以下のふたつのうちのいずれかの形式でなければならない。

(pattern output-expr)
(pattern fender output-expr)

pattern は識別子、定数、または以下のいずれかである。

(pattern*)
(pattern+ . pattern)
(pattern* pattern ellipsis pattern*)
(pattern* pattern ellipsis pattern* . pattern)
#(pattern*)
#(pattern* pattern ellipsis pattern*)

pattern にあらわれる識別子は下線(_)やリテラルのリスト (literal ...)、省略記号(...)であってもかまわない。 それ以外の識別子はパターン変数である。 省略記号や下線は (literal ...) 中にはあらわれない。

パターン変数は入力中の任意の下位フォームにマッチし、入力のその部分を参照するのに使われる。 同一のパターン変数は pattern 内に二回以上現れない。

下線も任意の下位フォームにマッチするが、これはパターン変数ではなく、入力の要素を参照するのには使えない。 pattern 中に下線が複数あらわれることもある。

リテラル識別子は、入力の部分フォームが識別子で、かつ入力中にあらわれたものとリテラルのリストにあらわれたものが同一の静的束縛をもつ場合か、ふたつの識別子が等価で、どちらも静的に束縛されていない場合、かつまたその場合にかぎり、入力フォームとマッチする。

省略記号が後続するパターンは入力の要素零個以上にマッチする。

より形式的に言うと、入力フォーム F がパターン P にマッチする必要十分条件は以下の通りである。

syntax-case はまず expr を評価する。 次にその値を clause の最初の pattern から順にマッチさせていく。 このとき、マッチの必要に応じて expr の評価結果のラップをはずす。 パターンが値にマッチし、fender がなければ、 output-expr が評価され、その値が syntax-case 式の値として返される。 パターンが値にマッチしなかった場合、二番目の clause 三番目の clause と、順に試していく。 値がいずれのパターンともマッチしなかった場合にはエラーが通知される。

省略可能な fender があった場合、その節を受理するかどうか評価する追加の基準になる。 与えられた clausepattern が入力にマッチした場合、対応する fender が評価される。 fender を評価した値が真だった場合、その節は受理される。 さもなくは、パターンがマッチしなかった場合と同様、その節は棄却される。 フェンダーは論理的にはマッチング処理の一部である。 つまり、入力の基本的な構造以外の追加的な基準を指定するのである。

節中のパターン変数は、fender(もしあれば)と output-expr 中では、入力値の対応する部分に束縛される。 パターン変数は syntax 式(下記参照)のなかでだけ参照することができる。 パターン変数はプログラム中の変数やキーワードの同じ名前空間に格納される。

syntax の説明文の下の例を参照。

syntax の構文は以下の通りである。

(syntax template)

#'template(syntax template) と等価である。 略記形は読み込み時、すなわち展開時よりまえに展開形に変換される。

syntax 式は quote 式とほぼ同じである。 ただし、 (1) template 中のパターン変数の値は template に挿入される。 (2) 入力とテンプレート中の文脈情報は静的スコープ用に保持される。 (3) syntax 式の値は構文オブジェクトである。

template はパターン変数か、パターン変数でない識別子か、パターンデータか、以下のいずれかである。

subtemplatetemplate に零個以上の省略記号が続いたものである。

syntax の値は template のコピーのパターン変数の部分を入力のなかの対応する部分フォームで置き換えたものになる。 パターン変数ではないパターンデータと識別子は出力にそのままコピーされる。 省略記号の前の部分テンプレートは零個以上の部分テンプレートの出現に展開される。 このとき、部分テンプレートは省略記号の続く部分テンプレートのパターン変数を最低でもひとつは含まなければならない (そうしないと、部分フォームが出力中で何度繰り返されるのか展開器が決定できない)。 省略記号の続く部分パターンのパターン変数は(最低でも)入力と同じ数だけの省略記号の続く部分テンプレートの中にだけ現れる。 このようなパターン変数は、出力では、入力の部分フォームを束縛、分配したものに置き換えられる。 パターン変数に対応するパターンよりも多くの省略記号の続いた場合、入力は必要に応じて複製される。

(ellipsis template) 形式のテンプレートは省略記号に特別な意味がないことを除けば template と同じである。 つまり、template に含まれる省略記号はすべてふつうの識別子としてあつかわれるのである。 特に (... ...) は省略記号 ... そのものになる。 これによって省略記号を含むフォームを出力する構文抽象を書けるようになる。

syntax の出力がラップの有無は以下の規則による。

対応する部分フォームがラップされていた場合、かつまたその場合にかぎり、パターン変数の部分に挿入された入力部分フォームはラップされる。

以下の or の定義は syntax-casesyntax の実例である。 ふたつめは syntax の代わりに #' をつかっただけのひとつめとまったく同等なものである。

(define-syntax or
  (lambda (x)
    (syntax-case x ()
      [(_) (syntax #f)]
      [(_ e) (syntax e)]
      [(_ e1 e2 e3 ...)
       (syntax (let ([t e1])
                 (if t t (or e2 e3 ...))))]))) 

(define-syntax or
  (lambda (x)
    (syntax-case x ()
      [(_) #'#f]
      [(_ e) #'e]
      [(_ e1 e2 e3 ...)
       #'(let ([t e1])
           (if t t (or e2 e3 ...)))]))) 

(define-syntax case
  (lambda (x)
    (syntax-case x (else)
      [(_ e0 [(k ...) e1 e2 ...] ... [else else-e1 else-e2 ...])
       #'(let ([t e0])
           (cond
             [(memv t '(k ...)) e1 e2 ...]
             ...
             [else else-e1 else-e2 ...]))]
      [(_ e0 [(ka ...) e1a e2a ...] [(kb ...) e1b e2b ...] ...)
       #'(let ([t e0])
           (cond
             [(memv t '(ka ...)) e1a e2a ...]
             [(memv t '(kb ...)) e1b e2b ...]
             ...))])))

次の例では識別子マクロを定義している。 これはリスト構造の最初の部分に現れないキーワードへの参照を実現する構文抽象である。 ふたつめの例では make-variable-transformer を使い、キーワードが set! 式の左辺にあらわれた場合の処理をしている。

(define p (cons 4 5))
(define-syntax p.car
  (lambda (x)
    (syntax-case x ()
      [(_ . rest) #'((car p) . rest)]
      [_  #'(car p)])))
p.car ⇒ 4
(set! p.car 15) ⇒ syntax error 

(define p (cons 4 5))
(define-syntax p.car
  (make-variable-transformer
    (lambda (x)
      (syntax-case x (set!)
        [(set! _ e) #'(set-car! p e)]
        [(_ . rest) #'((car p) . rest)]
        [_  #'(car p)]))))
(set! p.car 15)
p.car           ⇒ 15
p               ⇒ (15 5)

3.10 節で述べる派生形式の identifier-syntax を使うと識別子マクロの定義を簡略化できる。

3.7. 識別子述語

述語 identifier? はある値が識別子かどうか調べるのに使う。

(identifier? x)

引き数 x が識別子、すなわち識別子をあらわす構文オブジェクトの場合 #t を返し、それ以外は #f を返す。

identifier? は、フェンダー中で入力の特定の部分フォームを検査するのに使うことが多い。 以下の、自己参照型のオブジェクトを作成する rec の定義等がその例である。

(define-syntax rec
  (lambda (x)
    (syntax-case x ()
      [(_ x e)
       (identifier? #'x)
       #'(letrec ([x e]) x)]))) 

(map (rec fact
       (lambda (n)
         (if (= n 0)                 ⇒ (1 2 6 24 120)
             1
             (* n (fact (- n 1))))))
     '(1 2 3 4 5))

(rec 5 (lambda (x) x)) ⇒ syntax error

手続き bound-identifier=?free-identifier=? は引き数に識別子をふたつとり、それが等価であれば #t を返し、さもなくは #f を返す。 これらの述語は識別子が与えられた文脈でその識別子が自由参照であるか束縛変数であるものとして、その使用意図にしたがって比較をおこなう。

手続き bound-identifier=? は、引き数の識別子の参照が束縛のスコープ内にあらわれた場合、変換子の出力中で、一方がもう一方への参照を捕捉する場合、かつまたその場合にかぎり真を返す。 一般に、ふたつの識別子が bound-identifier=? であるのは、両方がもとのプログラムにあらわれるか、両方が同一の変換子適用で(おそらくは暗黙のうちに。datum->syntax 参照)挿入された場合だけである。 実際的には、ふたつの識別子が bound-identifier=? であるとされるのは、同一の名前をもち、同一のマーク(3.2 節)がつけられている場合だけである。

bound-identifier=? は、束縛構文で識別子の重複を検出したり、束縛構文の前処理で束縛済みの識別子の検出をする場合に使うことができる。

手続き free-identifier=? は、引き数の識別子両方が変換子の挿入した束縛の外側で変換子の出力にあらわれ、ふたつが同一の束縛に帰着する場合、かつまたその場合にかぎり #t を返す(同名の識別子ふたつが束縛を参照しない場合、すなわちどちらも未束縛の場合、ふたつは同一の束縛に帰着したと考える)。 実際的には、ふたつの識別子が free-identifier=? であるとされるのは、いちばん上層にある置換が同一の束縛を指しているか(3.2 節)、識別子同士が同名で対応する置換が存在しない場合だけである。

syntax-casesyntax-rules では free-identifier=? をつかってリテラルのリストにあげられている識別子と入力中の識別子を比較する。

以下の named-let 部分をはぶいた let の定義では bound-identifier=? を使って識別子の重複を検出している。

(define-syntax let
  (lambda (x)
    (define unique-ids?
      (lambda (ls)
        (or (null? ls)
            (and (let notmem? ([x (car ls)] [ls (cdr ls)])
                   (or (null? ls)
                       (and (not (bound-identifier=? x (car ls)))
                            (notmem? x (cdr ls)))))
                 (unique-ids? (cdr ls))))))
    (syntax-case x ()
      [(_ ((i v) ...) e1 e2 ...)
       (unique-ids? #'(i ...))
       #'((lambda (i ...) e1 e2 ...) v ...)])))

unique-ids? の引き数の #'(i ...) は上の syntax の説明にある規則によりリストになる。

この let の定義により、次の式

(let ([a 3] [a 4]) (+ a a))

は構文エラー例外が送出される。一方で、

(let-syntax ([dolet (lambda (x)
                      (syntax-case x ()
                        [(_ b)
                         #'(let ([a 3] [b 4]) (+ a b))]))])
  (dolet a))

を評価すると 7 になる。 これは、識別子 adolet が挿入したものであり、入力から取り出した識別子 a とは bound-identifier=? ではないからである。

次の case の定義は 3.6 節の定義と同等である。 ただし、前の版では else をリテラルのリストに追加していたが、今回の版では free-identifier=? をつかって明示的に else を調べている。

(define-syntax case
  (lambda (x)
    (syntax-case x ()
      [(_ e0 [(k ...) e1 e2 ...] ... [else-key else-e1 else-e2 ...])
       (and (identifier? #'else-key)
            (free-identifier=? #'else-key #'else))
       #'(let ([t e0])
           (cond
             [(memv t '(k ...)) e1 e2 ...]
             ...
             [else else-e1 else-e2 ...]))]
      [(_ e0 [(ka ...) e1a e2a ...] [(kb ...) e1b e2b ...] ...)
       #'(let ([t e0])
           (cond
             [(memv t '(ka ...)) e1a e2a ...]
             [(memv t '(kb ...)) e1b e2b ...]
             ...))])))

どちらの定義でも、外側の静的束縛に else があると、else は補助キーワードとしてあつかわれない。 たとえば、

(let ([else #f])
  (case 0 [else (write "oops")]))

は構文エラーになる。 これは else が静的に束縛されてい、case の定義中の else と同一ではないからである。

3.8. 構文オブジェクトとデータの変換

手続き syntax->datum は構文オブジェクトから構文情報をすべて取り去り、対応する Scheme の「データ」を返す。

(syntax->datum syntax-object)

このとき、識別子は eq? で比較できる記号的な名前になる。 したがって、symbolic-identifier=? という手続きは次のように定義できる。

(define symbolic-identifier=?
  (lambda (x y)
    (eq? (syntax->datum x)
         (syntax->datum y))))

手続き datum->syntax は引き数として、テンプレート識別子 template-id と任意の値 datum のふたつをとる。

(datum->syntax template-id datum)

この手続きは template-id と同一の文脈情報をもつ、 datum の構文オブジェクト表現を返す。 このとき、この構文オブジェクトは datum (訳注: template-id か)が挿入されたのと同時にコードに挿入されたかのようにあつかわれる。

datum->syntax をつかうと、その識別子がもともと入力にあったかのようにあつかわれる暗黙の識別子をつくり、静的スコープの規則を「曲げる」ことができるようになる。 すなわち、入力フォームに明示的にあらわれなかった識別子に対して参照可能な束縛や参照を挿入する構文抽象を定義することができるのである。 たとえば、下に示す loop 式の定義は、この統制された識別子捕捉をつかってループの本体からの脱出手続きを変数 break に束縛している(派生形式 with-syntaxlet のようにしてパターン変数を束縛する。3.10 節参照)。

(define-syntax loop
  (lambda (x)
    (syntax-case x ()
      [(k e ...)
       (with-syntax ([break (datum->syntax #'k 'break)])
         #'(call-with-current-continuation
             (lambda (break)
               (let f () e ... (f)))))]))) 

(let ((n 3) (ls '()))
  (loop
    (if (= n 0) (break ls))
    (set! ls (cons 'a ls))
    (set! n (- n 1)))) ⇒ (a a a)

loop を以下のように定義すると、

(define-syntax loop
  (lambda (x)
    (syntax-case x ()
      [(_ e ...)
       #'(call-with-current-continuation
           (lambda (break) (let f () e ... (f))))])))

変数 breake ... 中で参照できない。

引き数 datum は下の include の定義に示すとおり、任意の Scheme フォームでよい。 これは load の展開時版である。

(define-syntax include
  (lambda (x)
    (define read-file
      (lambda (fn k)
        (let ([p (open-input-file fn)])
          (let f ([x (read p)])
            (if (eof-object? x)
                (begin (close-input-port p) '())
                (cons (datum->syntax k x)
                      (f (read p))))))))
    (syntax-case x ()
      [(k filename)
       (let ([fn (syntax->datum #'filename)])
         (with-syntax ([(exp ...) (read-file fn #'k)])
           #'(begin exp ...)))])))

(include "filename") "filename" というファイル中のフォームを下位要素にもつ begin 式に展開される。 たとえば、(define f (lambda (x) (g (* x x)))) という内容のファイル flib.ss(define g (lambda (x) (+ x x))) という内容の glib.ss というファイルがあるとき、次の式

(let ()
  (include "flib.ss")
  (include "glib.ss")
  (f 5))

を評価すると 50 になる。

include の定義では、datum->syntax をつかってファイルから読み込んだオブジェクトを、適切な字句文脈上の構文オブジェクトに変換している。 そのため、ファイル中の識別子の参照や定義は include フォームの現れた場所のスコープに入っている。

datum->syntax を使うと、健全性を完全に破壊し、昔ながらの Lisp スタイルのマクロを書くことさえできるようになる。 下で定義している lisp-transformer 手続きは入力をデータに変換する変換子を作成し、このデータに対してプログラマの定義した手続きを呼び出してその戻り値をトップレベル(より正確に言えば lisp-transformer の定義された)スコープの構文オブジェクトに戻す。

(define lisp-transformer
  (lambda (p)
    (lambda (x)
      (datum->syntax #'lisp-transformer
        (p (syntax->datum x))))))

lisp-transformer をつかって Common Lisp の defmacro の簡易版を書くのはちょうどいい練習になるだろう。

3.9. 一時識別子リストの生成

変換子中で固定個数の識別子を挿入するには、単純にそれぞれの識別子に名前をつければよい。 しかし、場合によっては挿入される識別子の個数が入力式の特徴によって決定されることもある。 たとえば、単純に letrec を定義した場合、入力中の束縛ペアと同じ個数だけ一時識別子が必要になる。 手続き generate-temporaries は一時識別子のリストを作成するのに使う。

(generate-temporaries list)

list は任意のリストないしはリスト構造を表現する構文オブジェクトである。 その内容は重要ではない。 生成される一時識別子の個数は list の要素数と同じである。 一時識別子はおのおの一意である、つまりほかのすべての識別子と異なることが保証されている。

generate-temporaries をつかった letrec の定義は次のようになる。

(define-syntax letrec
  (lambda (x)
    (syntax-case x ()
      ((_ ((i v) ...) e1 e2 ...)
       (with-syntax (((t ...) (generate-temporaries (syntax (i ...)))))
          (syntax (let ((i #f) ...)
                    (let ((t v) ...)
                      (set! i t) ...
                      (let () e1 e2 ...)))))))))

このような generate-temporaries を使った変換子は、多少読みにくくはなるものの、それを使わずに書きなおすこともできる。 再帰的に定義された中間フォームを介して各展開過程で一時識別子をひとつ生成し、一時識別子が必要なだけ生成されたところで展開を完了すればいいのである。

3.10. 派生形式と手続き

この節で述べる形式と手続きは派生型である。 すなわち、これらの形式と手続きは先の節で述べた形式や手続きをつかって定義することができる。

R5RS の syntax-rules は以下のような拡張をほどこして、派生形式としてサポートされている。

syntax-rules 形式の構文は次のとおり。

(syntax-rules (literal ...) clause ...)

literal は識別子、各 clause は以下のふたつの形式のいずれかでなければならない。

(pattern template)
(pattern fender template)

patternfendersyntax-case と、templatesyntax と同様である(3.6 節参照)。

下の or の定義は 3.6 節の定義の syntax-casesyntaxsyntax-rules でおきかえたものである。

(define-syntax or
  (syntax-rules ()
    [(_) #f]
    [(_ e) e]
    [(_ e1 e2 e3 ...)
     (let ([t e1])
       (if t t (or e2 e3 ...)))]))

変換子を生成する lambda 式は、出力を構成する syntax と同様見えなくなる。

任意の syntax-rules フォームは、明示的に lambda 式と syntax 式を書いて syntax-case で表現することができるし、次のように syntax-rulessyntax-case をつかって定義することもできる。

(define-syntax syntax-rules
  (lambda (x)
    (syntax-case x ()
      [(_ (k ...) [(_ . p) f ... t] ...)
       #'(lambda (x)
           (syntax-case x (k ...)
             [(_ . p) f ... #'t] ...))])))

より堅牢に実装するなら、リテラル k ... がすべて識別子であるか調べたり、各パターンの先頭部分が識別子であるか、各節に高々一個しかフェンダーがないか調べたりする。

syntax-rules には lambda 式と syntax があらわれないため、syntax-case で書いた同等なものよりも定義が短くなる。 どちらをつかっても書ける場合にどちらをつかうかは好みの問題であるが、syntax-case で簡単に書ける変換子が syntax-rules では簡単には書けなかったり、まったく書けなかったりすることもある。

3.6 節の p.car の定義では syntax-case をつかった識別子マクロの書き方を示した。 識別子マクロの多くは identifier-syntax 形式を使うとより簡潔に定義することができる。 identifier-syntax の構文は以下のいずれかである。

ひとつめの形式で生成された変換子をキーワードに束縛すると、その束縛のスコープ内のキーワードの参照は template で置き換えられる。

(define p (cons 4 5))
(define-syntax p.car (identifier-syntax (car p)))
p.car ⇒ 4
(set! p.car 15) ⇒ syntax error

ふたつめはより一般的で、set! がつかわれたとき何がおこるか決めることができる。

(define p (cons 4 5))
(define-syntax p.car
  (identifier-syntax
    [_ (car p)]
    [(set! _ e) (set-car! p e)]))
(set! p.car 15)
p.car           ⇒ 15
p               ⇒ (15 5)

syntax-casesyntaxmake-variable-transformer を使うと identifier-syntax は次のように定義できる。

(define-syntax identifier-syntax
  (syntax-rules (set!)
    [(_ e)
     (lambda (x)
       (syntax-case x ()
         [id (identifier? #'id) #'e]
         [(_ x (... ...)) #'(e x (... ...))]))]
    [(_ (id exp1) ((set! var val) exp2))
     (and (identifier? #'id) (identifier? #'var))
     (make-variable-transformer
       (lambda (x)
         (syntax-case x (set!)
           [(set! var val) #'exp2]
           [(id x (... ...)) #'(exp1 x (... ...))]
           [id (identifier? #'id) #'exp1])))]))

派生形式 with-syntax は、変数を束縛するのに let を使うのと同様にして、パターン変数を束縛するのに使う。 これをつかうと変換子中で出力を別個につくってまとめることができる。

with-syntax 形式の構文は以下の通りである。

patternsyntax-case のパターン部分にあたり、各 expr0 の値は計算されると対応する pattern にしたがって分解され、syntax-case の場合と同様 pattern 中のパターン変数が expr1 expr2 ... 中で束縛される。

with-syntaxsyntax-case をつかうと次のように定義できる。

(define-syntax with-syntax
  (lambda (x)
    (syntax-case x ()
      ((_ ((p e0) ...) e1 e2 ...)
       (syntax (syntax-case (list e0 ...) ()
                 ((p ...) (begin e1 e2 ...))))))))

下の cond の定義例では with-syntax をつかって、内部的に再帰をつかい出力を構築する変換子をつくる。 ここでは、cond の節のすべての場合をあつかい、可能ならば one-armed if を生成するようにしている。

(define-syntax cond
  (lambda (x)
    (syntax-case x ()
      [(_ c1 c2 ...)
       (let f ([c1 #'c1] [c2* #'(c2 ...)])
         (syntax-case c2* ()
           [()
            (syntax-case c1 (else =>)
              [(else e1 e2 ...) #'(begin e1 e2 ...)]
              [(e0) #'(let ([t e0]) (if t t))]
              [(e0 => e1) #'(let ([t e0]) (if t (e1 t)))]
              [(e0 e1 e2 ...) #'(if e0 (begin e1 e2 ...))])]
           [(c2 c3 ...)
            (with-syntax ([rest (f #'c2 #'(c3 ...))])
              (syntax-case c1 (=>)
                [(e0) #'(let ([t e0]) (if t t rest))]
                [(e0 => e1) #'(let ([t e0]) (if t (e1 t) rest))]
                [(e0 e1 e2 ...) #'(if e0 (begin e1 e2 ...) rest)]))]))])))

4. 参照実装

参照実装にはソース版(syntax.ss)とその展開版(syntax.pp)がある。 ソース版のほうが読みやすいが syntax-case を使っているので、展開版をつかってブートストラップしなければならない。

5. 今後の課題

5.1. ライブラリとのかねあい

この SRFI では提案されている R6RS ライブラリシステムとマクロシステムの兼ね合いや、変換子の実行される環境について十分にとりあつかっていない。 この問題についてはしばらく保留しておくが、我々は「構文用に」インポートされたライブラリ、あるいは変換子の評価される「メタレベル」といったもので実行環境を指定しようと考えている。

5.2. 名前の変更

我々は Chez Scheme [4]、 MzScheme [6]、そしてほかの多くの実装系で syntax-object->datumdatum->syntax-object となっている手続きについて、SRFI 72 [8] の syntax->datumdatum->syntax という名前を選択した。 SRFI 72 の名前の方が短かいからである。 これは既存のコードの大部と互換性のない変更であるが、非互換なコードを発見修正するのは簡単である。

5.3. トップレベルのキーワード束縛

本 SRFI ではトップレベルの束縛にはまったくふれていないが、 トップレベルの変数定義をみとめている処理系には、同様にトップレベルの構文定義を認めることを奨める。

5.4. 内部定義の展開

librarylambda の本体部分でのマクロ展開の操作意味論は R5RS に規定されているものよりもより詳細になり、 R5RS の 5.3 節に述べられている制約がとりはらわれた。 曰く、

マクロは、定義や構文定義がみとめられる文脈であればそういったものに展開されてもかまわないが、ある(構文)定義が構文キーワードを隠蔽し、そのとき、隠蔽されるキーワードを含むフォームが実際に定義であるか決定するときや、内部定義の場合に、内部定義とそれに続く式の境界を特定するのに、隠蔽された構文キーワードの意味が必要になると、そのような定義はエラーになる。

本文書のマクロ展開アルゴリズムでは、次の式

(let ([y 55] [z 73])
  (define foo (lambda (x) (set! y z)))
  (foo z)
  y)

を評価すると 73 になる。 しかし、R5RS の制限によると、これが下のような (foo z) を展開する foo の定義のスコープ内にあると、エラーになり、ふるまいが未定義になる。

(define-syntax foo
  (syntax-rules ()
    [(foo x) (define x 88)]))

本アルゴリズムではフォーム中の式の順序をとりかえるとエラーが通知される。 すなわち、

(let ([y 55] [z 73])
  (foo z)
  (define foo (lambda (x) (set! y z)))
  y)

このとき、定義 (foo z) を識別するためのキーワード foo の定義が後続の foo の定義で再定義されている。

本アルゴリズムでも、構文の内部定義をつかうと同じような結果になる。 たとえば、

(let ()
  (define-syntax foo
    (syntax-rules ()
      ((foo x) (define x 37))))
  (foo a)
  a)

で、(foo a) 中の foo は期待通り foo の局所束縛を参照し、式の値は 37 になるが、

(let ()
  (foo a)
  (define-syntax foo
    (syntax-rules ()
      ((foo x) (define x 37))))
  a)

では、(foo a) を定義とするキーワード foo が後ろで再定義され、エラーが通知される。

このように展開アルゴリズムを操作的に規定することが問題になる部分もあるかもしれないが、どちらも上記のふたくみの例の最初の方は期待通りの意味になり、ふたつめはエラーになる。我々は R5RS のより宣言的な意味論よりもこちらの方が望ましいと信じている。

5.5. 動的な識別子と束縛

Chez Scheme [4] や MzScheme [6]、その他多くのシステムには、(展開時に)動的に既存の構文束縛を再束縛する fluid-let-syntax 構文がある。 SRFI 72 [8] では、より一般的に動的識別子をサポートしている。 R6RS にもどちらかを含めるべきだろうか。

5.6. 展開時の環境

Chez Scheme [4] やほかの多くのシステムには、展開時の環境に任意の束縛を追加し、そのような束縛を取り出す仕組みがある。 Chez Scheme では、例えばこの機能をつかって構造体定義の情報を記録し、それを下位の構造体定義で使っている。 この機能は R6RS に含めるべきだろうか。

5.7. quasisyntax

MzScheme [6] には、quasiquoteunquoteunquote-splicing と同等の quasisyntaxunsyntaxunsyntax-splicing が、#`#,#,@ の入力構文つきで提供されている。 SRFI 72 [8] にも quasisyntax があり、こちらは unquoteunquote-splicing を多重定義し構文の追加を減らしているが、quasiquote を含む式を生成する quasisyntax が複雑になってしまっている。 どちらかを R6RS に含めるべきだろうか。

5.8. Fresh syntax

SRFI 72 [8] では、異なるふたつの syntax 形式に含まれる識別子が bound-identifier=? にならないように、syntax が新規のマークを適用することを提案している (ただし、同一の quasisyntax 内で入れ子になってあらわれた識別子は例外とする)。 我々は、3.2 節で述べた、新しいマークは変換子の出力すべてに適用されるという従来通りの意味論を選択した。 SRFI 72 の意味論を混乱させている quasisyntax の例外を置いておけば、どちらのモデルも設計領域では単純で論理的なものである。 SRFI 72 の意味論では別のライブラリで定義された変換補助手続きでそれぞれ一意な識別子の束縛を挿入することができる。 それに対して従来の意味論では、よくある場合の、マクロと変換補助手続きが完結してい、同一の名前で異なる識別子を挿入する必要のないような場合に手間がかからないようになっている。 また、あまり検討されていないが重要なこととして、SRFI 72 の意味論は潜在的に既存の syntax-case を使ったコードのほとんどと互換性がなく、影響を受けるコードを見つけるのも容易ではないということもある。

本 SRFI の generate-temporaries は、3.9 節の letrec の例で示したように一時識別子のリストを生成するのに使うのを意図したものであるが、同様に単一の識別子を生成するのに使うこともでき、ライブラリの補助手続きはそれをつかって必要に応じて各個一意な識別子を挿入することもできる。 我々はむしろ、 syntax の変形版で、一意なマークを出力に適用する fresh-syntax のようなものを考えるべきだろうか。 もしくは、より一般的に、MzScheme の make-syntax-introducer [6] のような、構文オブジェクトに同一のマークを適用する手続きを作成するものを考えるべきだろうか。 このどちらを使っても generate-temporaries は定義することができ、generate-temporaries を派生手続きと考えられるようになる。

5.9. ラップの度合

本 SRFI の構文オブジェクトのラップの仕方はふたつの極の中庸をとった。 一方の極では、すべての構文オブジェクトをラップする。 すなわち、変換器は完全にラップされた入力を変換子に渡し、変換子は完全にラップされた出力を展開器に返す、 syntax-case は完全にラップされた入力値を受け取り、 syntax は常に完全にラップされた出力値を作成する。 変換子とその補助手続きは syntax-case をつかって入力を分解し、syntax をつかって出力を作成しなければならない。 もう一方の極では、構文オブジェクトはほとんどラップされず、構文オブジェクト中にあらわれた識別子だけがラップされる。 変換子とその補助手続きは入力の分解と出力の作成に任意のリスト処理操作を使うことができる。

このどちらの極にもそれぞれ利点がある。 完全にラップする表現では、実装系が機能性や効率をかんがえて適切な内部表現を選ぶ必要がなくなる。 特に、実装系はリストやベクタに埋めこまれた識別子の、最終的には捨てられてしまう束縛情報を記録するのに、その構造を逐一たどらなくてもよいようになる。 定数をコピーしたり走査する必要がないため、定数内・定数間の(より正確にいえば、入力のうち、最終的な出力で定数として扱われる部分の)共有構造や循環構造を自由に保持することができ、 またさらに、入力の大きさと変換子が新たに追加したノードに対して線型な展開器を書くことができる。 完全にラップされた構文オブジェクトは syntax-case をつかってのみ分解できるため、syntax-case でマッチングと構文のチェック部分を自動化し、syntax-rules のような、安全で高レベルの形式の変換子のコードを書くことができる。

それに対して、ラップしない表現ではプログラマに対してより柔軟で、プログラマは map のような使いなれたリスト処理操作を変換子の入力の処理に使うことができる。

だが、この柔軟さにより行儀のわるいスタイル — 適切なマッチングをせずに変換子が入力を取り出す、または余計な入力を無視してしまう、といったようなスタイルが蔓延するようになるのは目に見えている。 これは、高レベル形式、すなわち読みやすくかつ堅牢な形式の変換子のコードを目指すという syntax-case の目標のひとつに反する。 構文のチェックは自動的におこなわれるというのが高レベル形式の神髄なのである。 このようなチェックを手で書いた低レベルのコードは退屈でほぼすべてが不完全であるといえる。

皮肉なことに、完全にラップした表現でも同じような問題が起こることがある。 完全にラップされた表現では、プログラマは MzScheme の sytax->list [6] のような手続きをつかってラップされた構文オブジェクトをリストに変換し、補助手続きを map せずにはいられなくなる。 syntax->list が適切にマッチングのおこなわれた入力に使われているのなら問題はない。 しかし、syntax->list はマッチングの行われていない任意の部分フォームに対して適用することもできるのである。

この SRFI で述べた方法では両極の中庸をとり瑕を除いて玉を得た。 syntax 形式の出力のラップに関する規則が両極の中間点のかなめである。 これにより、プログラマはリストやベクタを処理する操作を使うことができるようになるが、その対象は適切にマッチングのおこなわれたものだけである。 たとえば、変換子の入力が下記の syntax-case の入力パターンにマッチした場合、

(_ ([x e] ...) b1 b2 ...)

対応する出力式では #'(x ...)#'(e ...)#'([x e] ...)#'(b1 b2 ...) をリストとしてあつかうことができるが、 xeb1b2 の中身は、さらに syntax-case を使って分解しなければ見ることができない。

完全にラップする方法と比較すると、本手法では、潜在的な問題をはらんだ syntax->list のような補助手続きをなくしている。 また、変換子が部分的にラップされていない出力を返すことを認めたことにより、 list->syntax のような出力構成子も必要なくなった。 ほとんどラップしない方法と比較すると、本手法ではプログラマがリスト処理やベクタ処理使うことを過度に制限せず、実装系にそれほど制限をあたえていない。 また、より安全で、高レベル形式の変換子のコードを実現している。

本手法は極端な方法ほど単純ではなく、完全にラップする方法のようなまったくの実装系の自由もないが、この方法は両手法の自然で妥当な妥協点である。

註: 完全にラップされない表現を使いたい場合には、以下の、完全にラップされた表現をラップされない表現に変換する手続きを定義し使用することができる。

(define syntax->sexpr
  (lambda (x)
    (syntax-case x ()
      [(a . d) (cons (syntax->sexpr #'a) (syntax->sexpr #'d))]
      [#(a ...)
       (list->vector
         (map syntax->sexpr (syntax->sexpr #'(a ...))))]
      [_ (if (identifier? x) x (syntax->datum x))]))) 

(define sexpr->syntax
  (lambda (x)
    (cond
      [(pair? x)
       (with-syntax ([a (sexpr->syntax (car x))]
                     [d (sexpr->syntax (cdr x))]) 
         #'(a . d))]
      [(vector? x)
       (with-syntax ([(x ...) (map sexpr->syntax (vector->list x))])
         #'#(x ...))]
      [else (if (identifier? x) x (datum->syntax #'* x))])))

これらの操作を透過的におこなう define-syntaxmake-variable-transformersyntaxexport するライブラリは読者の宿題としておく。

ただし、入力のうち必要な部分だけを走査し、マッチした部分だけを S 式としてあつかう方が変換子にとって明確で効率的ではある。

6. Acknowledgments

This SRFI was written in consultation with the full set of R6RS editors: Will Clinger, Kent Dybvig, Matthew Flatt, Michael Sperber, and Anton van Straaten.

Much of this document has been copied from or adapted from Chapter 10 of the Chez Scheme Version 7 User's Guide [4], some of which also appears in Chapter 8 of The Scheme Programming Language, 3rd edition [3].

7. References

8. Copyright

Copyright © R. Kent Dybvig (2006). 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.

附: 訳語対照表

antimark
逆マーク
body
本体(部分)
capture
捕捉
core form
基本形式
derived form
派生形式
expander
展開器
fender
フェンダー
fluid identifier
動的な識別子
form
フォーム、形式
high-level style
高レベルの形式
hygiene
健全性
identifier macro
識別子マクロ
implementation
実装系、処理系
internal definition
内部定義
lexical binding
静的束縛
mark
マーク
reader syntax
入力構文
subform
部分形式
substitution
置換
syntax abstraction
構文抽象
syntax object
構文オブジェクト
transcription step
転写段階
transformation procedure
変換手続き
transformer
変換子
unwrapped syntax object
ラップされていない構文オブジェクト
variable transformer
変数変換子
wrap
ラップ
wrapped syntax object
ラップされた構文オブジェクト