--- title: syntax-parse Introduction tags: syntax-parse slideOptions: theme: white slideNumber: 'c/t' center: false transition: 'none' keyboard: true width: '93%' height: '100%' --- <style> /* basic design */ .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6, .reveal section, .reveal table, .reveal li, .reveal blockquote, .reveal th, .reveal td, .reveal p { font-family: 'Meiryo UI', 'Source Sans Pro', Helvetica, sans-serif, 'Helvetica Neue', 'Helvetica', 'Arial', 'Hiragino Sans', 'ヒラギノ角ゴシック', YuGothic, 'Yu Gothic'; text-align: left; line-height: 1.8; letter-spacing: normal; text-shadow: none; word-wrap: break-word; color: #444; } .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5 {font-weight: normal;}, .reveal h6 {font-weight: bold;} .reveal h1, .reveal h2, .reveal h3 {color: #2980b9;} .reveal th {background: #DDD;} .reveal section img {background:none; border:none; box-shadow:none; max-width: 95%; max-height: 95%;} .reveal blockquote {width: 90%; padding: 0.5vw 3.0vw;} .reveal table {margin: 1.0vw auto;} .reveal code {line-height: 1.2;} .reveal p, .reveal li {padding: 0vw; margin: 0vw;} .reveal .box {margin: -0.5vw 1.5vw 2.0vw -1.5vw; padding: 0.5vw 1.5vw 0.5vw 1.5vw; background: #EEE; border-radius: 1.5vw;} /* table design */ .reveal table {background: #f5f5f5;} .reveal th {background: #444; color: #fff;} .reveal td {position: relative; transition: all 300ms;} .reveal tbody:hover td { color: transparent; text-shadow: 0 0 3px #aaa;} .reveal tbody:hover tr:hover td {color: #444; text-shadow: 0 1px 0 #fff;} /* blockquote design */ .reveal blockquote { width: 90%; padding: 0.5vw 0 0.5vw 6.0vw; font-style: italic; background: #f5f5f5; } .reveal blockquote:before{ position: absolute; top: 0.1vw; left: 1vw; content: "\f10d"; font-family: FontAwesome; color: #2980b9; font-size: 3.0vw; } /* font size */ .reveal h1 {font-size: 5.0vw;} .reveal h2 {font-size: 3.0vw;} .reveal h3 {font-size: 2.8vw;} .reveal h4 {font-size: 2.6vw;} .reveal h5 {font-size: 1.8vw;} .reveal h6 {font-size: 2.2vw;} .reveal section, .reveal table, .reveal li, .reveal blockquote, .reveal th, .reveal td, .reveal p {font-size: 2.2vw;} .reveal code {font-size: 1.6vw;} /* new color */ .red {color: #EE6557;} .blue {color: #16A6B6;} /* split slide */ #right {left: -18.33%; text-align: left; float: left; width: 50%; z-index: -10;} #left {left: 31.25%; text-align: left; float: left; width: 50%; z-index: -10;} </style> <style> /* specific design */ .reveal h1 { margin: 0% -100%; padding: 2% 100% 4% 100%; color: #fff; background: #fffa5e; /* fallback for old browsers */ background: linear-gradient(-45deg, #f7f439, #54ffa4, #23A6D5, #238ed5); background-size: 200% 200%; animation: Gradient 60s ease infinite; } @keyframes Gradient { 0% {background-position: 0% 50%} 50% {background-position: 100% 50%} 100% {background-position: 0% 50%} } .reveal h2 { text-align: center; margin: -5% -50% 2% -50%; padding: 3% 10% 1% 10%; color: #fff; background: #fffa5e; /* fallback for old browsers */ background: -webkit-linear-gradient(to right, #c74646, #fffa5e); /* Chrome 10-25, Safari 5.1-6 */ background: linear-gradient(to right, #c74646, #fffa5e); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ } .reveal h4 { text-align: center; } </style> --- # syntax-parseについて <br> syntax/parseライブラリは、マクロを書いて構文を処理するためのフレームワークを提供しています。このライブラリは、強力な構文パターン言語を提供しており、パターンマッチ形式の syntax-parse や、指定形式の define-syntax-class で使用されます。syntax-parseを使用するマクロは、マクロの構文パターンに埋め込まれた説明やメッセージに基づいて、エラーメッセージを自動的に生成します。 --- ## Introduction このセクションでは、syntax-parseとsyntaxクラスを使って堅牢なマクロを書く方法を紹介します。 --- 実行例として、次のタスクを使用します。Racket の let フォームと同じ構文と動作を持つ mylet という名前のマクロを記述します。このマクロは、間違って使用された場合に適切なエラーメッセージを生成する必要があります。 mylet の構文の仕様は次のとおりです。 ```racket (mylet ([var-id rhs-expr] ...) body ...+) (mylet loop-id ([var-id rhs-expr] ...) body ...+) ``` 簡単にするために、ここでは第1のケースだけを扱います。2番目のケースについては、後ほど紹介します。 マクロは、define-syntax-rule を使用して非常に簡単に実装できます。 ``` (define-syntax-rule (mylet ([var rhs] ...) body ...) ((lambda (var ...) body ...) rhs ...) ``` 正しく使用された場合、マクロは動作しますが、エラーが発生した場合の動作は非常に悪くなります。一部のケースでは、マクロは情報のないエラー メッセージを表示して単に失敗します。他のケースでは、不正な構文を平然と受け入れて lambda に渡し、奇妙な結果になります。 ``` > (mylet ([a 1] [b 2]) (+ a b)) 3 > (mylet (b 2) (sub1 b)) mylet: use does not match pattern: (mylet ((var rhs) ...) body ...) in: (mylet (b 2) (sub1 b)) > (mylet ([1 a]) (add1 a)) lambda: not an identifier, identifier with default, or keyword at: 1 in: (lambda (1) (add1 a)) > (mylet ([#:x 1] [y 2]) (* x y)) eval:1:0: arity mismatch; the expected number of arguments does not match the given number expected: 0 plus an argument with keyword #:x given: 2 arguments...: 1 2 ``` これらの不正な構文の例は、典型的なプログラマーがmyletを使おうとしてこのような間違いを犯すことを示唆するものではありません。少なくとも、頻繁にではなく、最初の学習の後でもないでしょう。しかし、マクロは経験の浅いプログラマーや、他のマクロ(またはコードジェネレータ)のターゲットとしても使用され、多くのマクロはmyletよりもはるかに複雑なものです。マクロは、その構文を検証し、適切なエラーを報告しなければなりません。さらに、マクロ作成者は、機械的にチェックされた構文の仕様から、より読みやすく保守性の高いコードという形で恩恵を受けます。 syntax-parse を使用して、マクロのエラー動作を改善することができます。まず,マクロトランスフォーマの実装に syntax-parse を使用するので,トランスフォーマ環境に syntax-parse をインポートします. ``` > (require (for-syntax syntax/parse)) ``` 以下は、上記の構文仕様を syntax-parse マクロ定義に音訳したものです。上記のdefine-syntax-ruleを使用したバージョンと同じように動作します。 ``` > (define-syntax (mylet stx) (syntax-parse stx [(_ ([var-id rhs-expr] ...) body ...+) #'((lambda (var-id ...) body ...) rhs-expr ...)])) ``` ...は直前のパターンの0回以上の繰り返しに一致することを意味し、...+は1回以上の繰り返しに一致することを意味します。ただし、テンプレートでは ... のみを使用することができます。 検証と高品質なエラー報告のための最初のステップは、マクロの各パターン変数に、許容される構文を記述する構文クラスをアノテーションすることです。mylet では、各変数は識別子 (略して id) であり、各右辺は expr (式) でなければなりません。アノテーションされたパターン変数は、パターン変数名、コロン文字、および構文クラス名を連結して記述されます。 * "colon" syntaxの代わりに、「~var」パターンフォームを参照してください。 ``` > (define-syntax (mylet stx) (syntax-parse stx [(_ ((var:id rhs:expr) ...) body ...+) #'((lambda (var ...) body ...) rhs ...)])) ``` 構文クラス(syntax class)の注訳はテンプレートに表示されないことに注意してください (つまり、var:id ではなく var です)。 構文クラス(syntax class)の注訳は、マクロを使用する際にチェックされます。 ``` > (mylet ([a 1] [b 2]) (+ a b)) 3 > (mylet (["a" 1]) (add1 a)) mylet: expected identifier at: "a" in: (mylet (("a" 1)) (add1 a)) ``` expr構文クラスは、マッチした項が有効な式であるかどうかを実際にはチェックしません。それには、マクロエクスパンダを呼び出す必要があります。その代わり、expr はキーワードではないことを意味します。 ``` > (mylet ([a #:whoops]) 1) mylet: expected expression at: #:whoops in: (mylet ((a #:whoops)) 1) ``` また、syntax-parseは、いくつかの種類のエラーを何の助けもなく報告する方法を知っています。 ``` > (mylet ([a 1 2]) (* a a)) mylet: unexpected term at: 2 in: (mylet ((a 1 2)) (* a a)) ``` しかし、他の種類のエラーもあり、このマクロは優雅に処理できません。 ``` > (mylet (a 1) (+ a 2)) mylet: bad syntax in: (mylet (a 1) (+ a 2)) ``` マクロが "この式は(a 1)の周りに括弧のペアがない "と反応するのは無理があります。パターン マッチャーはそこまで賢くありません。パターンマッチャーはそこまで賢くはありませんが、エラーの原因を特定することができます。それは、aに遭遇したとき、「結合ペア」と呼ばれるものを期待していたのですが、その用語はまだマクロの語彙にはありません。 syntax-parseがより良いエラーを合成するためには、私たちが個別の構文カテゴリとして認識しているパターンに説明を付けなければなりません。そのための一つの方法は、新しい構文クラスを定義することです。 * もう一つの方法は、「~describe pattern」というフォームです。 ``` > (define-syntax (mylet stx) (define-syntax-class binding #:description "binding pair" (pattern (var:id rhs:expr))) (syntax-parse stx [(_ (b:binding ...) body ...+) #'((lambda (b.var ...) body ...) b.rhs ...)])) ``` ここで、b.varとb.rhsと書いていることに注意してください。これは、アノテーションされたパターン変数bと、構文クラスbindingの属性varとrhsから形成される入れ子の属性です。 これで、エラーメッセージは「バインディング・ペア」について語ることができます。 ``` > (mylet (a 1) (+ a 2)) mylet: expected binding pair at: a in: (mylet (a 1) (+ a 2)) ``` エラーが発生した場合は、可能な限りより具体的な表現で報告されます。 ``` > (mylet (["a" 1]) (+ a 2)) mylet: expected identifier at: "a" in: (mylet (("a" 1)) (+ a 2)) parsing context: while parsing binding pair term: ("a" 1) location: eval:16:0 ``` myletの合法的な構文には、もうひとつ制約があります。異なる結合ペアによって結合される変数は、それぞれ異なるものでなければなりません。そうでない場合、マクロは不正なラムダ形式を作成します。 ``` > (mylet ([a 1] [a 2]) (+ a a)) lambda: duplicate argument name at: a in: (lambda (a a) (+ a a)) ``` 識別性の要求などの制約は、側面条件として表現されます。 ``` > (define-syntax (mylet stx) (define-syntax-class binding #:description "binding pair" (pattern (var:id rhs:expr))) (syntax-parse stx [(_ (b:binding ...) body ...+) #:fail-when (check-duplicate-identifier (syntax->list #'(b.var ...))) "duplicate variable name" #'((lambda (b.var ...) body ...) b.rhs ...)])) > (mylet ([a 1] [a 2]) (+ a a)) mylet: duplicate variable name at: a in: (mylet ((a 1) (a 2)) (+ a a)) ``` fail-when キーワードの後には、条件とエラーメッセージの 2 つの式が続きます。条件が #f 以外の値で評価された場合、パターンは失敗します。さらに、条件が構文オブジェクトとして評価された場合、その構文オブジェクトは失敗の原因を特定するために使用されます。 シンタックスクラスにもサイドコンディションを持たせることができます。以下のマクロは、"異なる結合ペアのシーケンス "を表す別の構文クラスを含むように書き換えられています。 ``` > (define-syntax (mylet stx) (define-syntax-class binding #:description "binding pair" (pattern (var:id rhs:expr))) (define-syntax-class distinct-bindings #:description "sequence of distinct binding pairs" (pattern (b:binding ...) #:fail-when (check-duplicate-identifier (syntax->list #'(b.var ...))) "duplicate variable name" #:with (var ...) #'(b.var ...) #:with (rhs ...) #'(b.rhs ...))) (syntax-parse stx [(_ bs:distinct-bindings . body) #'((lambda (bs.var ...) . body) bs.rhs ...)])) ``` ここでは、#:with句を紹介します。with節は、パターンと計算された用語をマッチさせます。ここでは、var と rhs を distinct-bindings の属性としてバインドするために使用しています。デフォルトでは、構文クラスはそのパターンのパターン変数のみを属性としてエクスポートし、ネストした属性はエクスポートしません。 * 別の方法としては、#:attribute オプションを使用して、入れ子になった属性 b.var と b.rhs を含むように distinct-bindings の属性を明示的に宣言します。この場合、マクロは bs.b.var および bs.b.rhs を参照します。 しかし、今のところ、このマクロは Racket の let が提供する機能の半分しか実装していません。named-let」形式を追加する必要があります。これは、新しい節を追加するだけの簡単なことだとわかりました。 ``` > (define-syntax (mylet stx) (define-syntax-class binding #:description "binding pair" (pattern (var:id rhs:expr))) (define-syntax-class distinct-bindings #:description "sequence of distinct binding pairs" (pattern (b:binding ...) #:fail-when (check-duplicate-identifier (syntax->list #'(b.var ...))) "duplicate variable name" #:with (var ...) #'(b.var ...) #:with (rhs ...) #'(b.rhs ...))) (syntax-parse stx [(_ bs:distinct-bindings body ...+) #'((lambda (bs.var ...) body ...) bs.rhs ...)] [(_ loop:id bs:distinct-bindings body ...+) #'(letrec ([loop (lambda (bs.var ...) body ...)]) (loop bs.rhs ...))])) ``` distinct-bindings構文クラスを再利用することができるので、「named-let」構文の追加は3行で済みます。 しかし、この新しいケースを追加することで、syntax-parseがエラーを特定して報告する機能に影響はないのでしょうか? ``` > (mylet ([a 1] [b 2]) (+ a b)) 3 > (mylet (["a" 1]) (add1 a)) mylet: expected identifier at: "a" in: (mylet (("a" 1)) (add1 a)) parsing context: while parsing binding pair term: ("a" 1) location: eval:23:0 while parsing sequence of distinct binding pairs term: (("a" 1)) location: eval:23:0 > (mylet ([a #:whoops]) 1) mylet: expected expression at: #:whoops in: (mylet ((a #:whoops)) 1) parsing context: while parsing binding pair term: (a #:whoops) location: eval:24:0 while parsing sequence of distinct binding pairs term: ((a #:whoops)) location: eval:24:0 > (mylet ([a 1 2]) (* a a)) mylet: unexpected term at: 2 in: (mylet ((a 1 2)) (* a a)) parsing context: while parsing binding pair term: (a 1 2) location: eval:25:0 while parsing sequence of distinct binding pairs term: ((a 1 2)) location: eval:25:0 > (mylet (a 1) (+ a 2)) mylet: expected binding pair at: a in: (mylet (a 1) (+ a 2)) parsing context: while parsing sequence of distinct binding pairs term: (a 1) location: eval:26:0 > (mylet ([a 1] [a 2]) (+ a a)) mylet: duplicate variable name at: a in: (mylet ((a 1) (a 2)) (+ a a)) parsing context: while parsing sequence of distinct binding pairs term: ((a 1) (a 2)) location: eval:27:0 ``` 元の構文のエラー報告はそのままのようです。named-let構文が動作していること、syntax-parseが単にその節を無視していないことを確認する必要があります。 ``` > (mylet loop ([a 1] [b 2]) (+ a b)) 3 > (mylet loop (["a" 1]) (add1 a)) mylet: expected identifier at: "a" in: (mylet loop (("a" 1)) (add1 a)) parsing context: while parsing binding pair term: ("a" 1) location: eval:29:0 while parsing sequence of distinct binding pairs term: (("a" 1)) location: eval:29:0 > (mylet loop ([a #:whoops]) 1) mylet: expected expression at: #:whoops in: (mylet loop ((a #:whoops)) 1) parsing context: while parsing binding pair term: (a #:whoops) location: eval:30:0 while parsing sequence of distinct binding pairs term: ((a #:whoops)) location: eval:30:0 > (mylet loop ([a 1 2]) (* a a)) mylet: unexpected term at: 2 in: (mylet loop ((a 1 2)) (* a a)) parsing context: while parsing binding pair term: (a 1 2) location: eval:31:0 while parsing sequence of distinct binding pairs term: ((a 1 2)) location: eval:31:0 > (mylet loop (a 1) (+ a 2)) mylet: expected binding pair at: a in: (mylet loop (a 1) (+ a 2)) parsing context: while parsing sequence of distinct binding pairs term: (a 1) location: eval:32:0 > (mylet loop ([a 1] [a 2]) (+ a a)) mylet: duplicate variable name at: a in: (mylet loop ((a 1) (a 2)) (+ a a)) parsing context: while parsing sequence of distinct binding pairs term: ((a 1) (a 2)) location: eval:33:0 ``` syntax-parseはどのようにしてプログラマが試みた節を決定し、それをエラー報告の基準として使用するのでしょうか?結局のところ、named-let構文の悪い使い方は、通常の構文の悪い使い方でもあり、その逆もまた然りです。しかし、このマクロは "mylet: expected sequence of distinct binding pairs at: loop. "のようなエラーを出しません。 その答えは、syntax-parseが、すべての潜在的なエラー(distinct-bindingにマッチしないloopのようなものを含む)のリストと、各エラーの前に行われた進捗状況を記録するからです。最も進展したエラーだけが報告されます。 たとえば,このマクロの悪い使い方では,次のようになります. ``` > (mylet loop (["a" 1]) (add1 a)) mylet: expected identifier at: "a" in: (mylet loop (("a" 1)) (add1 a)) parsing context: while parsing binding pair term: ("a" 1) location: eval:34:0 while parsing sequence of distinct binding pairs term: (("a" 1)) location: eval:34:0 ``` ここでは2つのエラーが発生する可能性があります:loopで期待されるdistinct-bindingsと "a "で期待されるidentifierです。2つ目のエラーは、1つ目のエラーよりも用語の中で発生しているため、報告されます。 別の例として、この用語を考えてみましょう。 ``` > (mylet (["a" 1]) (add1 a)) mylet: expected identifier at: "a" in: (mylet (("a" 1)) (add1 a)) parsing context: while parsing binding pair term: ("a" 1) location: eval:35:0 while parsing sequence of distinct binding pairs term: (("a" 1)) location: eval:35:0 ``` ここでも2つのエラーが発生する可能性があります: expected identifier at (["a" 1]) と expected identifier at "a" です。どちらも第2項(あるいは第1引数)で発生しますが、2番目のエラーは項の中でより深く発生します。進捗状況は、構文の左から右へのトラバーサルに基づいています。 最後の例として、次のようなものを考えてみましょう。 ``` > (mylet ([a 1] [a 2]) (+ a a)) mylet: duplicate variable name at: a in: (mylet ((a 1) (a 2)) (+ a a)) parsing context: while parsing sequence of distinct binding pairs term: ((a 1) (a 2)) location: eval:36:0 ``` 重複した変数名([a 1] [a 2])と、期待される識別子([a 1] [a 2])という2つのエラーです。syntax-parseでは、重複エラーメッセージに関連する進捗状況は、aの2回目の出現ではなく、2番目の項(最初の引数)であることに注意してください。これは、チェックがdistinct-bindingsパターン全体に関連しているためです。両方のエラーの進捗状況が同じであるにもかかわらず、最初のエラーだけが報告されているように見えます。この2つの違いは、1つ目のエラーがポストトラバーサルチェックによるものであるのに対し、2つ目のエラーは通常の(つまりプレトラバーサル)チェックによるものだということです。ポスト・トラバーサル・チェックは、同じタームのプレ・トラバーサル・チェックよりも進歩していると考えられます。実際、ターム内のどの失敗よりも進歩しています。 しかし、同じ進捗度でも、複数の潜在的なエラーが発生する可能性があります。以下はその一例です。 ``` > (mylet "not-even-close") mylet: expected identifier or expected sequence of distinct binding pairs at: "not-even-close" in: (mylet "not-even-close") ``` この場合、syntax-parseは両方のエラーを報告します。 マクロにアノテーションを追加しても、この例のようにsyntax-parseのエラー報告機能を無視した誤用もあります。 ``` > (mylet) mylet: expected more terms at: () within: (mylet) in: (mylet) ``` syntax-parseの考え方は、このような状況では、「bad syntax」のような一般的なエラーが正当化されるというものです。ここでの mylet の使用は非常に大きく外れているため、唯一有益なエラーメッセージには mylet の構文を完全に再現することが含まれます。しかし、それはエラーメッセージの役割ではなく、ドキュメントの役割です。 このセクションでは、構文クラス、サイドコンディション、進行順のエラー報告について紹介しました。しかし、syntax-parseにはもっと多くの機能があります。他の機能のサンプルを実際のコードで見るには「例題」のセクションに進んでください。また、完全なリファレンス・ドキュメントを見るには次のセクションに進んでください。 [next section](https://hackmd.io/6u9DEOjVSoS-Cos9tT-pxw)