メタプログラミング技法 第1回: メタプログラミングとは

プログラムを記述して他のプログラムを生成する

現在最も多く使われているプログラミング技法の1つに、プログラムを記述してプログラム本体あるいはプログラムの部品を生成する技法があります。ここでは、メタプログラミングの必要性およびメタプログラミングを構成するいくつかのコンポーネント(テキスト・マクロ言語、特定コード・ジェネレータ)について学びます。コード・ジェネレータの作成方法を確認し、Scheme言語における言語依存マクロのプログラミングについて詳しく見てください。

Jonathan Bartlett (johnnyb@eskimo.com), Director of Technology, New Medio

Jonathan BartlettはLinuxアセンブリー言語を使ったプログラミングの入門書Programming from the Ground Upの著者です。New Media Worxでの主席開発者であり、顧客向けにWebアプリケーションや、ビデオ、キヨスク、デスクトップなどのアプリケーションを開発しています。連絡先はjohnnyb@eskimo.comです。



2005年 10月 20日

コード生成プログラムはメタプログラムと呼ばれることがあります。また、こうしたプログラムを書くことをメタプログラミングと呼びます。コードを生成するプログラムを書くアプリケーションには多数のアプリケーションがあります。

この記事では、なぜメタプログラミングが必要なのかを説明し、メタプログラミング技法のコンポーネントについてもいくつか説明します。さらにテキスト・マクロ言語や特定コード・ジェネレータまで踏み込んで説明し、こうした言語やジェネレータの作成方法を説明します。Schemeを使用した言語依存マクロのプログラミングについても詳しく見ていきます。

メタプログラミングのさまざまな用途

第一の用途として、実行時に使用するデータ・テーブルを事前に生成するプログラムを書く場合に使われます。たとえば、ゲームのプログラミング中にすべての8ビット整数値の正弦を表形式で見たいとします。この場合、それぞれの正弦を自分で計算し手作業でコーディングするか、起動時にこの表が作成されるようにプログラミングするか、あるいはコンパイル前に表のカスタム・コードを生成するプログラムを書くか、いずれかの方法が考えられます。表の中味が少なければ実行時にこうした表を作成してもよいのですが、そうでない場合はプログラムの起動に大変な時間がかかってしまいます。この場合、静的なデータ・テーブルを生成するプログラムを書くのが通常考えられる一番よい方法です。

第二の用途として、多くの関数に大量のボイラープレート・コード(繰り返し使用する定型コード)が設定されている場合、このボイラープレート・コードを処理するミニ言語を作成する際に使われます。こうすれば、重要な部分のコーディングだけに集中できます。プログラミングの際、ボイラープレート部分を抽出して1つの関数にまとめることができれば、それが最善の方法です。しかし実際には、ボイラープレートはそう都合よくはいきません。すべてのインスタンスで変数のリストを宣言したり、エラー・ハンドラーを登録したりする必要があるでしょう。場合によってはコードの挿入が必要なボイラープレート・コードもあるかもしれません。そうなると、単純に1つの関数だけを呼び出すことはできなくなります。こうした場合、ミニ言語を作成して手間をかけずにボイラープレート・コードを処理するのは有効な手段です。このミニ言語は通常のソース・コードに変換されてからコンパイルすることになります。

多くのプログラミング言語は、単純な処理を実行する場合でも非常に冗長なステートメントを書く必要があります。コード生成プログラムならば、こうした冗長さを省くことができ、タイピングの手間も省けます。これによってタイピングのミスが減り、多くのミスを防ぐことができます。これが第三の用途です。

プログラミング言語の機能が増えると、コード生成プログラムの魅力は薄れてきます。ある言語の標準機能として使用可能なものが、別の言語ではコード生成プログラムを介してのみ使用可能になっている場合があります。しかし、コード生成プログラムが必要である理由は、言語の設計が不十分だというだけではありません。メンテナンスが容易であることもその理由です。


基本テキスト・マクロ言語

コード生成プログラムを利用すれば、規模の小さな特定領域言語(特定分野での利用を想定して作られた専用言語)を使って開発することができます。対象言語を使って書くよりも、プログラムの作成とメンテナンスが容易になります。

こうした特定領域言語を作成するためのツールは通常マクロ言語と呼ばれます。この記事ではマクロ言語をいくつか紹介し、マクロ言語を使ってコード作成を改善する方法について見ていきます。

Cプリプロセッサ

まず最初に、テキスト・マクロ言語を必要とするメタプログラミングについて見てみましょう。テキスト・マクロとは、その言語の意味や仕組みを意識することなく、プログラミング言語のテキストを直接操作するマクロのことです。現在最も幅広く使用されているテキスト・マクロは、CプリプロセッサとM4マクロプロセッサの2つです。

