マクロに関する基本的なこと

このS式OCaml(仮)のマクロには大きく分けて2種類のマクロが存在します。
Lispでもよく使われるのが、リーダがソースコードを解析した結果作られる 構文木をコンパイル直前に操作するマクロです。 define-macro(Common Lispのdefmacro相当)や define-syntax~syntax-rules(Scheme R5RSマクロ)がこれに該当します。
そしてもう一つ、プログラマ自身に定義されることはあまり多くはありませんが リーダがソースコードを解析する段階で実行されるリーダーマクロ と呼ばれる機能が存在します。 set-macro-characterなどがこれに該当します。 前者はリーダが解析した結果を扱うため、S式の枠組みの中で自由に コードを変形、生成することが可能ですが、 後者はリーダそのものに割り込むため、S式の枠組みを超えた ソースコードを扱うことも可能になります。
実行のタイミングは異なりますが、どちらのマクロにも共通していることは 「S式を生成して返す」という点と「Schemeインタプリタ上で実行される、 ほぼ完全なScheme」という点です。
こうして生成されたS式は最終的にはOCamlのソースコードへ変換され、 OCamlコンパイラによって実行コードへとコンパイルされます。

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で返します。
また、マクロがdefine-macroを含むようなS式を生成しても、 それが正しいマクロ定義の式であれば正常に動作します。 つまりマクロを生成するマクロを書く事も可能です。
そういったマクロの定義は強力な機能ではありますが、 デバッグが困難なものになる可能性が高いですのでご注意ください。
common.scmの中にもそういったマクロを生成するマクロがありますので、興味があれば 参考にしてください。