--- title: section3 tags: macro --- ### 3 変形! あなたは部屋の中にいます。 地面に鍵があります。 近くにピカピカの真鍮のランプがあります。 間違った方向に行くと、あなたは絶望的に迷い、混乱します。 絶望的に迷ってしまいます。 > 鍵を拾う あなたは構文変換器を持っています --- ### 3.1 シンタックストランスフォーマーとは? シンタックストランスフォーマーは、トランスフォーマーのひとつではありません。 代わりに、それは単なる関数です。この関数は、シンタックスを受け取り、シンタックスを返します。つまり、構文を変換します。 以下は、入力された構文を無視して、常に文字列リテラルの構文を出力するトランスフォーマ関数です。 > これらの例は#lang racketを前提としています。#lang racket/baseを使って試したい場合は、(require (for-syntax racket/base))が必要になります。 ```ocaml= > (define-syntax foo (lambda (stx) (syntax "I am foo"))) ``` それを使って ```ocaml= > (foo) "I am foo" ``` define-syntaxを使用すると、トランスフォーマバインディングを行います。これはRacketのコンパイラに「fooで始まる構文の塊に出会ったら、それを私の変換関数に渡して、私が返した構文に置き換えてください」と伝えます。つまり、Racketは(foo ...)のようなものを私たちの関数に渡し、代わりに使用する新しい構文を返すことができます。検索と置換のようなものです。 Racketで関数を定義する通常の方法をご存知でしょうか。 ```(define (f x) ...)``` は、次のものが省略されています。 ```(define f (lambda (x) ...))``` この略記法を使えば、lambdaといくつかの括弧を入力する必要がなくなります。 同様の略記法がdefine-syntaxにもあります。 ```ocaml= > (define-syntax (also-foo stx) (syntax "I am also foo")) > (also-foo) "I am also foo" ``` ここで覚えておきたいのは、これは単なる省略形だということです。私たちが定義しているのは、構文を受け取って構文を返す変換関数です。マクロで行うすべてのことは、この基本的な考え方の上に構築されます。魔法ではないのです。 省略形といえば、シンタックスの省略形もあります。 > #'はシンタックスの略語で、'がクォートの略語であるのと同じです。 ```ocaml= > (define-syntax (quoted-foo stx) #'"I am also foo, using #' instead of syntax") > (quoted-foo) "I am also foo, using #' instead of syntax" ``` 今後は#' の省略形を使うことにします。 もちろん、文字列リテラルよりも面白い構文を発することもできます。(displayln "hi")を返すのはどうでしょうか? ```ocaml= > (define-syntax (say-hi stx) #'(displayln "hi")) > (say-hi) hi ``` Racket がこのプログラムを展開すると、(say-hi) の出現を確認し、そのための変換関数があることを確認します。Racketは古い構文で私たちの関数を呼び出し、私たちは新しい構文を返し、それがプログラムの評価と実行に使われます。 --- ### 3.2 入力とは? これまでの例では,入力の構文を無視して,固定の構文を出力してきました.しかし、通常は入力された構文を何か別のものに変換したいと思うでしょう。 まずは、入力が実際に何であるかをよく見てみましょう。 ```ocaml= > (define-syntax (show-me stx) (print stx) #'(void)) > (show-me '(+ 1 2)) #<syntax:eval:10:0 (show-me (quote (+ 1 2)))> ``` (print stx)は、トランスフォーマーに与えられたもの、つまり、シンタックスオブジェクトを示しています。 シンタックスオブジェクトはいくつかのもので構成されています。最初の部分はコードを表すS式で、たとえば'(+ 1 2)です。 Racketの構文はまた、ソースファイル、行番号、列などの興味深い情報で飾られています。最後に、レキシカルスコーピングに関する情報があります(今は気にする必要はありませんが、後で重要になるでしょう)。 構文オブジェクトにアクセスするには、さまざまな関数が用意されています。構文の一部を定義してみましょう。 ```ocaml= > (define stx #'(if x (list "true") #f)) > stx #<syntax:eval:11:0 (if x (list "true") #f)> です。 ``` それでは、シンタックスオブジェクトにアクセスする関数を使ってみましょう。ソース情報の関数を紹介します。 > (syntax-source stx) は 'eval を返していますが、これは Scribble でコードスニペットを実行するためにエバリュエータを使用してこのドキュメントを生成しているからです。通常は、"my-file.rkt "のようになります。 ```ocaml= > (syntax-source stx) 'eval > (syntax-line stx) 11 > (syntax-column stx) 0 ``` さらに興味深いのは、シンタックスの「もの」自体です。 syntax->datum はこれを完全に S 式に変換します。 ```ocaml= > (syntax->datum stx) '(if x (list "true") #f) ``` 一方、syntax-e は「一段階下」にしか行きません。syntax-eは、syntaxオブジェクトを含むリストを返すことができます。 ```ocaml= > (syntax-e stx) '(#<syntax:eval:11:0 if> #<syntax:eval:11:0 x> #<syntax:eval:11:0 (list "true")> #<syntax:eval:11:0 #f>) ``` これらの構文オブジェクトはそれぞれsyntax-eで変換することができ、さらに再帰的に変換することができます-これがsyntax->datumの役割です。 ほとんどの場合、syntax->listはsyntax-eと同じ結果になります。 ```ocaml= > (syntax->list stx) '(#<syntax:eval:11:0 if> #<syntax:eval:11:0 x> #<syntax:eval:11:0 (list "true")> #<syntax:eval:11:0 #f>) ``` (syntax-eとsyntax->listはどんな時に違うのか?今は横道にそれるのはやめよう)。 シンタックスを変換するには、通常、与えられたピースを使い、その順序を変えたり、いくつかのピースを変更したり、あるいはまったく新しいピースを導入したりします。 --- ### 3.3 入力を実際に変換する 与えられた構文を逆にするトランスフォーム関数を書いてみましょう。 > 例題の最後にある値によって、結果をきれいに評価することができます。なぜ便利なのか、(reverse-me "backwards" "am" "i") を試してみましょう。 ```ocaml= > (define-syntax (reverse-me stx) (datum->syntax stx (reverse (cdr (syntax->datum stx))))) > (reverse-me "backwards" "am" "i" values) "i" "am" "backwards" ``` Understand Yoda, we can. すごい!でも、これってどうやって使うの? まず、入力された構文を syntax->datum に渡します。これにより、構文が単なるリストに変換されます。 ```ocaml= > (syntax->datum #'(reverse-me "backwards" "am" "i" values)) '(reverse-me "backwards" "am" "i" values) ``` cdrを使うと、reverse-meというリストの最初の項目を切り取って、残りの項目を残します。("backwards" "am" "i" values)となります。これをreverseに渡すと、(values "i" "am" "backwards")に変わります。 ```ocaml= > (reverse (cdr '(reverse-me "backwards" "am" "i" values))) '(values "i" "am" "backwards") ``` 最後にdatum->syntaxを使って、これをシンタックスに戻します。 ```ocaml= > (datum->syntax #f '(values "i" "am" "backwards")) #<syntax (values "i" "am" "backwards")>。 ``` これがRacketコンパイラに返す変換関数で、この構文が評価されます。 ```ocaml= > (values "i" "am" "backwards") "i" "am" "backwards" ``` > datum->syntaxの第一引数には、変換器が出力する構文に関連付ける字句のコンテキスト情報が入ります。第1引数に#fを指定した場合、語彙的なコンテキストは関連付けられません。 --- ### 3.4 コンパイル時とランタイム時の比較 ```ocaml= (define-syntax (foo stx)) (make-pipe) ;Ce n'est pas le temps d'exécution #'(void)) ``` 通常のRacketコードが実行されるのは...ランタイムです。ダウト。 > 「コンパイルタイム対ランタイム」ではなく、「シンタックスフェーズ対ランタイムフェーズ」と表現されることもあります。同じ違いです。 しかし、構文変換器は、プログラムの解析、展開、コンパイルのプロセスの一部としてRacketから呼び出されます。言い換えれば、構文変換関数はコンパイル時に評価されます。 このように、マクロを使うことで、通常のコードではできないことができるようになります。典型的な例としては、Racket の if 形式があります。 ```ocaml= (if <condition> <true-expression> <false-expression>) ``` ifを関数として実装した場合、すべての引数は関数に与えられる前に評価されます。 ```ocaml= > (define (our-if condition true-expr false-expr) (cond [condition true-expr] [else false-expr])) > (our-if #t "true" "false") "true" ``` これでうまくいったようです。しかし、こんなのはどうでしょう。 ```ocaml= > (define (display-and-return x) (displayln x) x) > (our-if #t (display-and-return "true") (display-and-return "false")) true false "true" ``` > 一つの答えは、「関数型プログラミングは良いもので、副作用は悪いものだ」というものです。しかし、副作用を避けることは必ずしも実用的ではありません。 おっと。これらの式には副作用があるので、両方とも評価されることは明らかです。副作用にディスク上のファイルの削除が含まれていたらどうでしょう?そんなことはしたくないでしょう (if user-wants-file-deleted? (delete-file) (void)) は、user-wants-file-deleted? が#fであっても、ファイルを削除したくないでしょう。 ですから、これは単純な関数としては動作しません。しかし、シンタックストランスフォーマーは、コンパイル時にシンタックスを再編成し、コードを書き換えることができます。構文の断片は移動されますが、実行時まで実際には評価されません。 これを行う方法の一つを紹介します。 ```ocaml= > (define-syntax (our-if-v2 stx) (define xs (syntax->list stx)) (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)]))) > (our-if-v2 #t (display-and-return "true") (display-and-return "false")) true "true" > (our-if-v2 #f (display-and-return "true") (display-and-return "false")) false "false" ``` これで正しい答えが出ました。でも、どうやって?変形関数自体を取り出して、それが何をしたのか見てみましょう。まず、入力構文の例から始めます。 ```ocaml= > (define stx #'(our-if-v2 #t "true" "false")) > (displayln stx) #<syntax:eval:32:0 (our-if-v2 #t "true" "false")> ``` 1. 元の構文を、syntax->listを使って、構文オブジェクトのリストに変更します。 ```ocaml= > (define xs (syntax->list stx)) > (displayln xs) (#<syntax:eval:32:0 our-if-v2> #<syntax:eval:32:0 #t> #<syntax:eval:32:0 "true"> #<syntax:eval:32:0 "false">) ``` 2. これをRacketのcond形式にするには、cadr、caddr、cadddrを使ったリストから、condition、true-expression、false-expressionの3つの興味深い部分を取り出して、cond形式に整える必要があります。 ```ocaml= '(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)]) ``` 3. 最後に、datum->syntaxを使ってシンタックスに変換します。 ```ocaml= > (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)])) #<syntax (cond (#t "true") (else "false"))> ``` このように動作しますが、cadddrなどを使用してリストを再構築するのは苦痛であり、エラーが発生しやすいです。Racketのmatchをご存知でしょうか?これを使えば、パターンマッチが可能になります。 > 構文リストの最初の項目は気にしていないことに注意してください。our-if-v2では(car xs)を取りませんでしたし、パターンマッチの際にはnameを使いませんでした。一般的には、構文変換器はそれを気にしません。なぜなら、それは変換器の結合の名前だからです。言い換えれば、マクロは通常、自分の名前を気にしません。 その代わりに ```ocaml= > (define-syntax (our-if-v2 stx) (define xs (syntax->list stx)) (datum->syntax stx `(cond [,(cadr xs) ,(caddr xs)] [else ,(cadddr xs)]))) ``` と書くことができます。 ```ocaml= > (define-syntax (our-if-using-match stx) (match (syntax->list stx) [(list name condition true-expr false-expr) (datum->syntax stx `(cond [,condition ,true-expr] [else ,false-expr]))])) ``` いいですね。では、実際に使ってみましょう。 ```ocaml= > (our-if-using-match #t "true" "false") match: undefined; cannot reference an identifier before its definition in module: 'program ``` おっと。matchが定義されていないことを訴えています。 私たちの変換機能は、実行時ではなく、コンパイル時に動作します。そして、コンパイル時に自動的に要求されるのはRacket/baseだけで、Racket全体ではありません。 そしてコンパイル時にはrequireのfor-syntax形式を使って要求します。 この場合、単純に(require racket/match)とするのではなく、(require (for-syntax racket/match))とします。for-syntaxの部分は「コンパイル時に」という意味です。 では、試してみましょう。 ```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]))])) > (our-if-using-match-v2 #t "true" "false") "true" ``` Joy. --- ### 3.5 for-syntaxの開始 コンパイル時に match を使用する必要があるため、for-syntax を使用して racket/match モジュールを要求しました。 マクロで使用する独自のヘルパー関数を定義したい場合はどうすればよいでしょうか。1 つの方法は、それを別のモジュールに入れ、racket/match モジュールで行ったように、for-syntax を使用して要求することです。 代わりにヘルパーを同じモジュールに置く場合、単純に定義して使用することはできません。定義は実行時に存在しますが、コンパイル時には必要です。その答えは、ヘルパー関数の定義をbegin-for-syntaxの中に入れることです。 ```ocaml= (begin-for-syntax (define (my-helper-function ....) ....)) (define-syntax (macro-using-my-helper-function stx) (my-helper-function ....) ....) ``` 単純なケースでは、begin-for-syntaxとdefineを合成した ```ocaml= (define-for-syntax (my-helper-function ....) ....) (define-syntax (macro-using-my-helper-function stx) (my-helper-function ....) ....) ``` おさらいですが * 構文変換は実行時ではなくコンパイル時に動作します。良い点は、構文を評価することなく、構文の一部を並べ替えることができることです。実行時の関数では適切に動作しない if のような形式を実装できます。 * さらに良いニュースは、構文変換を書くための特別な、奇妙な言語があるわけではないということです。これらの変換関数は、私たちがすでに知っているRacket言語を使って書くことができます。 * 半分悪いニュースは、親しみやすさのために、実行時に作業していないことを忘れがちになることです。このことを忘れてはいけない場合もあります。 * 例えば、Racket/baseだけは自動的に必要になります。他のモジュールが必要な場合は、それらを必要とする必要があり、for-syntax を使用してコンパイル時に行う必要があります。 * 同様に、ヘルパー関数を使用するマクロと同じファイル/モジュールに定義したい場合は、begin-for-syntax 形式で定義を囲む必要があります。そうすることで、コンパイル時に利用できるようになります。 * [next section(4 パターンマッチング: syntax-case と syntax-rules)](https://hackmd.io/Nk0tT1F8St2-iu07-mqllA?view) * [previous section(2 私たちの進攻計画)](https://hackmd.io/CciUmfpTTr6mBAx_84giOQ?view)