BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル CLR上でのドメイン特化言語の構築

CLR上でのドメイン特化言語の構築

ドメイン特化言語は最近非常に人気が高まっている話題です。これは恐らく、Rails現象に起因していると考えられます。Railsの人気と、Railsにおけるドメイン特化言語(以降、DSL)の大規模な使用は、DSLに対する広範な関心を呼び起こしました。

最近まで、開発者は、DSLを構築するにはコンパイラ理論の専門知識を持ち、LexとYaccの内部構造を理解し、一般にDSLの構築にかなりの時間をつぎ込む心構えでいる必要があると思っていました。そのため、今までに試みた人はごく少数で、実施する際には、ゼロから独自の言語を構築する方法を取っていました。

その方法は高いコストがかかる傾向にあります。

同時に、動的言語の支持者は、トラブルなくDSLを構築するためにお気に入りの言語の動的性質を利用することが可能でした。実際、彼らの多くにとって、このアプローチは非常に複雑なアプリケーションを開発する際の標準となっています。

2つのアプローチの間には大きな違いがあります。独自の言語を構築する最初のアプローチは、外部DSLと呼ばれます。これは、演算子優先規則や、コード、エラー処理、およびI/Oを実行するランタイムライブラリを考慮に入れ、ゼロからすべてを構築する必要があるため、取り組むには費用のかかるプロジェクトです。ホスト言語を利用してそれを編集する2つ目のアプローチは、内部DSLと呼ばれます。内部DSLは構築および保守がはるかに容易です。独自の編集について気にかければよいだけです。その他すべてのこと(一般にあまり気にかけないこと)は、すでにホスト言語によって処理されています。

別のアプローチは、流れるようなインターフェースを構築し、それをDSLと呼ぶことです。私はこれをDSLとはみなしませんが、他の一部の人はDSLとみなしています。これは、構文の自由が厳しく制限される言語でよく取られるアプローチです。JavaとC#はそのような言語の好例です(Java 6とC# 3を含めます)。あなたは言語指向のAPIに取り組むことはできますが、これは私がDSLとみなすレベルには程遠いものです。

私の好みは、ほとんどすべての場合、構文の柔軟性が高い言語に基づいた内部DSLに取り組むことです。私は、ほぼ例外なくCLRで作業する傾向があるため、このプラットフォームで動作するホスト言語の使用を望みます。それにより、骨を折って得たCLRに関する知識の多くを再利用できます。この利点を過小評価しないでください。手近に馴染みのある環境を持つことは、極めて重要です。

言語について掘り下げる前に、まず「構文の柔軟性が高い言語」とは一体何でしょうか? 内部DSLに適したホスティング環境を提供するために、言語にはどのような機能が必要でしょうか?

私には自分の考えを示すために適切な手段が必要です。意図的に名前を明らかにし、ドメイン特化の概念を表現し、そして一般に汎用プログラミング言語のアプローチから離れることで、これを達成できます。あなたは第四世代言語(4GL)を作成し、それを容易に行えるようになることを望んでいます。スプレッドシートを使ったスクリプトに使用する単純なDSLの例を挙げましょう。

あなたのタスクは、乗法グリッドを作成することです。

for x in range(100):
for y in range(100):
cell[ x+1 , y+1 ] = x * y
formula x, 100, sum( x1, x100 )

これはあまり印象深いものではないですよね? プログラミング言語とほぼ同じように見え、コードは些細なものです。Excelオートメーションを使用して同じことをしようとすることを除けば、APIは決して簡単で些細なものではなくなるでしょう。

ここにあるコードが存在するすべてのコードであることに注目してください。クラス定義もメインメソッドも必要ありません。これは、構文上のお荷物なしで直接実行できるDSLスクリプトです。

前の例であまり感心しなかったのなら、数量割引のビジネスルールを定義するのはどうでしょうか。

apply_discount_of 5.percent:
when order.Total > 1000 and customer.IsPreferred
when order.Total > 10000

suggest_registered_to_preferred:
when order.Total > 100 and not customer.IsPreferred

これはプログラミング言語とは全然似ておらず、ビジネスアナリストがワード文書でルールを定義する方法に似ています。

私の見方では、どちらの例もドメイン特化言語です。これらは単純に、ドメインを表現するためのスタイルとアプローチが異なるだけです。どちらにおいても、私たちは自身のドメインに直接関連しないすべてのことを言語から実際に取り除きました。このため、ドメインに集中して、願わくはそれに対処する優れたツールを持つことが可能です。

