読者です 読者をやめる 読者になる 読者になる

マクロ、拙作のテキストマクロ言語m55について主に

テキストマクロプロセッサとは何か

Lispのマクロはテキスト変換ではなく、リスト(ないし多分木)からリストに変換するものですから、まず除外されます。とは言えたいていのプログラマがマクロと言ってまず思い浮かべるのはCプリプロセッサでしょうから、ここではそのような、テキストを処理するマクロプロセッサについての話をします。

CPP

CプリプロセッサC言語の歴史と同じくらいの長さの歴史があります。しばしばC言語ソースコードプリプロセス以外に使おうとする人がいますが、そういう人は(Unix上の処理系の場合なら) unix という文字列が 1 に書き換えられて泣く覚悟があるのだろうと想像します。また、マクロの再展開などに関するルールは魔境といってよく、古い文章ですが、LSI C-86添付のCPPのきだあきらさんによる改造版、ANSICPPの添付文書は一読の価値があります( http://metanest.jp/ansicpp/ )。現在は松井潔さんによる、完成度の高いCプリプロセッサ mcpp が存在します( http://mcpp.sourceforge.net/index-jp.html )。エキスパートCプログラマであれば、mcpp のドキュメント mcpp-summary-jp.pdf は必読、と思います。

m4

前世紀の人であればいまわしき sendmail.cf を、今世紀の人であればいまわしき Autotools を連想するのではないかと思いますが、m4 は実際のところ、そこそこよくできているマクロ言語です。杉浦 K.さんによる「m4 チュートリアル」( http://www.nurs.or.jp/~sug/soft/super/m4.htm )や、拙作の「m4入門」( http://metanest.jp/m4intro/ )などが参考になるでしょう。マクロを定義するマクロ、といったものを作れるようになっているので、最初は「手強い」と思うかもしれませんが、仕掛けは単純ですのできちんと理解すれば便利に使いこなせるツールです。

Macr055(m55)

m4にあきたらず自作してしまったマクロプロセッサが、Macr055(以下、m55)です( https://github.com/metanest/macr055 )。実装にあたっては、『ソフトウェア作法』("Software Tools")の8章を参考にしました。以下、READMEにある例で、m4と異なる点を紹介します。

あらゆるレキシカル要素がカスタマイズ可能

m4ではクウォートの開始と終了の記号が変更できますが、m55ではマクロの開始・終了や実引数の区切りなど、あらゆる記号が変更できます。

{m55_changequote|[|]} # {comment}
{m55_changecom|[/*]|[*/]} /* {comment} */
{m55_changebracket|[<]|[>]}
<m55_changesep|[,]>
<m55_changepre,[@]>

<m55_define,[cat],[[@1@2@3@4@5@6@7@8@9]]>
<cat,foo,bar,baz>
マクロの呼出しは明示する必要がある

簡単な例から示します。

{m55_define|HELLO|hello, world}
{HELLO}

上が入力、下が出力です。

(空行)
hello, world

m55_defineは「マクロを定義する」という副作用がある組込みマクロで、自身は空文字列に展開されます。
m4(ないしCPP)では、入力のうち識別子のように見える部分をトークンとして読み(CPPには「プリプロセッサトークン」というジャーゴンがあります)、その名前のマクロが定義されていればマクロとしての処理が起きます。そのような仕様は、意図的に名前を衝突させて、たとえば標準関数を横取りする仕掛けを簡単に仕込みたいであるとかそういう場合には便利ですが、一方でソースコード中のあらゆる識別子が、何らかのマクロによって不意に書き換えられてしまわないか、という不安がつきまといます。マクロの呼出しは明示する、というようにマクロ言語を設計すれば、「識別子のように見える部分」を常にいちいち検出することもなく、どこでマクロ展開が起きるかは一見して明らかになります。
そのような設計のマクロ言語は別に私のオリジナルというわけではなく、m4が影響を受けているマクロ処理系であるChristopher StracheyさんのGPMがそのような設計になっています(GPMについては C. Strachey "A General Purpose Macrogenerator" http://dx.doi.org/10.1093/comjnl/8.3.225 を参照。12月5日まで参加申込み延長中の第56回プログラミング・シンポジウムでも、和田先生による「GPMとそのプログラム」というお話があるそうです( http://www.ipsj.or.jp/prosym/56/56program.html ))。
マクロの呼出しを明示するようにするとそれとひきかえに、m4では必要だった「展開されるのを回避するためのクウォート」が必要なくなり、記述がわかりやすくなったように感じます。以下、より複雑なマクロを示します。

名前の連結が起きるマクロ
{m55_define|foo|bar}{m55_dnl}
{m55_define|barbaz|quuz}{m55_dnl}
{{foo}baz}

↑入力、出力↓

quuz

m55_dnlは、m4のdnlと同じで、その後改行まで入力を読み飛ばさせるという副作用がある組込みマクロです。
2個のマクロを定義した後、{{foo}baz} という入力の、まず内側の {foo} が bar に展開されて {barbaz} になり、それが quuz に展開されています。

マクロ呼出しに展開されるマクロ
{m55_define|foo|`{bar}'}{m55_dnl}
{m55_define|bar|`{baz}'}{m55_dnl}
{m55_define|baz|quuz}{m55_dnl}
{foo}
quuz

「マクロ呼出しに展開されるマクロ」のデモです。m55でもm4と同様、マクロ呼出しの実引数はそれ自身がまず展開されようとするので、クウォートでくくってやる必要があります。
この例では入力の {foo} は {foo} → {bar} → {baz} → quuz と展開されます。
ここで定義を誤って {quuz} と展開されるようにしてしまった場合、quuz という名前のマクロは定義されていないので、エラーになります。

複数回展開されるマクロ
{m55_define|foo|bar}{m55_dnl}
{m55_define|bar|baz}{m55_dnl}
{m55_define|baz|quuz}{m55_dnl}
{{{foo}}}
quuz

複数回展開されるマクロ」のデモです(一つ前の「マクロ呼出しに展開される」との違いに注意してください)。{{{foo}}} → {{bar}} → {baz} → quuz と展開されます。m4では「その名前で定義されたマクロが見つかり続ければ」展開されますが、m55ではこのように、展開を意図していることが明らかな書き方が必要です。厳格過ぎる、と感じるかもしれませんし、実際そうかもしれませんが、ともかくそういう言語設計になっています。

引数のあるマクロ

引数のあるマクロはm4と同様です。

{m55_define|rev3|$3 $2 $1}{m55_dnl}
{rev3|foo|bar|baz}
baz bar foo
実装について

GitHubリポジトリを見ていただければわかりますが、awkで実装してあり、全体で500行ちょっとです。算術式のbcによる計算を効率よくおこなうための仕掛けのため、シェルスクリプトによるwrapperが必要になっていますが、これはgawk依存の機能(双方向パイプ)を使えば必要ないのですが、nawkでも動くようこのようにしています。
機能を階層分けして、関数にしてあるので行数がちょっと多めになっています。改造するには前後のコードを追う必要があってちょっと大変かもしれませんが、一旦読めてしまえば容易に組込みマクロの追加などはできるでしょう。