マクロ展開時に副作用を起こすとどうなるか
2016-12-31
TechR7RSのマクロシステムはsyntax-rulesだけだが、R6RSにあったsyntax-caseや、いくつかの処理系が実装しているexplicit-renaming, implicit-renamingといったマクロシステムでは、マクロ展開時に任意の式が書ける。
では、マクロ展開時にdisplayを使って文字列を出力した場合、それはどのタイミングで評価されるのだろうか?特にインタプリタじゃなくてコンパイラの場合は?
Chicken Scheme
ということで適当なSchemeコンパイラを用意する。Macだとbrew install chickenでChiken Schemeが入る。マクロシステムはexplicit-renamingがあるようなのでそれを使う。
(define-syntax foo
(er-macro-transformer
(lambda (expr rename id=?)
(display "hello")
(cadr expr))))
(display (foo 1234))
このようにしたとき、「hello」はどのタイミングで出力されるだろうか?
実行してみる
% csc compile-time.scm
hello
% ./compile-time
1234
ということで、コンパイル時に出力されることが分かった。
本来の使い方
やっておいて何だけど、任意の式が書けるというのは別に文字列を出力するためではない。Racketのマニュアルを見ると、「マクロの使い方を間違えたときにより丁寧なエラーメッセージを出せる」というメリットが挙げられている。あとは動的に関数名を組み立てるとか?
(define-syntax define-getter
(er-macro-transformer
(lambda (expr rename id=?)
(let* ((name (symbol->string (cadr expr)))
(str (string-append "get-" name))
(sym (string->symbol str)))
`(define (,sym) '("get" ,(cadr expr)))))))
(define-getter a) ; (define (get-a) '("get" a))に展開される
(write (get-a))
;=> ("get" a)
追記:マクロ展開時に呼べる関数について
副作用が起こせるといっても、任意の関数が呼べるとは限らないようだ。例えばRacketではbegin-for-syntaxなどでコンパイル時に使う関数だと明示する必要がある。Chickenでも以下はエラーになった。ふーむ。
(define (bar)
(display "hello"))
(define-syntax foo
(er-macro-transformer
(lambda (expr rename id=?)
(bar)
(cadr expr))))
(display (foo 1234))
% csc compile-time.scm
Error: during expansion of (foo ...) - unbound variable: bar
Call history:
<syntax> (##core#begin (display (foo 1234)))
<syntax> (display (foo 1234))
<syntax> (foo 1234)
<eval> (bar) <--