おっす、おらメタプログラマ!
前回 まで「良いコードとは?」という観点から、「 名前付け」「 スコープ」「 処理の分割」といったプログラミングで必須の基礎内容を解説してきました。今回は少し趣向を変えてメタプログラミングを取り上げます。メタプログラミングは「プログラミングをプログラムする」と言われますが、なんだかつかみどころがない概念に感じませんか? 実際、「 メタプログラミングとは何ですか?」と聞くと、人によって回答がまちまちです。そんなメタプログラミングですが、使いこなすとたいへん強力です。それでは、メタプログラミングについて考えを深めていきましょう。
メタプログラミングの悩み
まずは毎回好例、各界の代表者にメタプログラミングについての所信表明演説をしてもらいましょう。
良い仕事をしたい普通のプログラマ
達人プログラマを目指す 初級~中級のプログラマ
達人プログラマ
さすがに今回は達人プログラマ以外は歯切れが悪いですね。言葉が難しく感じる、使いやすく作るのが難しいという意見が多いようです。まずは「メタプログラミングとは何なのか?」を見ていきましょう。
メタプログラミングとは?
メタプログラミングとはいったい何でしょうか? 普通のプログラムを「普通に処理を並べたプログラムを書くこと」だとすると、メタプログラミングは「ある特定の問題を解決するプログラムを生み出すプログラムを書くこと」です。これではよくわからないので、いくつかの適用場面ごとに具体例を見ていきます。
コードの自動生成
プログラムを自動生成するプログラムはメタプログラムです。たとえばSeasarプロジェクトのS2JDBCGen やDBFlute などは、データベースのスキーマをもとにエンティティクラスなどたくさんのソースコードを自動生成してくれます。Ruby on Rails などのモダンなWebフレームワークにもビューやコントローラを自動生成してくれる機能があります。また、Visual Studioなどの統合開発環境(IDE)で「ぽとぺた」でGUIフォームやデータベースにアクセスできるアプリが作れるのも、マウスからの指示を入力にしたコードの自動生成であり、メタプログラミングの一種です。
自動生成のうれしい点として、単純作業の繰り返しコードを書かなくてよいことや、自動生成されたコードは基本的にエラーや間違いがあり得ないことが挙げられます。
このように「コードを自動生成するプログラム」をプログラミングすることが「メタプログラミング」です。要件が変わった場合は「生成されたコードを修正する」のではなく、「 コードを自動生成するプログラム」を修正することで、「 自動生成されたコード」が変更されるようなプログラミングスタイルになります。
DSL
ある特定の問題領域を解決するための小さな言語のことを「DSL」( Domain Specific Language; ドメイン特化言語)といいます。DSLを用いることでホストとなる言語でプログラムを書くことなくプログラミングできるようになるので、DSLはメタプログラミングの一種であるといえます。
外部DSL
Javaで一番使われているDSLはJSPでしょう。JSPは「HTMLを出力する」という領域を満たすテンプレート言語です。JSPではもととなるテンプレートがプリコンパイルされてJavaコードに変換されます。その後さらにJavaコードがコンパイルされて実行されます。以下は、JSPとプリコンパイル後のJavaコードです。
JSP
<body>
<h1>Cuuby archetype sample app : /hello/</h1>
<c:import url="/common/errors.jsp"/>
プリコンパイル
Javaコード
out.write("<body>\n");
out.write("<h1>Cuuby archetype sample app : /hello/</h1>\n");
if (_jspx_meth_c_005fimport_005f0(_jspx_page_context))
return;
out.write('\n');
JSPのようにホストとなる言語(Java)とは別の言語(JSP)を外部から読み込むことで実現するDSLを「外部DSL」( または言語外DSL)と言います。外部DSLとしてはXML形式の設定ファイルが利用されることが多いです。以下はSeasar2の設定ファイルです。
<component class="helper.Printer">
<initMethod name="addPrinterName">
<arg>"Printer1"</arg>
</initMethod>
</component>
XML形式の外部DSLでは、プログラム(通常はフレームワーク)がXMLを読み込み、内容を解釈しながら処理を組み立てて実行していきます。たとえば上記のXMLをSeasar2が読み込むと次の処理が実行されます。
helper.Printerクラスのオブジェクトを動的に作成する
作成したオブジェクトに対して"Printer1"を引数にaddPrinterNameメソッドを実行する
このように外部DSLは、ときとして設定ファイルと見分けがつかないことがあります。メタプログラミングの入り口としては、外部DSLを「設定ファイルに毛が生えたようなもの」ぐらいの軽いものとして考えてもよいのではないかと思います。
まったくオリジナルのミニ言語を作成して外部DSLとして利用する場合もあります。その場合はパーサ作成のコストやオリジナル構文を新たに考えるコストが必要なので、通常は何かの言語に似せるかXMLなどのような汎用データ形式を利用することが多いようです。本稿ではのちほど汎用データ形式であるExcelを使ってDSLっぽい小さなフレームワークを作成します。
内部DSL
次は内部DSLの例を見てみましょう。以下はRuby on Railsのルーティング処理です。
ActionController::Routing::Routes.draw do |map|
map.connect '/todo/:id',
:controller => "todo", :action=> "show"
end
一見、設定ファイルや独自言語のようにも見えますが、通常のRuby構文のみで表現されています。このようにホストとなる言語のみで実現するDSLを「内部DSL」( または言語内DSL)と言います。Rubyではメソッド呼び出しの括弧が省略可能だったり、do~endでブロックが使えたりなど内部DSLを簡単に実現できる環境がそろっています。内部DSLではホスト言語の構文のみで実現するので、新たに独自の構文を覚える必要はありません(フレームワークの作法やAPIは覚える必要があります) 。また、言語のパワーをフル活用できるので、たとえば繰り返しや分岐などの制御構造と組み合わせて使用できるといったメリットもあります。
Javaのように構文の自由度があまり高くない言語では、Rubyのような内部DSLを行うことは難しい部分があります。限定的ながらJavaで内部DSLを実現する方法としてアノテーションが挙げられます。以下はWebアプリケーションフレームワークCubby のアクションメソッドに対するアノテーションです。
@Accept(RequestMethod.GET)
@Path("/todo/{id}")
public ActionResult show() {
...
}
これは「/todo/{id}というパスでGETアクセスの場合、showメソッドが実行される」という意味になります。設定のようでもありますが、フレームワークの挙動を変えるので内部DSLであるとも言えます。
ただ、アノテーションは利用できる個所に制限がありますし、複雑なアノテーションは可読性が落ちてしまうので、上記の例のようにフレームワークにメタ的な情報を渡したり、フレームワークに対する目印を付けたりといったシンプルな利用形態が多いです。またJavaでも「流れるようなインタフェース」注1 を利用することでもう少し言語っぽい内部DSLを実現できます(コラム参照 ) 。
COLUMN 流れるようなインタフェースとstaticインポートによる内部DSL
S2JDBC やEasyMock などのフレームワークでは、「 流れるようなインタフェース」と呼ばれるメソッドチェインを利用した可読性の高いAPIを提供しています。
以下はS2JDBCのコードです。
List<Employee> results = jdbcManager.from(Employee.class)
.join("department")
.where("id in (? , ?)", 11, 22)
.orderBy("name")
.getResultList();
また、以下はEasyMockのコードです。
expect(mock.voteForRemoval("Document"))
.andReturn((byte) 42).times(3)
.andThrow(new RuntimeException(), 4)
.andReturn((byte) -42)
上記のコードを薄目で見て、括弧とドットがないものとして考えてみてください。まるで独自のミニ言語(=DSL)のように見えませんか? このように、流れるようなインタフェースを用いると、Javaなど静的言語では難しい内部DSLを実現できるようになります。通常のメソッド呼び出しのみで実現できるため、副次的な効果としてIDEのコードアシスト機能を使ってさくさくコーディングができるというメリットもあります。
ただし、流れるようなインタフェースを使いやすく作るには、APIのセンスが必要で設計にも時間がかかります。慎重に検討して、ポイントを押さえて導入するのがよいでしょう。
また、流れるようなインタフェースと同様、Java 5から導入されたstaticインポートも内部DSLっぽい記述を実現する際によく活用されます。先ほどのEasyMockのexpectメソッドがstaticインポートされたメソッドです。