Cのプログラミングの経験があれば、おそらくCの#define マクロを使用したことがあるでしょう。有効なコード生成機能を持たない多くの言語において、テキスト・マクロの拡張機能は、最善の方法ではないにしても、基本メタプログラミングを実行するには手軽な方法です。リスト1は#defineマクロの例です。

リスト1.2つの値を交換する単純なマクロ
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }

このマクロを使って、指定されたデータ型の2つの値を交換できます。マクロを使用しない場合は以下に挙げる不都合が発生することから、この処理はマクロとして書くことが最善だと言えます。

  • 1つの関数を呼び出す場合、処理は単純であるにもかかわらず、膨大なオーバーヘッドが発生します。
  • 変数の値ではなく、変数のアドレスを関数に渡す必要があります(これ自体は悪いことではないのですが、アドレスを渡すことにより関数の呼び出しが複雑になり、コンパイル時に変数の値をレジスタに格納しておくことができなくなります)。
  • 交換したいデータのそれぞれの型に合わせて、異なった関数をコーディングする必要があります。

リスト2は、このマクロの使用例です。

リスト2.SWAPマクロの使用
#define SWAP(a, b, type) { type __tmp_c; c = b; b = a; a = c; }
int main()
{
    int a = 3;
    int b = 5;
    printf("a is %d and b is %d\n", a, b);
    SWAP(a, b, int);
    printf("a is now %d and b is now %d\n", a, b);

    return 0;
}

Cプリプロセッサが起動すると、テキストがSWAP(a, b, int) から { int __tmp_c; __tmp_c = b; b = a; a = __tmp_c; } へとそっくり変換されます。

テキストの置き換え機能は便利ですが、使用方法がかなり限られています。この機能には以下に挙げる問題点があります。

  • 他の式と組み合わせて使用する場合、テキストの置き換え処理は非常に複雑になることがあります。
  • Cプリプロセッサでは、マクロで使用できる引数の数は限られています。
  • C言語におけるデータ型の取り扱い方法のせいで、異なった種類の引数に対して異なったマクロが必要になる場合が多くあります。異なったマクロが必要でない場合でも、少なくともパラメータの型を引数として渡す必要があります。
  • テキストの置き換え機能に限っているため、渡された引数のいずれかと一時変数の名前が同じだった場合、Cではその一時変数の名前を変更することができません。このマクロの場合、__tmp_cという名前の変数が渡されたらまったく機能しなくなります。

マクロを式と組み合わせる場合、マクロの作成は非常に複雑になります。たとえば、2つの値のうち小さい方の値を返すMINという以下のマクロを例に取ってみます。

リスト3.2つの値のうち小さい値を返すマクロ
#define MIN(x, y) ((x) > (y) ? (y) : (x))

これを見て最初に、どうしてかっこをこんなに多く使っているのだろうと思うかもしれません。これは演算子に優先順位があるためです。たとえば、かっこを付けずにMIN(27, b=32) を実行した場合、27 > b = 32 ? b = 32 : 27となり、コンパイルエラーになってしまいます。演算子に優先順位があるため、27 > bが優先されてしまうからです。かっこを元に戻して実行すれば、正しい結果が得られます。

しかし、もう1つ問題があります。パラメータとして呼び出される関数は、すべて式の右辺の要素として呼び出されます。プリプロセッサはC言語の規則についてはおかまいなしにテキストの置き換えだけを処理します。つまり、MIN(do_long_calc(), do_long_calc2()) を実行した場合、( (do_long_calc()) > (do_long_calc2()) ? (do_long_calc2()) : (do_long_calc())) になってしまいます。少なくとも1つの計算式が2度繰り返されるため、この処理には時間がかかってしまいます。

これらの計算式のうち、別の機能(印刷やグローバル変数の変更など)を持った計算式がある場合はさらに問題です。こうした機能も同じように2度繰り返されるからです。この「複数回呼び出し」の問題は、呼び出しのたびに関数が異なった値を返す場合、マクロから正しくない値が返されるという問題さえ引き起こすことがあります。

Cプリプロセッサのマクロ・プログラミングについての詳しい情報は、CPPマニュアルを参照してください(リンクのリソース・セクションを参照)。

M4マクロプロセッサ

M4マクロプロセッサは、最も進んだマクロ・プロセッシング方式の1つです。このプロセッサの一番のうたい文句は、広く普及しているsendmailメーラーの設定ファイルのお助けツールであるということです。