ドメインの概念以外を取り除くことは、ドメインに一致する構文を持つことと同じくらい重要です。

CLR上で構文の柔軟性が高い言語を検討する際、さまざまな選択肢があります。それらのいくつかを評価してみましょう。Microsoftが開発した言語から見ていきます。

C# - これは非常に強固な言語で、型定義の、スタンドアロンメソッド/コードのない、柔軟性のない構文です。これらの特徴はすべて、C#をDSLホスト言語には好ましくない選択にしています。可能ではありますが、他のアプローチほど良くはありません。

VB.Net - VB.Netは、多数の英単語をキーワードや演算子として使用するため、実のところ言語指向のプログラミングにはるかに適しています。残念ながら、非常に冗長な言語でもあるため、ドメイン概念に対して冗長性を排除することを望みます。

JScript - おかしいかもしれませんが、JScriptは非常に柔軟な言語であり、多くのことに対してかなり良い構文を提供します。あらゆるJavaScriptライブラリに目を向けてください。JScriptは結局JavaScriptと同じ機能を提供します。そして、いかに柔軟に実現できるかを理解するためには、JQueryやPrototypeなどのライブラリを調べればよいだけです。しかし、もはや開発されていないため、JScriptにどんな将来があるのか分かりません。また、多くのことに対して柔軟な構文を持ちますが、まだプログラミング言語感があるため、それがDSLで妨げになると思います。

F# - これは将来的に出荷されるであろう、Microsoftが開発した関数型言語です。F#は言語指向のプログラミングをサポートします。私はその言語にざっと簡単に目を通しました。F#の能力は優れていますが、私の見方では、他の何よりもBNF定義に似ています。これは明らかに、関数型プログラミング言語の経験が作者に欠けているという問題ですが、私はF#を読みやすいとは見なしません。

Microsoftが開発した言語については終わりです。もう少し先の領域に進みましょう。CLRは全部合わせて100以上の言語をサポートするため、私がDSLホスト言語に有用な候補だと思う2つの言語をピックアップします。

Nemerle(リンク)は、コンパイラマクロ(C++ではなく、Lispの種類のマクロ。その詳細は後ほど)、およびDSLのホスト言語に適したターゲットとなるその他多くのものをフルサポートするマルチパラダイム言語(OOおよび関数型)です。単純な理由の最たるものは、Nemerleコードを実際に読むことができるということです。

Boo(リンク)はPythonベースの構文を持つ静的型付けOO言語です。マクロ(ここでもLispの種類のマクロ)、オープンコンパイラのパイプラインをサポートし、DSLの構築を容易にするよう明示的に設計された特有の機能を持ちます。Booは私がDSLの構築に推奨する言語ですが、うわべだけでも客観性を確保するために、Booがどれほど強力であるかを証明する前に次の話題について確認する必要があります。

DLRはどうでしょうか?

これまで、動的言語ランタイム(DLR)に関する話を省いてきました。DLRは、CLR上で動的言語(Ruby、Python、EcmaScriptなど)をサポートするためのMicrosoftプロジェクトです。

さらに具体的に言うと、人々はDLRについて考える際、IronRubyとIronPythonについて考えます。

Rubyは内部DSLの記述に適していると判明した言語であり、CLR上で実行すると、馴染みのある環境で作業できることが確実になります。

DLRをDSLのプラットフォームとして使用することは確かに可能ですが、私は少なくとも当面はそれを避けるでしょう。DLR、およびIronRuby自体は進行中の未完成品です。リリース日に関する確約はまだ何もないと思います。さらに、私はRubyで実行できてBooで実行できないことをあまり発見していませんが、Booのメタプログラミング機能が非常に自然かつ極めて強力であることに気付きました。

「自然かつ極めて強力」とはどういう意味でしょうか?

もう少し深くBooについて検証しましょう。私は、Booにはオープンコンパイラがあると言いました。それはオープンソースであるという意味ではなく(オープンソースではありますが、それは関係ありません)、コンパイル時にコンパイラに手を伸ばしてコンパイラの内部オブジェクトモデルを操作する方法があるという意味で言いました。つまり、興味深い方法でコンパイラの動作方法を変更できることを意味します。

上記の2つのコード例は、どちらもBoo DSLコードです。

Booのメタプログラミング機能に関する全詳細を話すことは範囲外ですが、その能力を発揮する簡単な例を示すことはできると思います。

