--- title: section4 tags: macro --- ### 4 パターンマッチング: syntax-case と syntax-rules ほとんどの有用な構文変換ツールは、入力された構文を受け取り、その断片を別のものに並べ替えることで動作します。これは cadddr のようなリストアクセサを使えば可能ですが、退屈です。パターンマッチを行うために match を使用する方がより便利で、エラーが起こりにくいです。 > 歴史的には、シンタックスケースとシンタックスルールのパターンマッチが先に行われ、matchは後にRacketに追加されました。 パターン マッチングは、Racket のマクロ システムに追加された最初の改良点の 1 つであることがわかりました。これは syntax-case と呼ばれ、define-syntax-rule と呼ばれる単純な状況のための略記法があります。 先ほどの例を思い出してください。 ```ocaml= (require (for-syntax racket/match)) (define-syntax (our-if-using-match-v2 stx) (match (syntax->list stx) [(list _ condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) ``` syntax-caseを使ってみるとこうなります。 ```ocaml= > (define-syntax (our-if-using-syntax-case stx) (syntax-case stx () [(_ condition true-expr false-expr) #'(cond [condition true-expr] [else false-expr])])) > (our-if-using-syntax-case #t "true" "false") "true" ``` かなり似ていますよね?パターンマッチの部分はほとんど同じように見えます。新しい構文を指定する方法は、よりシンプルです。準クオートやアンクオートは必要ありません。datum->syntaxを使う必要もありません。代わりに、パターンの変数を使用する「テンプレート」を用意します。 単純なパターンマッチのケースのために、syntax-caseに展開する略記法があります。これはdefine-syntax-ruleと呼ばれています。 ```ocaml= > (define-syntax-rule (our-if-using-syntax-rule condition true-expr false-expr) (cond [condition true-expr] [else false-expr])) > (our-if-using-syntax-rule #t "true" "false") "true" ``` define-syntax-ruleについては次のような点があります。あまりにもシンプルなので、define-syntax-ruleはマクロについて最初に教えられることが多いのです。しかし、それはほとんど欺瞞的なほど簡単です。これは通常の実行時の関数を定義するように見えますが、そうではありません。実行時ではなく、コンパイル時に動作しているのです。さらに悪いことに、define-syntax-ruleが処理できる以上のことをしたいと思った瞬間に、複雑で混乱した領域に崖から落ちてしまう可能性があります。願わくば、私たちは基本的な構文変換器から始めて、そこから発展させてきたので、そのような問題は起こらないでしょう。define-syntax-ruleは便利な略記として評価できますが、その略記であるものを怖がったり、混乱したりすることはありません。 私が見つけたマクロを学ぶための資料のほとんどは、Racket Guideを含め、パターンやテンプレートがどのように機能するかを非常によく説明しています。そのため、ここではその説明は省略します。 時には、パターンやテンプレートよりも一歩進んだ使い方をする必要があります。いくつかの例を見て、どのように混乱するか、そしてどうすればうまくいくかを考えてみましょう。 --- ### 4.1 パターン変数とテンプレートの戦い 例えば、a-bというハイフンでつながれた名前の関数を定義したいが、aとbの部分は別々に用意するとします。(struct foo (field1 field2)) は、foo-field1、foo-field2、foo?など、fooという名前のバリエーションを持つ多くの関数を自動的に定義します。 では、そのようなことをしていると仮定してみましょう。(hyphen-define a b (args) body) という構文を (define (a-b args) body) という構文に変換したいのです。 間違った最初の試みは ```ocaml= > (define-syntax (hyphen-define/wrong1 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (let ([name (string->symbol (format "~a-~a" a b))]) #'(define (name args ...) body0 body ...))])) eval:47:0: a: pattern variable cannot be used outside of a template in: a ``` はぁ。このエラーメッセージが何を意味するのか,さっぱりわかりません.では、解決してみましょう。エラーメッセージが言っている「テンプレート」とは、#'(define (name args ...) body0 body ...)の部分です。letはそのテンプレートの一部ではありません。letの部分にa(またはb)を使うことはできないようですね。 実は、syntax-caseには、好きなだけテンプレートを持つことができます。明らかに必要なテンプレートは、出力シンタックスを供給する最終式です。しかし、パターン変数に syntax (別名 #') を使うことができます。これは、「お楽しみサイズ」の小さなテンプレートですが、もうひとつのテンプレートになります。それを試してみましょう。 ```ocaml= > (define-syntax (hyphen-define/wrong1.1 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (let ([name (string->symbol (format "~a-~a" #'a #'b))]) #'(define (name args ...) body0 body ...))])) ``` エラーがなくなってよかったですね。では、実際に使ってみましょう。 ```ocaml= > (hyphen-define/wrong1.1 foo bar () #t) > (foo-bar) foo-bar: undefined; cannot reference an identifier before its definition in module: 'program ``` どうやらこのマクロは foo-bar 以外の名前の関数を定義しているようです。はぁ。 ここで、DrRacketのMacro Stepperが威力を発揮します。 > Emacsを使うことが多い人でも、Macro Stepperのために一時的にDrRacketを使う価値は十分にあると思います。  Macro Stepperは、私たちのマクロの使用を `(hyphen-define/wrong1.1 foo bar () #t)` に展開されます。 `(define (name) #t)` なるほど、それなら納得です。そうではなく、次のように展開したかったのです。 (define (foo-bar) #t) テンプレートではシンボル name を使用していますが、このマクロでは foo-bar のような値が必要でした。 このように、テンプレートの中で変数を使うとその値が得られるような動作をするものは、すでにありますか?はい、パターン変数です。私たちのパターンにはnameが含まれていませんが、これは元々の構文にnameが含まれていることを期待していないからで、このマクロの目的はnameを作成することです。つまり、nameはメインパターンには含まれないのです。それでは、追加のパターンを作成しましょう。追加のネストされたシンタックスケースを使用して行うことができます。 ```ocaml= > (define-syntax (hyphen-define/wrong1.2 stx)) (syntax-case stx () [(_ a b (args ...) body0 body ...) (syntax-case (datum->syntax #'a (string->symbol (format "~a-~a" #'a #'b))) () [name #'(define (name args ...) body0 body ...)]])) ``` 変だと思いますか?深呼吸してみましょう。通常、変換関数はRacketから構文を与えられ、その構文をsyntax-caseに渡します。しかし、その場で独自の構文を作成して、それをsyntax-caseに渡すこともできます。ここでやっているのはそれだけです。(datum->syntax ...)式全体が、その場で作成したシンタックスです。これをsyntax-caseに渡して、nameというパターン変数を使ってマッチさせます。ほら、新しいパターン変数のできあがりです。これをテンプレートで使うと、その値がテンプレートに入ります。 あと一つだけ、約束します!-小さな問題が残っています。では、新しいバージョンを使ってみましょう。 ```ocaml= > (hyphen-define/wrong1.2 foo bar () #t) > (foo-bar) foo-bar: undefined; cannot reference an identifier before its definition in module: 'program ``` foo-bar はまだ定義されていません。マクロステッパーに戻ります。今は次のように展開していると書いてあります。 `(define (|#<syntax:11:24foo>-#<syntax:11:28 bar>|) #t)` そうか。#aとbはシンタックスオブジェクトです。そこで ``` (string->symbol (format "~a-~a" #'a #'b)) ``` は、両方の構文オブジェクトをハイフンでつないで印刷したものです。 `|#<syntax:11:24foo>-#<syntax:11:28 bar>|` 代わりに、シンタックスオブジェクト内のデータム、例えば foo や bar といったシンボルが必要です。これは syntax->datum を使って得られます。 ```ocaml= > (define-syntax (hyphen-define/ok1 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (syntax-case (datum->syntax #'a (string->symbol (format "~a-~a" (syntax->datum #'a) (syntax->datum #'b)))) () [name #'(define (name args ...) body0 body ...)])])) > (hyphen-define/ok1 foo bar () #t) > (foo-bar) #t ``` これで動作するようになりました。 次に、いくつかのショートカットを紹介します。 --- ### 4.1.1 with-syntax 入れ子になった追加の構文ケースの代わりに、with-syntaxを使うことができます。これは、シンタックスケースをlet文のように並べ替えたもので、最初に名前、次に値を指定します。また、複数のパターン変数を定義する必要がある場合には、この方が便利です。 > with-syntaxの別の名前は、「with new pattern variable」です。 ```ocaml= > (define-syntax (hyphen-define/ok2 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (with-syntax ([name (datum->syntax #'a (string->symbol (format "~a-~a" (syntax->datum #'a) (syntax->datum #'b))))]) #'(define (name args ...) body0 body ...))])) > (hyphen-define/ok2 foo bar () #t) > (foo-bar) #t ``` 繰り返しますが、with-syntaxは単にsyntax-caseを並べ替えたものです。 ``` (syntax-case <syntax> () [<pattern> <body>]) (with-syntax ([<pattern> <syntax>]) <body>) ``` 追加のsyntax-caseを使用しても、with-syntaxを使用しても、どちらにしても単に追加のパターン変数を定義しているだけです。用語や構造のせいでミステリアスな印象を与えないようにしましょう。 --- ### 4.1.2 with-syntax* letでは、あるバインディングを後続のバインディングで使うことができないことがわかっています。 ```ocaml= > (let ([a 0] [b a]) b) a: undefined; cannot reference an identifier before its definition in module: 'program ``` 代わりにletをネストさせます。 ```ocaml= > (let ([a 0]) (let ([b a]) b)) 0 ``` あるいは、入れ子の省略形であるlet*を使います。 ```ocaml= > (let* ([a 0]. [b a]) b) 0 ``` 同様に、ネストしたwith-syntaxを書く代わりに、with-syntax*を使うこともできます。 ```ocaml= > (require (for-syntax racket/syntax)) > (define-syntax (foo stx)) (syntax-case stx () [(_ a) (with-syntax* ([b #'a] [c #'b]) [c #'b]) #'c)])) ``` 1つの問題は、with-syntax*がRacket/baseで提供されていないことです。私たちは(require (for-syntax racket/syntax))しなければなりません。そうしないと、かなり不可解なエラーメッセージが表示されます。 ...: ellipses not allowed as a expression in: .... --- ### 4.1.3 format-id racket/syntaxにはformat-idというユーティリティー関数があり、識別子名を上で行ったものよりも簡潔にフォーマットすることができます。 ```ocaml= > (require (for-syntax racket/syntax)) > (define-syntax (hyphen-define/ok3 stx) (syntax-case stx () [(_ a b (args ...) body0 body ...) (with-syntax ([name (format-id #'a "~a-~a" #'a #'b)]) #'(define (name args ...) body0 body ...))])) > (hyphen-define/ok3 bar baz () #t) > (bar-baz) #t ``` format-idを使うと、シンタックスからシンボル、データム、文字列......と変換していく面倒な作業を処理できるので便利です。 format-idの最初の引数であるlctxは、作成される識別子の字句のコンテキストです。マクロが変換する構文の全体的な塊である stx を指定することはほとんどありません。代わりに、ユーザーがマクロに提供した識別子など、より具体的な構文を提供します。この例では、#'a を使用しています。結果の識別子は、ユーザーが指定したものと同じスコープになります。これは、特にマクロが他のマクロと一緒に構成されている場合に、ユーザーの期待通りに動作する可能性が高くなります。 --- ### 4.1.4 別の例 最後に、任意の数の名前部分をハイフンで結合することができるバリエーションを紹介します。 ```ocaml= > (require (for-syntax racket/string racket/syntax)) > (define-syntax (hyphen-define* stx) (syntax-case stx () [(_ (names ...) (args ...) body0 body ...) (let ([name-stxs (syntax->list #'(names ...))]) (with-syntax ([name (datum->syntax (car name-stxs) (string->symbol (string-join (for/list ([name-stx name-stxs]) (symbol->string (syntax-e name-stx))) "-")))]) #'(define (name args ...) body0 body ...)))])) > (hyphen-define* (foo bar baz) (v) (* 2 v)) > (foo-bar-baz 50) 100 ``` format-idを使用したときと同様に、datum->syntaxを使用する際には、最初のlctx引数に注意します。作成する識別子は、ユーザーがマクロに提供した識別子の語彙的コンテキストを使用するようにします。この場合、ユーザーの識別子はテンプレート変数 (names ...) にあります。これを 1 つのシンタックスからシンタックスのリストに変更します。最初の要素はレキシカル コンテキストに使用します。そしてもちろん、すべての要素を使ってハイフン付きの識別子を形成します。 おさらいです。 * パターン変数はテンプレートの外では使えません。しかし、パターン変数にシンタックスや#'を使うことで、その場限りの「お楽しみサイズ」のテンプレートを作ることができます。 * パターン変数をテンプレート内で使用するために分解したい場合は、with-syntaxが役に立ちます、なぜなら新しいパターン変数を作成できるからです。 * 通常は、syntax->datumを使用して内部の興味深い値を取得する必要があります。 * format-idは、識別子名をフォーマットするのに便利です。 --- ### 4.2 独自の構造体を作る 先ほど学んだことを、より現実的な例に当てはめてみましょう。ここでは、Racketに構造体の機能がないものとします。幸いなことに、構造体を定義して使用するための独自のシステムを提供するマクロを書くことができます。物事をシンプルにするために、構造体は不変 (読み取り専用) で、継承はサポートしません。 次のような構造体宣言があるとします。 `(our-struct name (field1 field2 ...))` いくつかの手続きを定義する必要があります。 * 構造体名を名前とするコンストラクタ手続きです。ここでは、構造体をベクトルで表現します。構造体名は要素0になります。フィールドは要素1以降になります。 * 構造体名に ? を付加した名前の述語です。 * 各フィールドには、その値を取得するアクセサ手続きがあります。これらの名前は struct-field (構造体の名前、ハイフン、フィールド名) となります。 ```ocaml= > (require (for-syntax racket/syntax)) > (define-syntax (our-struct stx) (syntax-case stx () [(_ id (fields ...)) (with-syntax ([pred-id (format-id #'id "~a?" #'id)]) #`(begin ; Define a constructor. (define (id fields ...) (apply vector (cons 'id (list fields ...)))) ; Define a predicate. (define (pred-id v) (and (vector? v) (eq? (vector-ref v 0) 'id))) ; Define an accessor for each field. #,@(for/list ([x (syntax->list #'(fields ...))] [n (in-naturals 1)]) (with-syntax ([acc-id (format-id #'id "~a-~a" #'id x)] [ix n]) #`(define (acc-id v) (unless (pred-id v) (error 'acc-id "~a is not a ~a struct" v 'id)) (vector-ref v ix))))))])) ; Test it out > (require rackunit) > (our-struct foo (a b)) > (define s (foo 1 2)) > (check-true (foo? s)) > (check-false (foo? 1)) > (check-equal? (foo-a s) 1) > (check-equal? (foo-b s) 2) > (check-exn exn:fail? (lambda () (foo-a "furble"))) ; The tests passed. ; Next, what if someone tries to declare: > (our-struct "blah" ("blah" "blah")) format-id: contract violation expected: (or/c string? symbol? identifier? keyword? char? number?) given: #<syntax:eval:83:0 "blah"> ``` エラーメッセージはあまり役に立たない。これはformat-idから来ていますが、これは私たちのマクロのプライベートな実装の詳細です。 syntax-case句は、オプションで「guard」または「fender」式を取ることができることをご存知でしょう。 `[pattern template]` 代わりに `[pattern guard template]` とすることができます。 それでは、ガード式を節に追加してみましょう。 ```ocaml= > (require (for-syntax racket/syntax)) > (define-syntax (our-struct stx) (syntax-case stx () [(_ id (fields ...)) ; Guard or "fender" expression: (for-each (lambda (x) (unless (identifier? x) (raise-syntax-error #f "not an identifier" stx x))) (cons #'id (syntax->list #'(fields ...)))) (with-syntax ([pred-id (format-id #'id "~a?" #'id)]) #`(begin ; Define a constructor. (define (id fields ...) (apply vector (cons 'id (list fields ...)))) ; Define a predicate. (define (pred-id v) (and (vector? v) (eq? (vector-ref v 0) 'id))) ; Define an accessor for each field. #,@(for/list ([x (syntax->list #'(fields ...))] [n (in-naturals 1)]) (with-syntax ([acc-id (format-id #'id "~a-~a" #'id x)] [ix n]) #`(define (acc-id v) (unless (pred-id v) (error 'acc-id "~a is not a ~a struct" v 'id)) (vector-ref v ix))))))])) ; Now the same misuse gives a better error message: > (our-struct "blah" ("blah" "blah")) eval:86:0: our-struct: not an identifier at: "blah" in: (our-struct "blah" ("blah" "blah")) ``` この後、syntax-parseを使えば、使い方をチェックしたり、間違いを指摘するメッセージを表示したりすることが、より簡単にできるようになります。 --- ### 4.3 入れ子になったハッシュ検索にドット記法を使う 前の2つの例では、マクロを使って、マクロに与えられた識別子を結合した名前の関数を定義しました。この例では、その逆を行っています。マクロに渡された識別子を分割しています。 Webサービスのプログラムを書いていると、JSONを扱いますが、Racketではjsexpr? JSONは、しばしば他の辞書を含む辞書を持っています。jsexpr? では、これらは入れ子になったhasheqテーブルで表されます。 ```ocaml= ; Nested ‘hasheq's typical of a jsexpr: > (define js (hasheq 'a (hasheq 'b (hasheq 'c "value")))) ``` JavaScriptではドット記法が使えます。 `foo = js.a.b.c;` Racketではあまり便利ではありません。 `(hash-ref (hash-ref (hash-ref js 'a) 'b) 'c)` これを少しでもすっきりさせるために、ヘルパー関数を書きます。 ```ocaml= ; This helper function: > (define/contract (hash-refs h ks [def #f]) ((hash? (listof any/c)) (any/c) . ->* . any) (with-handlers ([exn:fail? (const (cond [(procedure? def) (def)] [else def]))]) (for/fold ([h h]) ([k (in-list ks)]) (hash-ref h k)))) ; Lets us say: > (hash-refs js '(a b c)) "value" ``` それはいいですね。さらに進んで,JavaScriptのようなドット記法を使うことはできないだろうか。 ```ocaml= ; This macro: > (require (for-syntax racket/syntax)) > (define-syntax (hash.refs stx) (syntax-case stx () ; If the optional ‘default' is missing, use #f. [(_ chain) #'(hash.refs chain #f)] [(_ chain default) (let* ([chain-str (symbol->string (syntax->datum #'chain))] [ids (for/list ([str (in-list (regexp-split #rx"\\." chain-str))]) (format-id #'chain "~a" str))]) (with-syntax ([hash-table (car ids)] [keys (cdr ids)]) #'(hash-refs hash-table 'keys default)))])) ; Gives us "sugar" to say this: > (hash.refs js.a.b.c) "value" ; Try finding a key that doesn't exist: > (hash.refs js.blah) #f ; Try finding a key that doesn't exist, specifying the default: > (hash.refs js.blah 'did-not-exist) 'did-not-exist ``` うまくいきました。 マクロは、エラー時に役立つメッセージを出すべきだということがわかってきました。ここではそのようにしてみましょう。 ```ocaml= > (require (for-syntax racket/syntax)) > (define-syntax (hash.refs stx) (syntax-case stx () ; Check for no args at all [(_) (raise-syntax-error #f "Expected hash.key0[.key1 ...] [default]" stx)] ; If the optional ‘default' is missing, use #f. [(_ chain) #'(hash.refs chain #f)] [(_ chain default) (unless (identifier? #'chain) (raise-syntax-error #f "Expected hash.key0[.key1 ...] [default]" stx #'chain)) (let* ([chain-str (symbol->string (syntax->datum #'chain))] [ids (for/list ([str (in-list (regexp-split #rx"\\." chain-str))]) (format-id #'chain "~a" str))]) ; Check that we have at least hash.key (unless (and (>= (length ids) 2) (not (eq? (syntax-e (cadr ids)) '||))) (raise-syntax-error #f "Expected hash.key" stx #'chain)) (with-syntax ([hash-table (car ids)] [keys (cdr ids)]) #'(hash-refs hash-table 'keys default)))])) ; See if we catch each of the misuses > (hash.refs) eval:97:0: hash.refs: Expected hash.key0[.key1 ...] [default] in: (hash.refs) > (hash.refs 0) eval:98:0: hash.refs: Expected hash.key0[.key1 ...] [default] at: 0 in: (hash.refs 0 #f) > (hash.refs js) eval:99:0: hash.refs: Expected hash.key at: js in: (hash.refs js #f) > (hash.refs js.) eval:100:0: hash.refs: Expected hash.key at: js. in: (hash.refs js. #f) ``` 悪くないですね。もちろん、エラーチェック付きのバージョンはかなり長くなっています。エラーチェック付きのコードは一般的にロジックを不明瞭にする傾向があり、ここでもそうなっています。幸いなことに、通常のRacketのコントラクトや型付きRacketのタイプと同じように、syntax-parseがどのようにしてこの問題を解決するかをすぐに見ることができます。 (hash.refs js.a.b.c)と書くことが、(hash-refs js '(a b c))と書くことよりも本当に明確であるとは納得できないかもしれません。もしかしたら、この方法は実際には使わないかもしれません。しかし、Racketのマクロシステムを使えば、そのような選択も可能です。 * [next section(5 構文パラメータ)](https://hackmd.io/Hi_rX5oZTLKILvCF-QL6Lw) * [previous section(3 変形!)](https://hackmd.io/xLxLlckORtSxKWclYNtJ0Q)
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up