ゆるコード

僕はね、とにかくコードが書きたいのだよ君。

Clojureで解説する: メタプログラミング

目次

簡単に説明すると

コンパイル時にコードを生成する仕組み。

ちゃんとした意味

メタプログラミング、または、メタプログラムとは、プログラム自身を生成・操作・変換するための技法や概念を指す。

具体的には、ソースコードや実行時のプログラム構造を、別のプログラム(あるいは自分自身)が動的あるいは静的に解析・変更し、それをもとに新たなコードを生成したり動作を変化させたりする手法のこと。

効果

メタプログラミングを活用すると、冗長なコードの自動生成や高度な抽象化が可能になるため、生産性や保守性を向上させることができる。

一方で、プログラムが複雑化しすぎる場合や、可読性が下がるリスクがあるため、適切な抽象化レベルで慎重に取り入れることが重要である。

使用例

マクロ

一般的にコンパイル時に展開されるマクロを用いて、ソースコードを生成・変換する。LispClojureなどでは言語仕様として強力なマクロシステムが提供され、プログラムの構造を直接操作しながらコード生成が可能である。

リフレクション (Reflection)

実行時にクラスやオブジェクトのメタ情報(型情報など)を取得し、動的にインスタンス生成やメソッド呼び出しを行う。Javaなど多くの言語が標準ライブラリとしてリフレクションAPIを提供している。

コード生成ツール

テンプレートやアノテーションをもとにクラスや関数を自動生成する仕組み。いわゆる“スキャフォールド (Scaffold)”やアノテーションプロセッサなどは、このカテゴリに含まれることがあります。

Clojureのマクロとメタプログラミング

1. Clojureのマクロとは

Clojureのマクロは、引数として渡されたS式(=コード)を受け取り、別のS式に変換して返す関数のようなもの。

マクロ展開はコンパイル時に行われるため、実行時の処理ロジックを自由に組み替えたり生成したりできる。

たとえば、独自の制御構文を作る場合にマクロを使うと、通常の関数呼び出しでは不可能な“評価タイミングの制御”を行うことができる。

マクロのメリット

  • 制御構造の自作が簡単
    例: unless や for のような構文糖をユーザーが独自に定義できる。
  • 不要な評価を避けられる
    関数は基本的に引数を「先に評価」してから本体が呼ばれますが、マクロなら引数の評価を好きなタイミングで行えます。
  • DSL (Domain-Specific Language) 風の構文
    メタプログラミングによって、特定の用途に特化した言語機能を組み込むかのようなインタフェースを作り込むことが容易です。

2. 簡単な例:unlessマクロ

典型的な例として、「条件が偽の場合にのみ本体を実行」する unless 構文をマクロで作ると下記の様になる。 (Clojureでは標準ライブラリにないが、マクロで簡単に定義できる)

  • defmacro でマクロを定義。
  • バッククォート ( ` ) と チルダ ( ~ ) を使うと、マクロが展開時に返すS式 を「テンプレート」的に書ける。
  • ~condition は condition の値を展開(unquote)。
  • & body と書くことで、複数の式を可変引数として受け取り、~@body で「アンスプライシング展開(リストをスプライスする)」ができる。

使用例

3. マクロの仕組みと展開の流れ

マクロが呼び出されるとき

  1. マクロの呼び出し
    (unless (= 1 2) (println ...)) というコードを読んだとき、Clojureコンパイラはまず「unless はマクロである」と認識される。
  2. マクロ展開
    unless マクロが呼び出されたら、condition には (= 1 2) が、body には *1 のリストが渡される。
  3. 新たなS式を生成
    マクロ本体(defmacro で定義した部分)では、これらの引数を使って (when (not ... ) ...) のS式を組み立て、コンパイラに返えされる。
  4. マクロ展開後のコードがコンパイルされる
    コンパイラは返された (when (not (= 1 2)) (println ...)) を通常のClojureコードとしてコンパイルし、バイトコード等に変換される。
  5. 実行時
    コンパイル結果のコードが実行される。

このように、マクロが生成したコードは最終的に「普通のClojureコード」として評価される。

4. もう少し複雑な例:フォームを加工する

前述の例は、単に条件式の否定を挟んだだけだが、もう少し手の込んだマクロを見てみる。ここでは、複数の式をまとめて「デバッグ出力付き」で実行するマクロ debug-block を定義する。

  • & forms で複数の式を受け取り、map 関数内でそれぞれの式に対して「println を挟むラッパー」を生成している。
  • gensym はユニークなシンボル(ここでは dbgXXXX のようなもの)を作るための関数。マクロ内でローカル変数を作る際に衝突を避ける目的でよく使われる。
  • 最終的にすべてを (do ...) で包んで「順番に評価」できるようにしている。

返り値は最後に実行した式(ここでは 24)になる。

実際には下記のようなコードに展開されているイメージ:

このようにマクロでは、ユーザーが書いたコードを動的に再配置・修飾できるため、デバッグ用途・DSL化・制御構造など様々な場面での柔軟なメタプログラミングが実現できる。

*1:println ...