sendmailの設定は楽しいものでも簡単なものでもありません。sendmailの設定ファイルでは、丸々1冊分の本が設定の説明に費やされています。しかし、設定処理を簡単にするためのマクロがsendmailのメーカーによって用意されています。このマクロを使えば、特定のパラメータを指定するだけで、ローカルのインストールとsendmailの両方に固有なボイラープレートをM4プロセッサが適用し、設定ファイルが作成されます。

例として、M4マクロを使用した典型的なsendmailの設定ファイルをリスト4に示します。

リスト4.M4マクロを使用したsendmail設定の例
divert(-1)
include(`/usr/share/sendmail-cf/m4/cf.m4')
VERSIONID(`linux setup for my Linux dist')dnl
OSTYPE(`linux')
define(`confDEF_USER_ID',``8:12'')dnl
undefine(`UUCP_RELAY')dnl
undefine(`BITNET_RELAY')dnl
define(`PROCMAIL_MAILER_PATH',`/usr/bin/procmail')dnl
define(`ALIAS_FILE', `/etc/aliases')dnl
define(`UUCP_MAILER_MAX', `2000000')dnl
define(`confUSERDB_SPEC', `/etc/mail/userdb.db')dnl
define(`confPRIVACY_FLAGS', `authwarnings,novrfy,noexpn,restrictqrun')dnl
define(`confAUTH_OPTIONS', `A')dnl
define(`confTO_IDENT', `0')dnl
FEATURE(`no_default_msa',`dnl')dnl
FEATURE(`smrsh',`/usr/sbin/smrsh')dnl
FEATURE(`mailertable',`hash -o /etc/mail/mailertable.db')dnl
FEATURE(`virtusertable',`hash -o /etc/mail/virtusertable.db')dnl
FEATURE(redirect)dnl
FEATURE(always_add_domain)dnl
FEATURE(use_cw_file)dnl
FEATURE(use_ct_file)dnl
FEATURE(local_procmail,`',`procmail -t -Y -a $h -d $u')dnl
FEATURE(`access_db',`hash -T<TMPF> -o /etc/mail/access.db')dnl
FEATURE(`blacklist_recipients')dnl
EXPOSED_USER(`root')dnl
DAEMON_OPTIONS(`Port=smtp,Addr=127.0.0.1, Name=MTA')
FEATURE(`accept_unresolvable_domains')dnl
MAILER(smtp)dnl
MAILER(procmail)dnl
Cwlocalhost.localdomain

内容を理解する必要はありませんが、M4マクロ・プロセッサを実行すると、この小さなファイルが1,000行を超える設定情報を生成するということを覚えておいてください。

これと同じように、autoconfもM4を使って単純マクロに基づくシェル・スクリプトを生成します。プログラムをインストールして最初に必要な入力操作が /configureである場合、おそらくautoconfマクロを使用して作成されたプログラムが実行されているはずです。リスト5では、3,000行を超える設定プログラムを生成するautoconfプログラムの例を示しています。

リスト5.M4マクロを使用したautoconfスクリプトの例
AC_INIT(hello.c)
AM_CONFIG_HEADER(config.h)
AM_INIT_AUTOMAKE(hello,0.1)
AC_PROG_CC
AC_PROG_INSTALL
AC_OUTPUT(Makefile)

マクロ・プロセッサを介してこのスクリプトを実行すると、標準構成のチェックを行うシェル・スクリプトが生成され、標準パスとコンパイラー・コマンドの検索が実行され、テンプレートを基にしてconfig.h および Makefileが作成されます。

M4マクロ・プロセッサの詳細は非常に複雑なのでここでは述べません。M4マクロ・プロセッサに関する詳細情報とsendmailおよびautoconfにおける使用方法については、リソース・セクションのリンクを参照してください。


プログラムを記述するプログラム

今までは一般的なテキスト置き換えプログラムを見てきましたが、ここからはさらに特化したコード・ジェネレータについて見ていくことにします。さまざまな使用可能なプログラムやその使用例を確認し、コード・ジェネレータを作成していきます。

コード・ジェネレータの概要

GNU/Linux システムには、プログラムを記述するプログラムがいくつか実装されています。最も幅広く使用されているのは、おそらく以下に挙げるプログラムでしょう。

  • Flex: 字句解析ジェネレータ
  • Bison: 構文解析ジェネレータ
  • Gpref: 完全ハッシュ関数ジェネレータ

これらのツールはすべてC言語に対応するアウトプットを生成します。どうしてこれらのツールが関数ではなくコード・ジェネレータとして実装されているのか、不思議に思うかもしれません。それには以下のような理由があります。

  • これらを関数にすると、インプットが非常に複雑になり、C言語に正しく対応する形式で処理することが難しくなります。
  • これらのプログラムは操作に必要な多くの静的検索テーブルを計算し生成します。そのため、プログラムが起動されるたびにこうしたテーブルを生成するよりも、プリコンパイル時に生成しておく方が処理効率がよくなります。
  • これらのプログラムの機能の多くは、特定の箇所に任意のコードを追加することによってカスタマイズが可能です。追加されたコードは、コード・ジェネレータによって生成された構造体の一部である変数や関数を参照できます。この際、手作業で変数を定義したり抽出したりする必要は一切ありません。

これらのツールはそれぞれ、特定の種類のプログラムを作成することを目的としています。Bisonは構文解析ツールの生成に使用し、Flexは語句解析ツールの生成に使用します。他のツールはこれらのツールに比べて、プログラムの特定機能の自動化に主眼が置かれています。

たとえば、データベースへのアクセス方法を命令型言語に統一するのは、退屈な作業である場合が多いものです。この作業を簡単に、より標準化したものにするには、組み込みSQLを使うのが便利です。このSQLはメタプログラミング方式で、データベース・アクセスとC言語を手軽に組み合わせる際に使用されます。

C言語にはデータベースへのアクセスを可能にする使用可能なライブラリが多くありますが、組み込みSQLのようなコード・ジェネレータを使えば、C言語の拡張機能の一部としてSQLエントリをC言語に組み込むことにより、C言語とデータベース・アクセスを簡単に組み合わせることができます。ただし、実装された組み込みSQLの多くは、基本的には通常のCプログラムをアウトプットとして生成するために特化したマクロ・プロセッサにすぎません。しかし、組み込みSQLを使用することにより、ライブラリを直接使用する場合に比べ、プログラマにとってデータベースへのアクセスがよりスムーズでわかりやすいものになり、エラーを防ぐことができます。組み込みSQLにより、一種のマクロ特殊プログラム言語がデータベース・プログラミングの煩雑さを覆い隠すのです。

コード・ジェネレータの使用方法

短い組み込みSQLプログラムを例に取り、コード・ジェネレータの仕組みを見てみましょう。そのためには、組み込みSQLのプロセッサが必要です。PostgreSQLには組み込みSQLであるecpgが実装されています。このプログラムを実行するには、PostgreSQLに「test」という名前でデータベースを作成する必要があります。作成したら、そのデータベースで以下のコマンドを発行します。

リスト6.データベース作成スクリプトのプログラム例
create table people (id serial primary key, name varchar(50));
insert into people (name) values ('Tony');
insert into people (name) values ('Bob');
insert into people (name) values ('Mary');

リスト7は、名前フィールド順にソートされたデータベースの内容を読み込んで印刷する単純なプログラムです。

リスト7.組み込みSQLのプログラム例
#include <stdio.h>
int main()
{
   /* Setup database connection -- replace postgres/password w/ the
      username/password on your system*/
   EXEC SQL CONNECT TO unix:postgresql://localhost/test USER postgres/password;

   /* These variables are going to be used for temporary storage w/ the database */
   EXEC SQL BEGIN DECLARE SECTION;
   int my_id;
   VARCHAR my_name[200];
   EXEC SQL END DECLARE SECTION;

   /* This is the statement we are going to execute */
   EXEC SQL DECLARE test_cursor CURSOR FOR
      SELECT id, name FROM people ORDER BY name;

   /* Run the statement */
   EXEC SQL OPEN test_cursor;

   EXEC SQL WHENEVER NOT FOUND GOTO close_test_cursor;
   while(1) /* our previous statement will handle exitting the loop */
   {
      /* Fetch the next value */
      EXEC SQL FETCH test_cursor INTO :my_id, :my_name;
      printf("Fetched ID is %d and fetched name is %s\n", my_id, my_name.arr);
   }

   /* Cleanup */
   close_test_cursor:
   EXEC SQL CLOSE test_cursor;
   EXEC SQL DISCONNECT;

   return 0;
}

以前に通常のデータベース・ライブラリを使ってC言語でデータベース・プログラムを書いた経験があれば、このコーディング例がごく標準的なものであることがわかるでしょう。通常のCプログラムでは任意のデータ型の値を複数返すことはできませんが、このEXEC SQL FETCH行を使用すれば可能です。

このプログラムをコンパイルして実行するには、プログラムをtest.pgcという名前のファイルに格納し以下のコマンドを実行します。

リスト8.組み込みSQLプログラムの作成
ecpg test.pgc
gcc test.c -lecpg -o test
./test

コード・ジェネレータの作成

コード・ジェネレータとそれによって実行できる処理とをいくつか見てきたところで、ここからは簡単なコード・ジェネレータの書き方を見ていきます。作成が最も単純で便利なコード・ジェネレータは、おそらく静的検索テーブルを生成するコード・ジェネレータでしょう。Cプログラムで高速な関数を作成するには、すべての回答を設定した検索テーブルを作成しておく場合が多いでしょう。この場合、手作業であらかじめ計算しておくか(プログラマにとって時間の無駄です)、あるいはプログラムの起動時に毎回テーブルを作成するように設定する(ユーザーにとって時間の無駄です)必要があります。

この例では、関数あるいは関数のセットを1つの整数に取り込み、回答用の検索テーブルを作成します。

こうしたプログラムをどのように作成したらよいか、終わりの方からさかのぼって見ることにします。5から20までの数字の平方根を返す検索テーブルを作成したいとします。このテーブルを作成する簡単なプログラムは以下のようになります。

リスト9.平方根検索テーブルの作成と使用
/* our lookup table */
double square_roots[21];

/* function to load the table at runtime */
void init_square_roots()
{
   int i;
   for(i = 5; i < 21; i++)
   {
      square_roots[i] = sqrt((double)i);
   }
}

/* program that uses the table */
int main ()
{
   init_square_roots();
   printf("The square root of 5 is %f\n", square_roots[5]);
   return 0;
}

このテーブルを静的に初期化された配列に変換するには、プログラムの前半部分を削除し、手作業で計算した以下のようなコードを代わりに挿入します。

リスト10.静的検索テーブルが設定された平方根プログラム
double square_roots[] = {
   /* these are the ones we skipped */ 0.0, 0.0, 0.0, 0.0, 0.0
   2.236068, /* Square root of 5 */
   2.449490, /* Square root of 6 */
   2.645751, /* Square root of 7 */
   2.828427, /* Square root of 8 */
   3.0, /* Square root of 9 */
   ...
   4.472136 /* Square root of 20 */
};

ここで必要なのは、これらの値を取り出して上で見たようなテーブルに書き出すプログラムです。こうすれば、テーブルに書き出された値はコンパイル時にロードされることになります。

他の要素についても見てみましょう。

  • 配列の名前
  • 配列の種類
  • 開始インデックス
  • 終了インデックス
  • スキップするエントリの省略値
  • 最終値を算出する式

これらは非常に単純で明確な要素であり、単純なリストとして定義できます。このマクロでは、これらの要素を以下のようにコロンで区切ったリストとしてまとめてみます。

リスト11.平方根のコンパイル時テーブルの最適な作成方法
/* sqrt.in */
/* Our macro invocation to build us the table.  The format is: */
/* TABLE:array name:type:start index:end index:default:expression */
/* VAL is used as the placeholder for the current index in the expression */
TABLE:square_roots:double:5:20:0.0:sqrt(VAL)

int main()
{
   printf("The square root of 5 is %f\n", square_roots[5]);
   return 0;
}

あとは、このマクロを標準的なC言語に変換するプログラムが必要です。この簡単な例では、Perlが使われています。Perlはユーザー・コードをストリング形式で評価でき、その構文もCの構文とよく似ています。そのため、ユーザー・コードを動的にロードして処理することができます。

このコード・ジェネレータはマクロの宣言を処理する必要がありますが、マクロ以外の部分はすべて変更せずに渡さなければなりません。そのため、基本的なマクロ・プロセッサの構造は以下のようになります。

  1. 1行読み込む。
  2. 読み込んだ行の処理が必要か?
  3. 必要であれば、読み込んだ行を処理してアウトプットを生成する。
  4. 必要でなければ、読み込んだ行を変更せずに直接アウトプットへコピーする。

リスト12は、テーブル・ジェネレータを生成するPerlのコードです。

リスト12.テーブル・マクロのコード・ジェネレータ
#!/usr/bin/perl
#
#tablegen.pl
#

##Puts each program line into $line
while(my $line = <>)
{
   #Is this a macro invocation?
   if($line =~ m/TABLE:/)
   {
      #If so, split it apart into its component pieces
      my ($dummy, $table_name, $type, $start_idx, $end_idx, $default,
         $procedure) = split(m/:/, $line, 7);

      #The main difference between C and Perl for mathematical expressions is that
      #Perl prefixes its variables with a dollar sign, so we will add that here
      $procedure =~ s/VAL/\$VAL/g;

      #Print out the array declaration
      print "${type} ${table_name} [] = {\n";

      #Go through each array element
      foreach my $VAL (0 .. $end_idx)
      {
         #Only process an answer if we have reached our starting index
         if($VAL >= $start_idx)
         {
            #evaluate the procedure specified (this sets $@ if there are any errors)
            $result = eval $procedure;
            die("Error processing: $@") if $@;
         }
         else
         {
            #if we haven't reached the starting index, just use the default
            $result = $default;
         }

         #Print out the value
         print "\t${result}";

         #If there are more to be processed, add a comma after the value
         if($VAL != $end_idx)
         {
            print ",";
         }

         print "\n"
      }

      #Finish the declaration
      print "};\n";
   }
   else
   {
      #If this is not a macro invocation, just copy the line directly to the output
      print $line;
   }
}

このプログラムを起動するには、以下を実行します。

リスト13.コード・ジェネレータの実行
./tablegen.pl < sqrt.in > sqrt.c
gcc sqrt.c -o sqrt
./a.out

わずか数行のコードを書くだけで、プログラミング作業を大幅に簡単にする単純なコード・ジェネレータが作成できました。このマクロだけで、整数指標付き数字テーブルの作成が必要なプログラミング作業が大幅に簡単になります。これに少し手をかければ、完全な構造体の定義を含むテーブルも作成できます。さらにもう少し手をかければ、配列の先頭部分に不要な空白のエントリが入らないようにすることもできます。


Schemeを使った言語依存マクロのプログラミング

コード・ジェネレータは対象言語の仕組みをわずかながら理解しますが、通常は完全な解析ツールとしては使用できず、対象言語の仕組みを理解させるにはコンパイラを書き換える必要があります。

しかし、単純なデータ構造を持つ言語が存在すれば、この問題はもっと簡単なものになるかもしれません。Schemeプログラミング言語では、言語そのものがリンクされたリストとして表現され、リスト処理のために開発された言語がまさにSchemeプログラミング言語なのです。これにより、Schemeは変換されたプログラムを作成するのに(ほぼ)最適な言語となっています。プログラムを解析するのに大がかりな解析ツールは必要ありません。Schemeそのものがリスト処理言語です。

実際には、Schemeの変換能力はこれにとどまりません。Scheme標準では、Scheme言語への追加を容易にする目的で特別に作成されたマクロ言語が定義されています。さらに、ほとんどのScheme実装では、コード生成プログラムの作成に役立つ追加機能が用意されています。

ここでC言語のマクロの問題点をおさらいしておきましょう。SWAPマクロでは、最初に交換する値のデータ型を明示的に宣言する必要がありました。その次に、他で使われていないとわかっている名前を一時変数として使用する必要がありました。この問題がSchemeではどう処理されて解決されているのか、確認してみましょう。

リスト14.Schemeによる値交換マクロ
;;Define SWAP to be a macro
(define-syntax SWAP
   ;;We are using the syntax-rules method of macro-building
   (syntax-rules ()
      ;;Rule Group
      (
         ;;This is the pattern we are matching
         (SWAP a b)
         ;;This is what we want it to transform into
         (let (
               (c b))
            (set! b a)
            (set! a c)))))

(define first 2)
(define second 9)
(SWAP first second)
(display "first is: ")
(display first)
(newline)
(display "second is: ")
(display second)
(newline)

これはsyntax-rulesマクロです。Schemeにはいくつかのマクロ方式がありますが、syntax-rulesマクロが標準です。

syntax-rulesマクロでは、define-syntaxがマクロ変換を定義する際に使用されるキーワードです。define-syntaxキーワードの後に定義されるマクロの名前が続き、その後に変換値が続きます。

syntax-rulesは適用する変換の種類です。かっこの中には、マクロ名を除く他のマクロ特有の使用記号が入ります(この例ではマクロ名はありません)。

この後には変換規則の連番が続きます。構文変換機能はそれぞれの規則を検査し、適応するパターンを探し出そうとします。適応するパターンが見つかると、構文検査機能は指定された変換を実行します。この例の場合、パターンは (SWAP a b) の1つしかありません。aとbはマクロの呼び出しに使用される単位コードに適応したパターン変数で、変換中に各部品を再編成するのに使用されます。

一見すると、Cのマクロと同じ問題点があるように見えるかもしれません。しかし、Cのマクロとは異なる点がいくつかあります。まず、これはScheme言語であるため、データの型は変数名とは関係なくデータの値そのものになります。Cのマクロで発生した変数の型についての問題は、ここでは一切考える必要はありません。では、Cのマクロにあった変数名の問題についてはどうでしょうか? 変数の1つがcという名前だった場合、問題は起こらないのでしょうか?

問題は起こりません。syntax-rulesを使用するSchemeのマクロは「清潔」だからです。つまり、マクロで使用される一時変数は名前の重複を防ぐために、すべて置き換え処理の前に自動的に名前が変更されるのです。したがってこのマクロの場合、ある置き換え変数にcという名前がつけられたら、元のcという変数は置き換えが実行される前に別の名前に変更されます。実際には、いかなる場合でも名前の変更は行われます。リスト15は、プログラムでマクロ変換を実行した場合の予想される結果です。

リスト15.値交換マクロの予想される変換結果
(define first 2)
(define second 9)
(let
   (
      (__generated_symbol_1 second))
   (set! second first)
   (set! first __generated_symbol_1))
(display "first is: ")
(display first)
(newline)
(display "second is: ")
(display second)
(newline)

これを見てわかるように、Schemeのマクロは他のマクロ方式に見られる多くの問題点を回避してその機能を発揮します。

しかし場合によっては、「清潔」なマクロを使用したくないときもあります。たとえば、変換するコードにアクセス可能なバインディングをマクロに取り込みたい場合などです。syntax-rules方式はどんな場合でも変数名を変更するため、単純に変数を宣言してもだめです。したがって、ほとんどのSchemeではsyntax-caseと呼ばれる「清潔」ではないマクロ方式が用意されています。

syntax-caseマクロを書くのはsyntax-rulesに比べて大変ですが、Schemeのランタイムをほとんどすべて使うことができるため、大変強力な機能を発揮します。systax-caseマクロは標準ではありませんが、多くのSchemeシステムに実装されています。syntax-caseが実装されていない場合でも、同じような方式のマクロが用意されています。

ここでは基本的な形式のsysntax-caseマクロを見ることにします。コンパイル中に指定の形式を実行するat-compile-timeと呼ばれるマクロを定義してみましょう。

リスト16.コンパイル時に1つの値あるいは一連の値を生成するマクロ
;;Define our macro
(define-syntax at-compile-time
   ;;x is the syntax object to be transformed
   (lambda (x)
      (syntax-case x ()
         (
            ;;Pattern just like a syntax-rules pattern
            (at-compile-time expression)

            ;;with-syntax allows us to build syntax objects
            ;;dynamically
            (with-syntax
               (
                  ;this is the syntax object we are building
                  (expression-value
                     ;after computing expression, transform it into a syntax object
                     (datum->syntax-object
                        ;syntax domain
                        (syntax k)
                        ;quote the value so that its a literal value
                        (list 'quote
                        ;compute the value to transform
                           (eval
                              ;;convert the expression from the syntax representation
                              ;;to a list representation
                              (syntax-object->datum (syntax expression))
                              ;;environment to evaluate in
                              (interaction-environment)
                              )))))
               ;;Just return the generated value as the result
               (syntax expression-value))))))

(define a
   ;;converts to 5 at compile-time
   (at-compile-time (+ 2 3)))

これはコンパイル時に指定された操作を実行するマクロです。正確には、マクロ展開時に指定の操作を実行するマクロです。Schemeシステムにおいては、マクロ展開時とコンパイル時とは必ずしも同じタイミングではありません。Schemeシステムでコンパイル時に許可されている式であれば、すべてこの例のように使用できます。それではどのように機能するか見てみましょう。

syntax-caseを使うことにより、実際には変換関数を定義することになります。この関数にはlambdaが入ります。xは変換される式です。with-syntaxは、変換される式で使用できる追加の構文要素を定義します。syntax-rulesの変換機能と同じ規則に従い、syntaxは構文要素を取り込んでこれらの要素を元通りに組み合わせます。それでは1行ずつ処理を追って見ていきましょう。

  1. at-compile-time式が適合する。
  2. 変換機能の最深部において、expressionがリスト表示に変換され、正常なSchemeコードとして認識される。
  3. 結果がシンボルのquoteと結合されリストになる。これにより、結合された値がコードになった場合、Schemeがその値を文字値として扱うことができる。
  4. このデータが構文オブジェクトに変換される。
  5. この構文オブジェクトにアウトプット用としてexpression-valueという名前が付けられる。
  6. 変換機能(syntax expression-value)により、expression-valueはこのマクロからのアウトプットのエンティティであることが宣言される。

コンパイル時に計算を実行するこの機能を使うことにより、C言語で定義したものよりも優れたTABLEマクロを作成できます。リスト17では、どうやってそれをat-compile-timeマクロを使ったSchemeで作成するかを示しています。

リスト17.Schemeによる平方根テーブルの作成
(define sqrt-table
   (at-compile-time
      (list->vector
         (let build
            (
               (val 0))
            (if (> val 20)
               '()
               (cons (sqrt val) (build (+ val 1))))))))

(display (vector-ref sqrt-table 5))
(newline)

上で見たC言語のマクロとほとんど同じようなテーブル生成用のマクロを作成することにより、このマクロの使用はもっと簡単になります。

リスト18.コンパイル時に検索テーブルを作成するマクロ
(define-syntax build-compiled-table
   (syntax-rules ()
      (
         (build-compiled-table name start end default func)
         (define name
            (at-compile-time
               (list->vector
                  (let build
                     (
                        (val 0))
                     (if (> val end)
                        '()
                        (if (< val start)
                           (cons default (build (+ val 1)))
                           (cons (func val) (build (+ val 1))))))))))))

(build-compiled-table sqrt-table 5 20 0.0 sqrt)
(display (vector-ref sqrt-table 5))
(newline)

これで、どんな種類のテーブルでも簡単に生成できる関数が作成されました。


まとめ

広範囲にわたって見てきましたが、ここで全体を振り返ってみましょう。まず最初に、コード生成プログラムで解決するのに最適なプログラム上の問題にはどういったものがあるかを見ました。それには以下のようなプログラムがありました。

  • データ・テーブルを事前に生成しておく必要のあるプログラム
  • 関数としてまとめることができないくらい多くのボイラープレート・コードが設定されているプログラム
  • 非常に冗長なコーディングを必要とする技法を使用しているプログラム

次に、メタプログラミングの方式とその使用例をいくつか見ました。これには一般的なテキスト置き換え方式、特定領域言語、関数の生成機能がありました。さらに、テーブル作成に特化したインスタンスを確認し、C言語で静的テーブルを作成するコード生成プログラムの書き方についても確認しました。

最後に、Schemeについて確認しました。Scheme言語そのものの構成要素である構造体を使用して、C言語で発生した問題をどうやって解決するかを見ました。Schemeは言語であると同時に、それ自体がコード生成言語として設計されています。こうした技法がScheme言語そのものに組み込まれているため、プログラミング作業がより簡潔になり、これまでに見てきた他の技法で発生したものと同じ多くの問題を回避できるのです。これにより、特定領域の拡張機能をより簡単にScheme言語に追加できるようになりました。こうした役割は、これまではコード・ジェネレータが担っていたのです。

このシリーズの第2回では、Schemeマクロのプログラミングについて詳しく見ていきます。また、Schemeマクロを使って大規模プログラムのプログラミング作業を大幅に簡単にする方法についても詳しく見ていきます。

参考文献

学ぶために

  • Jonathanが「効果的なリスト処理でプログラミングを改善する」(developerWorks, 2005年1月)の中で、単一リンクされたリストとスキームについて検証しています。
  • Using XML and XSL for code generation」(developerWorks, 2001年5月)は、XSLスタイルシートの、ちょっと変わった使い方を解説しています。ここでは、XML文書をソースコードに変換することによって、パーベイシブなデバイスで使用できるアプリケーションを作成するためにXSLスタイルシートを使っています。
  • チュートリアル、Code generation using XSLT(developerWorks, 2003年4月)では、コード生成の概念についての基本を学ぶことができます。
  • リフレクションに取って代わるコード生成」(developerWorks, 2004年6月)は、生成されたコードでリフレクション・コードを置き換えるためにランタイム・クラスワーキングを使う方法を解説しています。
  • GNU CPP manualには、GCCマクロ処理のためにどんなものが利用できるのか、また、そのプログラミングに関して知っておくべき注意点などの情報が豊富に提供されています。
  • M4についての詳しい情報は、GNU M4のマニュアルを見てください。
  • SendmailのM4マクロについて詳しく知るためには、official Sendmail M4 macro referenceを見てください。
  • GNUビルド・システムについて知るために、またautoconfやM4マクロによっていかに多くの作業が自動化できるかを理解するために、GNU build system tutorialを見てください。
  • syntax-caseやsyntax-rulesを使ったスキーム・マクロ・プログラミングを始めるためには、このチュートリアル・リストが役立つでしょう。
  • developerWorksのLinuxゾーンには、Linux開発者のための資料が他にも豊富に取り揃えられています。
  • developerWorks technical events and Webcastsを利用して、最新技術を学んでください。

製品や技術を入手するために

  • 皆さんの次期Linux開発プロジェクトを、IBM trial softwareを使って構築してください。developerWorksから直接ダウンロードすることができます。

議論するために

  • プログラミングにおけるコード生成の側面に関する興味深いリソースとして、Code Generation Networkがあります。ここでは、「実用主義のエンジニア」のために、コード生成についての情報を提供しています。
  • developerWorks blogsに参加して、developerWorksのコミュニティーに加わってください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=226860
ArticleTitle=メタプログラミング技法 第1回: メタプログラミングとは
publish-date=10202005