RacketのMacroを調べてみる (11)
ではSyntax: Meta-Programming Helpersを見ていきます
1 Parsing and Specifying Syntax
- ここでは
syntax-parse
とdefine-syntax-class
を説明していく syntax-parse
はsyntax patternにしたがって自動的にエラーメッセージを生成する
1.1 Introduction
syntax-parse
とsyntax classを使って堅固なマクロの書き方を紹介するlet
を題材とする
最も単純には以下のように書けます
(define-syntax-rule (mylet ([var rhs] ...) body ...)
((lambda (var ...) body ...) rhs ...))
しかしちゃんと動くときはいいんですが
- 間違ったときはどこでどう間違ったのかわかりづらい
- 最悪、エラーにならずおかしな動きをする可能性がある
- マクロは構文を検証し、適切なメッセージを表示するべき
- 読みやすく維持しやすいコードで機械的にチェックされるのはマクロ作成者にも利点
このあと細かく段階を追ってひとつずつsyntax-parse
の機能を紹介し、
それがどういうエラーをチェックしてどんなメッセージを出すか示しながら進みます
まずは基本
; syntax-parse関連をインポートする
(require (for-syntax syntax/parse))
(define-syntax (mylet stx)
; まずはsyntax-caseと同じような感じで書く
(syntax-parse stx
; パターン変数をsyntax classでアノテートすることによって構文をチェックする
; デフォルトでは識別子を表すid、式を表すexpr(など?)が利用できる
; ここでは、varは識別子でrhsは式ですよ、ってこと
; `...+`は1個以上の繰り返し
[(_ ([var:id rhs:expr] ...) body ...+)
#'((lambda (var ...) body ...) rhs ...)]))
これでvarが識別子かどうかを調べてダメなら"mylet: expected identifier"みたいな
エラーを出してくれるようになります
次はsyntax classの定義です
(define-syntax (mylet stx)
; カスタムsyntax classの定義
(define-syntax-class binding
; syntax-classの説明(エラーメッセージで使われる)
#:description "binding pair"
; "binding"とは(識別子 式)という形をしたもの
(pattern (var:id rhs:expr)))
(syntax-parse stx
; カスタムsyntax classを使ってアノテーション
[(_ (b:binding ...) body ...+)
; パターン変数.属性名という形で参照する
#'((lambda (b.var ...) body ...) b.rhs ...)]))
これで、"mylet: expected binding pair"のようなエラーメッセージが
出るようになります
これはなんだかいい感じがしてきました
さらに、letで割り当てる識別子が一意でないといけないという制約を入れるために
もうひとつsyntax classを定義します
(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"
; bはbindingであり・・・
(pattern (b:binding ...)
; さらに、一意でなければならない
; エラーメッセージも指定する
#:fail-when (check-duplicate-identifier
(syntax->list #'(b.var ...)))
"duplicate variable name"
; 属性名をb.varと書く代わりにvarと書けるようにする
#:with (var ...) #'(b.var ...)
#:with (rhs ...) #'(b.rhs ...)))
(syntax-parse stx
; bsはdistinct-bindings
[(_ bs:distinct-bindings body ...+)
; 上で#:withしてないと、bs.b.varなどと書かなければいけない
#'((lambda (bs.var ...) body ...) bs.rhs ...)]
; 名前付きletも使えるようにする
[(_ loop:id bs:distinct-bindings body ...+)
#'(letrec ([loop (lambda (bs.var ...) body ...)])
(loop bs.rhs ...))]))
確かにsyntax-case
と比べて構文チェックはかなり強力になってる感じですね