流行について
Clojure(LISP系)、Ruby、elixirをはじめ最近開発されたり注目されている言語では、メタプログラミングの要素を持つものが増えてきている。また、ScalaにMacroが実装されるようになったり、既存の言語でも言語拡張によりその要素を取り込もうとする流れがある。
これは、最近のプログラミング言語においてはメタプログラミングが流行となっているといってよいだろう。
ここでそのメタプログラミングについて、さらっとした理解をしていこう。
メタプログラミングとは
メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。主に対象言語に埋め込まれたマクロ言語によって行われる。
(wikipedia:メタプログラミング)
例)LISP,closure,Scala,elixirのマクロ,Protocol Polymorphism、Rubyのdefine_methodおよびattr_readerメソッドによるクラス設計、GroovyのAST変換と実行時拡張(metaclass)と
上記の例の他、自由度が高いとされる言語では、記述方法自体を記述できるメタプログラミングの要素を持っていることがおおい。
メタプログラミングの実例
- マクロによる実例(Commpn Lisp)
(defmacro nif (expr pos zero neg)
`(case (truncate (signum ,expr))
(1 ,pos)
(0 ,zero)
(-1 ,neg)))
マクロの動作は普通の関数とは違っている. マクロはどのように,そしてなぜ違うのかを知ることは,マクロを正しく使うための鍵だ. 関数は結果を生むが,マクロは式を生む ---そしてこの式が評価されると結果を生む.
(On Lisp --- マクロ 本文邦訳 ― Paul Graham著,野田 開 訳)
上記の定義ののち、Lispはパースされたコードにnif
というマクロ式を発見すると、上記の定義の式に展開する。その後その式を評価する。この式にはさらにマクロ式が含まれることがある。
- プロトコル多態性による実例(elixir)
defprotocol Blank do
@doc "Returns true if data is considered blank/empty"
def blank?(data)
end
上記の定義は、blank?
関数を持つBlankプロトコルを定義する。このプロトコル定義を行うことで、以降任意の型に対してこのプロトコルを拡張できる。
defimpl Blank, for: Integer do
def blank?(_), do: false
end
上記の定義は、BlankプロトコルのInteger型に対する定義である。この定義によりBlankはInteger型を引数として、次の式を評価できるようになる。
> Blank.brank?(0)
false
> Blank.brank?(1)
false
上記のように自分で型を定義したときなど、必要なときにBlank.brank?
によるその型に対する評価の方法を定義できる。このような方式での多態性の実現もメタプログラミングの一種と考えられている。
- 実行時拡張の実例(Groovy)
Integer.metaClass.double = { ->
delegate * 2
}
println(1.double()) // 2
上記のコードは実行時にIntegerクラスがdouble
というメソッド(実際にはクロージャ変数)を持つように拡張する。メタクラスに追加された以降では、
- コンパイル時メタプログラミング AST変換の例(groovy)
「(Groovy で AST 変換してみた ブログ記事) から 抜粋」
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import org.codehaus.groovy.transform.GroovyASTTransformationClass
@Retention(RetentionPolicy.SOURCE) // クラスファイルにはAST変換の情報は不要
@Target([ElementType.METHOD]) // 対象はメソッドのみ
@GroovyASTTransformationClass('HogeHogeTransformation')
public @interface HogeHoge {
String value() default 'ほげほげ'
}
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.builder.AstBuilder
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation
// CompilePhaseについては後述
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class HogeHogeTransformation implements ASTTransformation {
@Override
// astNodes[0] : アノテーションを表すノード
// astNodes[1] : アノテーションがつけられた要素(今の場合はメソッド)を表すノード
// sourceUnit : グローバルAST変換で使用(今は不要)
public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
def (annotationNode, annotatedNode) = astNodes
// アノテーションの引数を取得
// (デフォルト値はアノテーション側で指定した値を取得したかったが、できないようなので仕方なくここで設定)
def value = annotationNode.getMember('value')?.value ?: 'ほげほげ'
// メソッドの始めと終わりにprintlnを挿入
annotatedNode.code.statements.with {
add(0, createPrintlnStatement(value))
add(createPrintlnStatement(value))
}
}
// 「println 文字列」というコードを表すノードを作成して返す
def createPrintlnStatement(str) {
new AstBuilder().
buildFromString(
CompilePhase.SEMANTIC_ANALYSIS,
false, // 戻り値のListの第2要素(全体を表すノード)が必要な場合はtrue
"println '$str'"
).first()
}
}
上記では、コンパイル時に行われる構文解析の結果得られたAST(抽象構文木)を操作することでコンパイルの結果得られるバイトコードを変更している。
メタプログラミングとDSL
このようなメタプログラミングの機能を持った言語では、単にプログラミングにおいて効率的な記述ができるだけではなく、特定の領域に特化し新たな言語ともいえる構文を生み出すことができる。
そうした特定領域のための言語をDSLという。
DSL(domain-specific language/ドメイン固有言語)とは
ドメイン固有言語(ドメインこゆうげんご、英: domain-specific language、DSL)とは、特定のタスク向けに設計されたコンピュータ言語を意味する。ドメイン特化言語あるいは単にドメイン言語とも呼ばれるが、学術的にはドメイン特化言語と呼ばれることが多い。C言語やJavaのような汎用のプログラミング言語の対照とされる。
(wikipedia:ドメイン固有言語)
DSLは、大きく内部DSLと外部DSLに分類することができます。
内部DSL: 内部DSLは、汎用のプログラミング言語の書き方を工夫して、見かけ上の構文を自然言語に近づけた言語です。
外部DSL: 外部DSLは、汎用のプログラミング言語とはまったく別の構文を持ったDSLです。(一部抜粋 ドメイン特化言語 - プログラマが知るべき97のこと Webドキュメント ― Michael Hunger)
例)内部DSLの例:、Rubyのテスト記述FWであるRSpec、Groovyビルド定義記述用ツールのGradle
外部DSLの例:ハードウェア記述言語であるVerilog、データ操作特化言語であるSQL、Webページ生成特化言語であるPHP(但し、PHP5以降では言語の拡張によりDSLの範疇から外れてきている)
内部DSLは、言語の標準機能による拡張であるため、フレームワークや特定作業の支援ツールとして提供されることが多い。
DSLは、特定領域に特化した言語であるため、特定領域の専門家が理解しやすかったり、記述する際に汎用言語の持つ煩雑さを排して自然な思考で記述することができたりするメリットがあるとされている。
しかしながら、正しい適用範囲を探し、設定し、維持することが難しくDSLの設計開発自体にコストがかかること、学習コストに対し適用できる領域が限られていることなどの問題も指摘されている。
(雑談) 最近のメタプログラミングの流行について
メタプログラミングが言語の高次元での操作を実現し、記述方法の自由度を高める手法であることより、この機能を持った言語では、書き方の工夫により構文を作り上げる内部DSLとの親和性が高い。
プログラミングをはじめIT技術が利用される領域拡大され続けているなか、DSLのような特定領域での利用に特化したプログラミング方法が求められている一方で、DSLの問題点として指摘されることは開発と学習コストに見合った効果を得られないことだった。
そこでメタプログラミングの機能が扱いやすい汎用言語を使用することで、必要な領域に必要な分の要素を必要な時につぎ足すことができ「正しい適用範囲を探し、設定し、維持することが難しい」という問題を解決している。また、内部DSLでは既存の汎用言語の拡張であるため、学習した分のコストが他の領域でも生かしやすいことで、学習の意欲につながっていると考えられる。
##【参考文献】
- 実践プログラミングDSL amazon.jp ― Debasish Ghosh (著), 佐藤 竜一 (監修, 翻訳)
- On Lisp --- マクロ 本文邦訳 ― Paul Graham著,野田 開 訳
- Scala document - def macro Webドキュメント ― Eugene Burmako 著,Eugene Yokota 訳
- elixir - Protocols Webドキュメント― Plataformatec.
- ドメイン特化言語 - プログラマが知るべき97のこと Webドキュメント ― Michael Hunger
- Groovy で AST 変換してみた ブログ記事