マクロに関する基本的なこと
syntax-rules
syntax-rulesはScheme R5RSで定義されたSchemeマクロです。
マクロに渡されたパラメータのトークンの並びでパターンマッチさせる高水準のマクロです。
以下に簡単なマクロのサンプルを紹介します。
関数が値を受け取るのに対し、マクロは式そのものを受け取ります。
;; Common Lisp風の (defun 関数名 (引数1 引数2 ...) 処理1 処理2 ...)
(define-syntax
defun
(syntax-rules ()
((_ name args)
(define name (fun args) ())
)
((_ name args e1 e2 ...)
(define name (fun args e1 e2 ...))
)
))
(defun add (x y) (+ x y))
;; 上記の用に書くと、以下の用に展開されます
(define add (fun (x y) (+ x y)))
この例では2種類のパターンが列挙されており、下のパターンが適用され展開されています。
マクロに渡された「add」がマクロのパターンにおける「name」に該当します。
また「(x y)」の部分が「args」に、「(+ x y)」の部分が「e1 e2 ...」に該当します。
「e1 e2 ...」というのは1つ以上の式を表しますので、2以上の式が与えられた場合でも
下のパターンが適用されます。
式の数が0だった場合にのみ、上のパターンが適用されます。
マクロは関数と異なり、コードそのものをパラメータとして受け取りますので、
普通の関数ではパラメータとして受け取れないような要素もパラメータ化することが可能です。
例えば以下の例はモジュールを生成するマクロですが、マクロがモジュール名をパラメータとして
受け取ることで、擬似的にfunctorのように振る舞います。
(require "common.scm")
(module BaseMod (struct
(define output (fun (x) (print_int (* x x))))
))
(define-syntax make-mod
(syntax-rules ()
((_ mod)
(struct
(define output
(fun (x)
((#. mod output) x)
(print_newline)
))
)
)
))
(module ExMod (make-mod BaseMod))
(ExMod.output 3)
define-macro
define-macroはCommon Lispのdefmacroのような伝統的なLispマクロです。
より低水準で自由度の高いコード生成を可能にします。
準備中
define-macro(defmacro)に関しては「OnLisp」が推薦図書です……
リーダーマクロ
リーダマクロは指定した文字がソースコードに現れた際に呼び出され、
その際に引数として受け取った入力ポート(Common Lispでいう入力ストリーム)から、
必要な分だけソースコードを読み込んでS式を返します。
S式を返したあとは、リーダが残りのソースコードの解析を続行します。
現在リーダマクロを定義する手段は以下の2つです。
(set-macro-character 文字 (lambda (port char) exp ...))
(set-dispatch-macro-character 文字1 文字2 (lambda (p char1 char2) exp ...))
前者がCommon Lispの同名のset-macro-characterと同じ様な機能で、
後者がCommon Lispの同名のset-dispatch-macro-characterと同じような機能です。
ただし、Common Lispと異なりmake-dispatch-macro-characterは不要です。
(後者にはset-macro-character2という別名も用意してあります)
以下に簡単なリーダマクロのサンプルを紹介します。
;; ブレース(波カッコ)をレコード型のリテラル表記として扱うリーダーマクロ
(set-macro-character #\{ (lambda (p char)
`(record . ,(read-delimited-list p #\}))
))
(type record_sample "{ name: string; mutable age: int; }")
;; 本来ブレースはS式のカッコとしては使えませんが、上記のリーダーマクロを
;; 定義することで、以下の二つは同じ意味として解釈されるようになります。
(define user_data (record (name "foo") (age 28)))
(define user_data { (name "foo") (age 28) })
S式のカッコは現在2種類のカッコが利用可能です。
丸カッコ"( ~ )"と角カッコ"[ ~ ]"で自由に使い分けることが可能ですが、
以下のリーダーマクロを定義することで、角カッコをカッコとして使えなくなる代わりに
OCamlのリテラル記法に近い形でlistやarrayを記述可能になります。
;; [ ... ] をlistのリテラル記法として扱うリーダーマクロ
(set-macro-character #\[ (lambda (p char)
`(list ,@(read-delimited-list p #\]))
))
;; [| ... |] をarrayのリテラル記法として扱うリーダーマクロ
(set-macro-character2 #\[ #\| (lambda (p char cha2)
`(array ,@(read-delimited-list2 p #\| #\]))
))
;; listの例
(let
((sample_ls [ 10 20 30 40 50 60 ]))
(List.iter
(fun (x) (print_int x) (print_newline))
sample_ls)
)
(print_newline)
;; arrayの例
(let
((sample_ar [| 10 20 30 40 50 60 |]))
(Array.iter
(fun (x) (print_int x) (print_newline))
sample_ar)
)
(print_newline)
マクロから別のマクロを展開することも可能です。
以下のリーダーマクロの例では、前述の例の「list」の代わりに「common.scm」内で
定義されている「list-int」を展開しています。
これでHaskellのような等差数列のlistの記述が可能になります。
(ただし、この例では整数の等差数列限定となります)
(require "common.scm")
(set-macro-character #\[ (lambda (p char)
`(list-int ,@(read-delimited-list p #\]))
))
;; 等差数列のlistの例
(let ((sample_ls [ 10 20 .. 60 ]))
(List.iter
(fun (x) (print_int x) (print_newline))
sample_ls)
)
(print_newline)
以下のコードはPerlの正規表現リテラルを真似たものです。
リーダーマクロを利用することで言語のコアに手を加える事無く、
プログラマの手によるリテラルの拡張が容易に行えます。
;; m/正規表現/ で正規表現(Str.regexp)オブジェクトを生成するマクロを定義する
(set-macro-character2 #\m #\/
(lambda (p char char2)
(letrec ((reader (lambda (tmp)
(let ((regchar (read-char p)))
(cond
((eq? regchar #\/)
(string-join (string-split
(list->string (reverse tmp))
"\\\/") "/")
)
((eq? regchar #\\)
(reader (cons (read-char p) (cons regchar tmp)))
)
(#t
(reader (cons regchar tmp)))
)
)
)))
`(Str.regexp_case_fold ,(reader '()))
)))
;; (=~ 正規表現オブジェクト 文字列) でマッチを行うマクロを定義する
(define-syntax =~ (syntax-rules ()
((_ regexp target)
(Str.string_match regexp target 0)
)
))
;; (=~ m/^[0-9]+?$/ "123") は以下のコードと等価です
;; (Str.string_match (Str.regexp_case_fold "^[0-9]+?$") "123" 0)
(if
(=~ m/^[0-9]+?$/ "123")
(print_string "match!!\n")
(print_string "not match!!\n")
)
リーダーマクロは強力ですが、従来のマクロ以上に混乱を招きやすいので
慎重に扱う必要があります。
ちなみに、以下が入力ポートからソースコードを読み込む関数です。
読み込んだソースコードを文字列やlistで返します。
- (read ポート)
1つの式を読み込んで返す
- (read-char ポート)
1文字を読み込んで文字で返す
- (peek-char ポート)
1文字を読み込んで文字で返すが、ポートのポインタを進めない
- (read-line ポート)
1行を読み込んで文字列で返す
- (read-delimited-list ポート 文字)
指定した文字に到達するまで複数の式を読み込んでlistで返す
- (read-delimited-list2 ポート 文字 文字)
指定した2つ連続した文字に到達するまで複数の式を読み込んでlistで返す
Lispでもよく使われるのが、リーダがソースコードを解析した結果作られる 構文木をコンパイル直前に操作するマクロです。 define-macro(Common Lispのdefmacro相当)や define-syntax~syntax-rules(Scheme R5RSマクロ)がこれに該当します。
そしてもう一つ、プログラマ自身に定義されることはあまり多くはありませんが リーダがソースコードを解析する段階で実行されるリーダーマクロ と呼ばれる機能が存在します。 set-macro-characterなどがこれに該当します。 前者はリーダが解析した結果を扱うため、S式の枠組みの中で自由に コードを変形、生成することが可能ですが、 後者はリーダそのものに割り込むため、S式の枠組みを超えた ソースコードを扱うことも可能になります。
実行のタイミングは異なりますが、どちらのマクロにも共通していることは 「S式を生成して返す」という点と「Schemeインタプリタ上で実行される、 ほぼ完全なScheme」という点です。
こうして生成されたS式は最終的にはOCamlのソースコードへ変換され、 OCamlコンパイラによって実行コードへとコンパイルされます。