CLRには、IDisposableの概念と、それに合うusingステートメントがあります。今から、ITransactionableを定義し、それに合うtransactionステートメントを定義します。

public interface ITransactionable:
def Dispose():
pass
def Commit():
pass
def Rollback():
pass

macro transaction:
return [|
tx as ITransactionable = $(transaction.Arguments[0])
try:
$(transaction.Body)
tx.Commit()
except:
tx.Rollback()
raise
finally:
tx.Dispse()
|]

このコードだけで、最初のクラス言語エレメントとしてtransactionステートメントを使い始めることができます(実際、これはまさにusingステートメントがBooで実装される方法です)。

transaction GetNewDatabaseTransaction():
DoSomethingWithTheDatabase()

現在、トランザクション内のコードが例外を出すと、トランザクションは自動的にロールバックされます。正常な場合は、自動的にコミットされます。

しかし、それは単に言語で実行できることのデモンストレーションです。そして、ここで紹介している唯一の新しい概念は、マクロと、おかしな[| |]記号であることに注目してください。深入りせずに、トランザクションブロック内のコードとマクロの内容との構文の置換を行うようコンパイラに指示します。

これはテキスト置換を超えるものであることに注意することが重要であり、私たちはAST(抽象構文木 - コンパイラオブジェクトモデル)を編集しています。これは些細(ただし強力)な一例です。しばらく、より複雑なシナリオを検証し、この区別がなぜ重要であるかを示します。

DSLの構築には、このレベルさえほとんど必要になりません。メタプログラミングのオプションを使用せずに、言語構文を使用するだけで達成できます。Rubyと同系のBooには多数のオプション構文があり、多くのシナリオで非常に役に立ちます。たとえば、メタプログラミングに頼らずに、Booの構文の機能を利用して、同じ構文を作成できます。これにより、最後のパラメータがdelegate(クロージャ、ブロックなど)である場合、コードブロックをメソッドに渡すことが可能です。

例:

def transaction(tx as ITransactionable, transactionalAction as ActionDelegate):
try:
transactionalAction()
tx.Commit()
except:
tx.Rollback()
raise
finally:
tx.Dispse()

前と同じように、次のコードを使用できます。

transaction GetNewDatabaseTransaction():
DoSomethingWithTheDatabase()

構文の観点からすると、違いはまったくありません。ただし、2つのバージョン間には微妙な違いがあります。CLRは、'try'ブロックに先行する命令が成功すると、'try'ブロックが入力されることを確実にします。このことは、using()ステートメントの正しい機能、およびその他多くのシナリオにおいて非常に重要です。

最初のバージョンはこの能力を活用できますが、2つ目のバージョンは活用できません(その理由は、2つ目のバージョンにはランタイム時のメソッド呼び出しが伴うからです。一方、最初のバージョンは単にトランザクションブロックを、編集した結果に置き換えるだけです)。

Booのメタプログラミングで他に何ができるでしょうか? あなたはそれに関する本をかなり執筆できるかもしれません(実際、私はそれに関する本を執筆しています:-)。必ずしも優れた設計の最高の例ではありませんが、簡単な例として、あなたは'if'ステートメントのセマンティクスをこの言語で変更できます。

私は、次のパターンで'if'ステートメントを変更した際に、一度それを実行する必要がありました。

if foo == null:
# do something

次のパターンに変更:

if foo == null or foo isa NullObject:
# do something

nullを要求するときはいつも、オブジェクトがNullObjectのインスタンスであるかどうかもチェックします。NullObjectは私のアプリケーションにおけるカスタム型です。そのため、アプリケーションを通して、自然な方法でNullObjectパターンを使用できます。これは、次のコード例が"Value is null"を印刷することを意味します。

val = NullObject() # set val to a new instance of NullObject
if val == null: # will be compiled as val == null or val isa NullObject
print "Value is null"
else:
print "Value is not null"

私たちは、NullObjectから継承するすべてのオブジェクトをnullとみなすように言語を拡張しました。

言語のそうした基本的な部分に入って変更できることにより、私の仕事(そして、言語の使用)は長い目で見るとはるかに容易になりました。

先へ進む前に、最後の1つの例です。どのようにして(単純な)「契約による設計(design by contract)」(クラス不変表明)を、およそ20行のコードで、Booアプリケーションに追加できるかを示したいと思います。次のとおりです。

[AttributeUsage(AttributeTargets.Class)]
class EnsureAttribute(AbstractAstAttribute):

expr as Expression

def constructor(expr as Expression):
self.expr = expr

def Apply(target as Node):
type as ClassDefinition = target
for member in type.Members:
method = member as Method
continue if method is null
block = method.Body
method.Body = [|
block:
try:
$block
ensure:
assert $expr
|].Block

使用:

[ensure(name is not null)]
class Customer:
name as string

def constructor(name as string):
self.name = name

def SetName(newName as string):
name = newName

これで、名前をnullに設定するどんな試みも表明(assertion)例外を発生させます。この技法はかなり強力で、非常に使いやすいものです。私は、読者のために練習としてprecondition属性の記述を残しておきます。

この例は、コンパイラオブジェクトモデル(AST)を直接操作する力も実証します。C++マクロと同様に、テキスト置換に限られません。オブジェクトモデルにクエリーを実行し、それを非常に自然な方法で編集できます。さて、そろそろBooが素晴らしい言語であり、DSLの構築に非常に適しているということを納得してもらえたと思います。私は潜在的な可能性のごく表面をなぞっているにすぎません。さらに発見することがたくさんあります。

他のいくつかの利点を挙げます。Booは静的にコンパイルされた言語であるため、DSLは標準のCLRコード(JIT、GC、デバッギングなど)のすべての利点を持つことになります。性能の観点からすると、DSLコードとアプリケーションコードの間に違いはありません。

したがって、BooをベースとしたDSLは、頻繁に変更する必要があり高性能を要求するコードセクションにとって理想的な候補です。 稼動中のものを変更する必要があるという一般的な要件は、たいてい人々をXMLベースのシステムやルールエンジンなどに押し進めます。「XMLでプログラムする」の議論全体を考えなくても、そうした選択は性能が不十分です。

DSLスクリプトのセットを利用するシステムの構築は簡単であり、高性能を提供し、長い目で見ると非常に保守が容易です。また、Domain Driven Design(ドメイン駆動設計)の概念ともよく合います。ドメイン特化言語はドメインの概念を自然に表現することを容易にするからです。

公表されているBoo DSLがいくつかあります。

私の個人的なお気に入りはBinsorです。BinsorはCastle Windsor IoCコンテナ用の構成DSLであり、先進のIoC概念の扱いを順調に進ませます。Binsorの詳細については、Binsor 2.0(リンク)の発表にアクセスして学ぶことができます。その他、Booでの興味深いDSLは次のとおりです。:

Specter(リンク)は、ビヘイビア駆動開発(BDD)フレームワークであり、仕様の記述に非常に自然な構文を提供し、仕様を標準のNUnitテストに変換します。

Brail(リンク)は、テキストテンプレート言語です。

他にもいくつかありますが、それらは非常に小さなニッチをターゲットとしており、広く知られていません。

DSLの記述には初期の知識が必要ですが、その知識は簡単かつすぐに習得できます。このベース知識を得ると、DSLの記述がフォームの記述と同じくらい困難であるレベルにまで到達できます。

実際、私はさまざまなソースからのメッセージを処理するために、主にDSLから構成されるバックエンド処理システムを記述しました。そのシステムでは、プレゼンテーション層でフォームを記述していただろう頻度とほぼ同じくらいDSLを記述していました。

結論として、BooはDSLを構築するのに非常に優れた言語です。DSLの記述にBooを使用すると、柔軟性や性能を妥協することなく、DSLの記述にかかるコストを大幅に削減できる傾向があります。さらにBooは、構文の自由と、「自然な」方法でドメイン概念を表現する能力を提供します。

そして極めつけは、BooはJavaでも動作します(リンク)

著者について

Oren Einiは、Ayende Rahienとしてよく知られている、経験豊富な.NET開発者兼設計者であり、NHibernateやCastleなど、いくつかのオープンソースプロジェクトの有名な貢献者です。また、Ayendeは、Rhino Mocks、Rhino Commons、およびNHibernate Query Analyzerの創始者です。Booに関してAyendeは、Brail、Castle MonoRail用のテンプレート言語、Binsor、Castle Windsor IoCコンテナを構成するためのDSLを作成しました。また、「Building Domain Specific Languages in Boo」(リンク)という本を執筆中です。

原文はこちらです:http://www.infoq.com/articles/dsl-on-the-clr
(このArticleは2008年4月21日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT