ドメイン特化言語(DSL)の利用は現実的なものとなり、様々なソフトウェア開発プロジェクトにおいて、ソフトウェア開発者にとって必要なソリューションとなっています。DSLという言葉は既に聞いたことがあると思いますが、 DSLが内部DSLと外部DSLという異なる形式に分かれているということを知らない人もいるでしょう。さて、内部DSLと外部DSLとは一体何でしょうか?どちらにするかをいつ決めたら良いのでしょうか?そして、そもそも、複雑な外部DSLの開発を始めるためにはどうしたら良いのでしょうか?本稿では、これらの質問に答えつつ、複雑な外部DSLの開発にフォーカスします。
ドメイン特化言語を定義する
ドメイン特化言語(DSL)は、与えられた問題領域の必要なものに対処する上で特化して開発されるコンピュータ言語です。ドメインそれ自体は、多くのものがあるでしょう。それは、保険、教育、航空、医学など、業界に特化したものがあります。あるいは、JEE、.Net、データベース、サービス、メッセージング、アーキテクチャ、ドメイン駆動設計といった、技術や方法論に特化したものもあります。
私がDSLを開発する理由は、私が作業しているドメインの中の一連の課題を、よりエレガントに、そしてより扱いやすくするためです。そして、私が作る言語が、ちょうど私が独自に対処したいと思っていることだからです。そして、もちろん、私が自身の言語を他の人に提供するならば、彼らが必要とする対処に少しでも貢献しなければなりません。しかし、それ以外のなにものでもありません。その努力によって、汎用プログラミング言語や対象外のツールを使うよりも、DSLを使用する方がより自然なことであると感じることにつながります。
本稿での重要な区別は、内部DSLと外部DSLにあります。それぞれはDSLの形式ですが、特定の問題領域に対してどの形式が適切であるかを理解するのが重要です。なお、一般にどのようなDSLを定義するのか、内部DSLと外部DSLで何を作るのかについては、本稿の対象外となります。Martin Fowler氏を始めとして、そのような知見について先頭を走っているので、その主題に関するより詳細な内容は、彼らの作業結果を参考にすることをお勧めします。ここでは、基本的な内容について説明します。
内部DSL
それは、Java、C#、Rubyといった、あなたのプロジェクトで使われている主要な汎用プログラミング言語で実装され、それと深く関連して開発された言語です。
Railsフレームワークは、Rubyで書かれたWebアプリケーションを管理するための、RubyベースのDSLと呼ばれています。RailsがDSLと呼ばれる理由の一つは、汎用的な目的のRuby言語とは対照的にRailsがプログラミングでは無いように思えるのに対して、Ruby言語の機能のいくつかがプログラミングをするのに利用されているからです。言語として考えたとき、その基盤がそれ自体で言語となるのにとても有利なスタートであるとして、RailsはRuby上で作られました。
Dave Thomas (PragDave)が、Rails全体をDSLとして考えているのかは私にはわかりません。しかし、Railsのいくつかの機能が異なるDSLによって支えられていると彼は言っています。彼が示す例は、DSLとしてのActive Record宣言です。ドメインモデルのエンティティの関連に特有な、いくつかの単純な専門用語を使用することで、Rails開発者は、それ自身が高度なエンティティ関連の考えにフォーカスしている間、DSLによってもろもろの複雑なインフラやオペレーションを裏で管理することができます。
クリエーターや巨大なコンシューマの基盤が、Railsを包括的なDSLとして考えようが、Rails内のいくつかの機能(Active Record宣言)だけと考えようが、私がここで議論しているのは、内部DSLです。このDSL形式が内部という名前が付けられているのは、重ねて言いますが、主要なプログラミング言語と深く結びつき、その上で実装されているからです。しかし、それ自身で専門言語を含んでいるかのように思える技術を採用しています。
内部DSLの要求に適したフレームワークやライブラリに関するAPIの特徴を定めているポイントの一つは、Martin Fowler氏やEric Evans氏によれば、それが流れるような(fluent)インターフェースを持っていることです。それは、基本的には、より自然に読むためのより長い表現を形成するために、短いオブジェクト表現をつなげることができるということを意味しています。
私は、暫くの間、断続的ですが流れるようなAPIを設計し、それを利用していました。例をあげると、流れるようなAPIにおける初期経験のほとんどは、Smalltalkでした。Smalltalkで流れるようなインターフェースを開発し利用するには、2つの方法があります。1つは、次のメッセージのレシーバとなるあるオブジェクトメッセージ表現(メッセージ呼び出し)から答え(返り値)を得ることです。
1 + 2 + 4
これは、数値(オブジェクト)1が引数として数値(オブジェクト)2を持つ + メッセージを受けます。そして、その結果である数3(暗黙)が引数として数値(オブジェクト)4を持つ + メッセージを受けます。(明確にするために、Smalltalkでは数値リテラルはプリミティブではありません。ファーストクラスオブジェクトです。)もちろん、その本質がプログラミングでさえないことから、これが誰にとっても自然なことのように思うでしょう。そう、そこがポイントなのです。私は、次のようにして同じことを達成することができます。
result := 1 add: 2. result := result add: 4.
しかし、これは自然でないし、流れるようでもありません。一見すれば、流れるような表記はそれが明らかに7であることが分かります。従って、これは第2の例とはあまり言えません。そして、この技術は数の数学領域に限ったことではありません。Smalltalkプログラミングの流れるような性質により、多くの異なるドメインにおけるドメインに特化した側面を扱うことが容易になります。Smalltalkカスケードについて調べてみてください。それは、流れるようなインターフェースを支援するための第2言語としての機能です。それは、現代のオブジェクト指向言語によってサポートされているので、私は、初めのアプローチを実際にやってみました(しかし、数値リテラルの扱いで一部異なる点があります)。
ここで重要なことは、与えられたAPIの周囲にある流暢さは、与えられた問題領域をよりエレガントで効率的に扱うために設計されているということです。それは、ドメインのエキスパートにとって自然なことです。それが、DSLを作るということです。
もちろん、私たちにとって最も有用な方法で、私たちがこのようなことを考えるのは自由です。私たちがDSLとして与えられたAPIを考慮することを個人的に選択してもしなくても、それは私たちの選択です。しかし、Martin Fowler氏たちは、私が上述したものを内部DSLと識別しているということを認識することは重要です。彼らは、今後この業界が考え、行うことに対し、多くの影響を与える傾向があります。それは、しばらくの間議論の話題となり、そこで固執しそうな用語です。
私の経験上、私たちが私たちと仲間の開発者のための技術的なAPIを必要とするとき、私たちは内部DSLを設計、開発する傾向にあります。私たちがこの形式をサポートするための豊富な機能セットを持っている内部DSLの基礎を汎用プログラミング言語に置くとき、それは役に立ちます。明らかに、SmalltalkやRubyのような言語は、それをより容易なものとします。JavaやC#のような言語では、簡単にはいきません。有能なソフトウェア開発者を流れるようなAPIやその他の内部DSLの機能の対象とすることで、複雑さや開発時間を削減します。しかし、私たちがプログラマーでない領域の専門家に対して単純化して力を注ごうとしているのならば、内部DSLは選択肢にはなりません。
外部DSL
正直に言うと、私はドメイン特化言語の作業として、流れるようなAPIの利用、設計、開発について考えていませんでした。それ故、流れるようなAPIの長い歴史によって、内部DSLの概念に入り込むことを困難にしていたということを認めなければなりません。しかし、私は学んでいます。一方、私がDSLについて初めて知ったとき、私はその定義から「ちょっとした言語」をいくつか作成している私の作業をすぐに連想しました。あなたが上述した内部DSLの定義を受け入れるのが困難ならば、私はこれを提案します。この味わいはより良いものだと思います。
外部DSLを定義することは、内部DSLを定義することよりもはるかに容易です。それは、コンパイル型かインタープリタ型の汎用プログラミング言語を作るのに似ています。その言語は形式的な言語をもっています。つまり、その言語は、矛盾なく定まったキーワードや表現形式のみ許すことを強制されます。その言語で書かれたプログラムのソースコードは、テキスト形式の1つ以上のファイル、または、たくさんの表や、グラフィカルな形式となります。テキストのDSLの場合は、テキストエディタか十分な機能を備えたIDEを使用してソースファイルを作成します。あなたはソースコードをコンパイルし、結果として生じるプログラムの一部としてそれを実行します。あるいは、インタープリタによって直接ソースコードを実行します。
汎用プログラミング言語のソースと外部DSLのソースの主な違いは、DSLをコンパイルしたときに、一般的には、直接実行可能なプログラムとして出力されないということです(しかし、それは可能です)。一般に、外部DSLは核となるアプリケーションの運用環境と互換性のあるリソースに変換されます。あるいは、それは、核となるアプリケーションの一部として構築されたり使用されたりする主要な汎用プログラミング言語のソースコードに変換されます。
アプリケーションで使用されるリソースに変換される外部DSLの例として、HibernateやNHibernateによって利用されるオブジェクトリレーショナルのマッピングファイルがあります。別の例として、ThoughtWorksのJay Fields氏による「Business Natural Languages」が紹介されています。あなたは、自身のアプリケーションが必要とするメタデータを含んだ外部DSLの想像がつくでしょう。そう、ユーザー入力のバリデーションです。あなたがメタデータを読み取り、それを効果的な内部形式に変換し、実行時にそれを利用します。
対象アプリケーションのソースコードに変換される外部DSLの例として、MetaCaseのMetaEdit + Workbenchや、Jetbrainsのメタプログラミングシステム(MPS)を使用して開発した言語があります。別の例として、Markus Volter氏の「Architecture as Language」に関する取り組みがあります。この例では、Markus氏がソフトウェアアーキテクチャを定義し、その妥当性をチェックし、テキストのアーキテクチャ記述からコードを生成することができます。
外部DSLは、設計や開発の作業を直接サポートするのに利用されるかもしれません。それ故、それはソフトウェア開発者に利用されます。その場合、それは、内部DSLとして開発された流れるようなAPIを利用するコードを生成するのに利用されることもあります。ユーザーレベルを対象に、適切に設計されるならば、外部DSLは、プログラマ以外の分野の専門家によって利用されるのにも適しています。
多くの場合、ツールなどの言語サポートを伴うのであれば、外部DSLはベストなものとなります。DSLを作った人を含む小さなグループのソフトウェア開発者だけがそのユーザーとなるとき、シンプルなテキストエディタだけあれば十分かもしれません。しかし、DSLを製作者のチーム外に配布したり、プログラマでない分野の専門家を対象としている場合は、シンタックスハイライトやコードアシストエディタがDSLの成功にとって重要なものとなります。その他のツールもあれば良いもの、あるいは必須なものとなるかもしれません。
言語の複雑さ
私は、複雑なDSLを以下のように考えます。
-
パースが容易ではありません。コンマ区切りのテキストファイルは、比較的パースが容易ですが、Javaのような言語は、パースが容易ではありません。複雑な外部DSLは、その2つの間のどこかに位置します。そしておそらく、複雑さの点ではCSVよりもJavaに近いでしょう。(あなたは、CSVの形式的な文法を開発したいと思うかもしれません。しかし、私の主張したいことは、それは本質的なものではなく、場合によっては迅速でない場合もあります)
-
一旦解析された、複雑な内部表現を必要とします。複雑な表現は、使い勝手が良く、最適化された状態のソースのアーティファクト表現を含んだ、木構造かオブジェクトグラフです。この表現は、バリデーション、両方または片方のインタープリタ、生成ツールに対応します。
-
単一のソースファイルや複数のソースファイルから、複数、あるいは多くの複雑な対象アーティファクトを生成する仕組み。その言語が解釈されるだけでなく生成ツールによって利用されるなら、これはまさにうってつけです。あなたがもとのDSLによって提供されていないちょっとしたもので対象のアーティファクトを飾るだけなら、私は、対象となるもとの言語を何故最初に使用しなかったかを疑問に思うでしょう。
上記の状況で、あなたはどうやって複雑な外部DSLを実際に実装するのかを疑問に感じているかもしれません。次のセクションでそれについて説明します。
設計と開発
複雑な言語の設計や開発は、どんなものでも大きな挑戦となります。あなたが言語以外で必要としていることに関する良いアイデアがあるとしても、複雑な言語の詳細について考えることになるでしょう。それは、言語機能全ての識別を行い、構文の決定について考える時のようなものであり、(あなた自身を含む)あなたの将来のユーザー基盤に対し、新しい何かを考えます。これは、汎用プログラミング言語を設計するときよりもより一層多くの回数となるでしょう。
もちろん、私たちのフィールドで有益な何かと同様に、言語も時間と共に強化を重ねるでしょう。私たちがすべきことの1つは、時間と共にそれらの強化をサポートすることが可能となるようにすることです。したがって、言語設計は変化を受け入れる必要があります。さらに、良い言語設計は、言語開発をずっと容易なものとするでしょう。
私は、このセクションで、グラフィカルなものからテキスト形式のものを含んだ広範囲の言語構文について述べました。しかし、紙面の都合から、私はその範囲を1つの構文だけに制限しなければなりません。それ故、私はテキスト形式のDSLにフォーカスすることを選択しました。テキスト形式のDSLは理解が容易で、グラフィカルなDSLを利用する際でも適用できるからです。(理由は以降を参照してください)
構文設計の現在と今後
幸いにも、多くの言語設計者は朝起きずに、「私は、今日言語をつくる予定で、どうしたら良いか悩んでいる。」とは言いません。私たちが言語の開発について考えているならば、私たちは良い考えに対する理由があります。私たちが言語に対して明確なビジョンを実際に持っていなければ、結果として生じる設計は力の無いものとなるため、これは重要なことです。したがって、言語設計で重要なファーストステップは、あなたの言語でやりたいことについて十分理解していることです。
あなたが言語でやりたいことを理解しているということは、必ずしも、その構文がどうであるかを知っていることを意味している訳ではありません。言語構文は、そのユーザビリティにとって重要であるばかりでなく、それを強化するときの適応能力にも影響を与えます。もちろん、言語の構文はその利用者にとって適切なものでなければなりません。あなたの言語のユーザーが、プログラマのような技術寄りならば、その言語構文は、プログラマでない領域の専門家に対して選択するものとは異なるでしょう。
記事の残りでテキストDSLにフォーカスするものの、構文の説明は、それに閉じたものではありません。あなたの言語は2つの構文を持つことができます。その言語のユーザーと接点を持つものと、ファイルに保存、パース、翻訳されるものです。(行と列の表のような)グラフィカルなユーザーインターフェースや、ユーザーが構文を書くのではなく描画する(Visioのダイアグラムのような)記号や形状ベースの言語の時には、あなたはユーザー中心の構文を開発するでしょう。そのような場合、ファイルベースでは、あなたの言語は必要性はあるものの技術的なものとなるかもしれません。私たちは、モデルとしてグラフィカルな構文を考える傾向にありますが、複雑なDSLがテキストのDSLに限定されていないように、モデルもグラフィカルなものに制限されません。すぐ分かりますが、いくつかのポイントについて、構文がどうあるかに関わらず、あなたはモデルの観点で言語について考え始めるでしょう。
あなたの複雑なDSLにおける技術的構文が、将来の機能強化を支えるのに十分な柔軟性を備えていないならば、下位互換性が制限されるか全くないという危険性に直面するでしょう。そして、あなたは機能強化の前に存在したソースを無かったものとして、新しい言語を作るかもしれません。私は、双方の構文をサポートするときにこの危険が増すものと考えています。もちろん、ソースファイル(モデル)を変換ツールで更新することも可能です。しかし、ファイルの構文更新ユーティリティは、機能強化の本質や複雑さから得られるメリットを制限します。さらに、それらはユーザーにとって歓迎されるものではないですし、更新による記述方法の変更も発生します。
私は、適切で拡張可能な構文を選択するためにいくつかの提案をします。それらは、あなたの設計を初期段階から後期まで進展させるものとして、関連する順番となっています。
-
他の言語を調べてください。JavaとC#といった言語が何故そのように設計され、そして、それらが成功しているのかについて考えてください。JavaとC#の特定の構文は、これらの新しい言語の前に存在した言語の開発コミュニティに受け入れられるために設計されています。しかし、他の理由もあります。ブロックを基本としたスコープ言語は、本質的に拡張可能です。何故なら、様々な形式の新しいブロックを既存のブロック形式に隣接して追加することができ、それらをネストすることもできるからです。これは、あなたがそういった言語を新たに考案しなければならないという意味ではありません。それは、あなたの中で成功した言語の特定の側面を再利用することができるということを意味しています。Ruby、Smalltalk、Perl、Pythonといったその他の言語についても考えてみて下さい。それらのどこが好きで、どこが嫌いですか?あなたが任意の言語セットを切り替え、融合することができるなら、あなたは何をしますか?あなたの言語の考えに磨きを書けるために、既存の成功した言語の側面を再利用することはできますか?
-
アジャイルなテクニックを使い、さまざまな構文のアイデアを試してみてください。それぞれの構文を書いてみることでどのように感じますか?他の人たちはその試験的な構文をどのように感じますか?その構文は形式的文法として定義することはできますか?あなたのお気に入りの構文に対して、ツールのサポートはどうでしょうか?
-
できるだけ多くの言語機能について確認してください。(2で述べたように)それらの機能の上位70-80%をサポートするさまざまな構文を試し、残りは保留します。あなたが勝利の構文を得られたと思ったら、残りの20-30%の保留していた機能を追加することを考えてください。その言語の初期バージョンは、不安定ですか、それとも拡張可能ですか?保留していたり後に追加する機能とは別に、いくつかの構文を意図的に誤った形で実装してください。何が間違いを訂正することと関連しているかを考え、あなたが間違った構文と修正済の構文の両方をサポートし、構文を機能強化し続けなければならないならば、何が起こるかについてあなた自身に問い正してください。これらの問題をより良くする、あるいは、より悪くする構文について何かありますか?問題をより容易に扱う構文にするためにできることは何かありますか?
-
潜在的なユーザーコミュニティに、あなたの言語を提示してください。彼らは、その構文をどう思いますか?それは、頭の切れる人たちにとっても驚くようなものですか?あなたの言語を使いたいという人たちに、彼らのありのままの意見を求めてください。
-
あなたの言語のベータテストをおこなってください。ユーザーに高い影響変化を受け入れてもらうには、バージョン1.0以降の言語よりもバージョン0.9の方が容易です。あなたのベータ版のテスターに、彼らのフィードバックによって作成しているソースの成果物を捨てる結果となる可能性があることを意識させてください。
もちろん、あなたがグラフィカルなDSLを開発していて、それがグラフィック環境以外で書かれることが決してないならば、XMLを使用することが適切で、おそらくそれがベストでしょう。しかし、私が直接編集したテキストDSLのもとの構文として、XMLスキーマを利用することを提案することはないでしょう。Ant(またはNant)プログラムを書いている言語ユーザーを仮定すれば、私の趣旨が伝わるでしょう。Martin Fowler氏は次のように言っています。「XMLはパースが容易です。しかし、カスタム形式として読みやすいものではありません。人々は、山括弧が苦手な人のために、XMLファイルの操作を支援するためのIDEとしてプラグインを作ります。」今日、あなたは厄介なXMLベースの構文から離れ、テキストDSLを直接編集することになるでしょう。
最後に、DSL構文の記述について強調すると、複雑なテキストDSLは、BNF(または、EBNF)を使用した形式的な文法として定義可能でなければなりません。あなたの言語が形式的な言語として表現することができないならば、それをパースするのが非常に困難か不可能なものとなるでしょう。パースやBNFに関する詳細は、以降で説明しています。
言語メタモデルを設計する
あなたが記述している概念のモデルとして、ある言語の文法(構文)で書かれたソースコードを考えてください。あなたが記述している概念は、データ、構造、ふるまいかもしれませんが、それらは、コンピュータの世界では一般的なものです。言語設計者の観点から見ると、これらの概念の記述はソースコードではなく、モデルです。したがって、あなたがソースモデルをパースしてその表現をオブジェクトに含める時、そのオブジェクトはメタモデルと呼ばれます。
言語のもととなるアーティファクトが抽象的な構文ツリー(Abstract Syntax Tree、AST)に変換されるとしても、ASTはある種のメタモデルです。ASTは、構文と深く関連しているにも関わらず、抽象構造の記述に関するもとの構文の一部のメタモデルとなります。いずれにせよ、私は、多くの複雑なテキストDSLがASTに読み込まれるのでなく、より豊かなメタモデルに読み込まれるべきだと主張します。私は、(必要に応じて)メタモデルをグラフにするのが好きで、それは、Model-View-Controllerパターンのモデル層のようなものです。つまり、あるドメインモデルです。しかし、この場合は、そのグラフはモデルではなく、もとのモデルのメタモデルとなります。(私がここで言っているメタモデルに対して、Martin Fowler氏はセマンティックモデルという名称を使っている点に注意してください。彼は、この概念をオブジェクトモデルとしても定義しています。それは、あるドメインモデルです。)
この話題は構文設計へと続きますが、それが言語のメタモデルが最終的な構文より前に検討できないということを意味しているのではありません。実際、あなたの言語のメタモデルは、あなたの構文が外からの受け入れと将来の機能強化のためにあるのと同様に、あなたのDSLの内部の仕組みにとって重要なものです。メタモデルの設計は、言語構文を考える前に設計を始めることさえ可能です。何故なら、メタモデルは構文と強く結び付かない(そうすべきでない)からです。
これを示すものとして、最近、James Gosling氏はJavaの形式文法(構文)が「詐欺」であると言っています(27:00頃と60:00頃の映像)。何故なら、言語の当初の設計は、Cのような構文呼び出しでなかったからです。それにも関わらず、Javaは内部的にインターフェース、クラス、メソッド、フィールド、スレッド、プリミティブがあり、バイトコード上で実行されます。なじみ深い構文の使用によって、C/C++プログラマーをJavaに引き込む努力がなければ、Javaが今日あるのとは全く違った姿を私たちに見せてくれたかもしれません。しかし、Cのような構文を促進するために、Javaのメタモデルが(少なくとも大幅に)変わる必要はなかったと思って間違いありません(おそらく、事前・事後インクリメントのような考え方。但し、既にC以外の構文によってサポートされている場合を除く。)。それは、基礎をなしているメタモデルが、複数の構文から対応付けることのできる抽象的な方法で、言語の概念を定義しているからです。この特徴は、Java VMをGroovyやJRubyのようなスクリプト言語のための素晴らしいホストとしています。
メタモデルについて考えるとき、それがもとのモデルに関するメタ情報を保持しているオブジェクトモデルであることを忘れないで下さい。それ故、あなたの言語の概念全てが、メタモデルで十分に表現されなければなりません。オブジェクト指向のメタモデルという、身近な例を取り上げましょう。オブジェクト指向言語でのメタモデルのクラスは、次のものを含みます。
MetaClassクラスは、それが表現するもとのモデルの中にクラスに関するメタデータを持ちます。例えば、あるソースコードにEmailAddressという名前のクラスを定義したとき、string「EmailAddress」のnameアトリビュート/フィールドを持ったメタクラスのインスタンスを持ちます。MetaClassクラスは、MetaFieldインスタンスのコレクションや、MetaMethod インスタンスのコレクションも持ちます。モデルのもとのクラスであるEmailAddressがname addressで宣言されたフィールドを持つならば、そのメタクラスはそのフィールドのコレクションの中に少なくとも1つの要素を持つでしょう(string「address」のname属性/フィールドを持ったMetaFieldクラスのインスタンス)。さらに、それぞれのMetaFieldインスタンスは、modelフィールドへのMetaClass型の参照を持ちます。そして、メタモデルはグラフを形成します。
あなたのDSLで示した特別なモデル表現を除いて、あなたはこの例に似たメタクラスを使用するでしょう。私は、抽象基底クラスMetaObjectから始まるメタクラス階層を検討することを提案します。MetaObjectは、全てのメタサブクラスに対するデフォルトの状態と振る舞いを提供するのに使用されます。たとえば、多分、あなたの言語がサポートしている多くのメタオブジェクトには名称があります。その場合、MetaObjectクラスはすべてのサブクラスに代わって、nameアトリビュート/フィールドとアクセサを持ちます。あなたが有益なMetaObjectを定義した後、あなたはメタモデルに関する完全なクラス階層を設計し始めることができます。もちろん、時間と共にあなたのメタモデルも成長し、共通の状態や振る舞いをMetaObjectクラスへとリファクタするかもしれません。
このアプローチを一歩進んで見るために、あなたがEric Evan氏の「ドメイン駆動設計」パターン(DDD)を知っているならば、あなたはそれらのパターンをメタモデルに適用することができるでしょう。私は、自身のDomainMETHODツールに対してこのアプローチを利用しています。それは、DDDを促進し、アプリケーションドメイン層の作業を生成するためのDSLです。それ故、私は両方の分野でのベストを持っています。私はDDDを使用したツールを設計・開発し、DDDベースのドメイン層の作業の設計と生成を支援します。私のツールの設計は、エンティティ、バリューオブジェクト、集約、リポジトリなどを備えています。私は、メタオブジェクトリポジトリのモデルソースから読み込んだメタモデルオブジェクトの集約を格納します。私はプロジェクト設定やカスタム生成のコマンドを検索するために、そして、生成された対象のアーティファクトを作成、検索、格納するためにプロジェクトリポジトリを使用します。それらは、最終的に出力ファイルとして永続化されます。私は、TemplateRepositoryという名前のソーステンプレートの状態を検索、管理するために利用しているリポジトリも持っています。
DDDを使用することで、あなたのメタモデルを概念化し、設計、実行するのに実践的で強力な手段となるでしょう。このことは、DSLの特性に依存して解決できないときに、DDDの使用を検討することができるということです。
メタモデルと対象モデルの関係
本稿で、私は外部DSLに関する3つの潜在的対象の利用について言及しました。私は、メタモデルや対象アーティファクトやモデルの間の興味深い関係を示すために、メタモデルの関連についてここで再度説明したいと思います。
ここで私が言及した3つの対象は、次のとおりです。DSLソースモデルは、あなたのアプリケーションの一部となるソースコードへのパースや変換が可能であること。DSLソースモデルは、あなたのアプリケーションのランタイム環境でのパースやインタープリタが可能であること。DSLソースモデルは、あなたのアプリケーションが実行時に利用するための別形式のデータにパースしたり変換することが可能であること。
メタモデルの第一の利用(対象となるソースコードのアーティファクトの出力を得ること)は、通常より複雑なものではあるものの、それは単純です。基本的に、あなたは、1つ以上のソースモデルをメタモデルに変換し、その後で、対象のモデル(または、アーティファクトのもととなるもの)へと変換します。
解釈された残りの2つのバリエーション(インタープリトとデータ形式の変換)には、類似点があります。何故なら、インタープリトされたモデルと変換されたすべての形式のデータは、単純にメタモデルの型が異なるだけだからです。本質的には、あなたのパーサーによって作られたメタモデルを得ることと、あなたのアプリケーションでサポートされている別のデータ形式にそれを変換することは、モデルからモデルへの変換として実行されるものとして考えることができます。しかし、重要なのは、それがメタモデルの状態のままであるということです。それにも関わらず、あなたがデータ形式のメタモデルをメモリ上にのみ保持するのではなく、永続化するのであれば、生成プロセスの一部として最後の永続化の変換が必要になるでしょう。
インタープリトのモデルは、振る舞いを多く持ち、状態や状態の変換を伴う振る舞いの混成であるため、残りの2つのバリエーションにその他の違いがあるかもしれません。しかし、インタープリトの目標が実際に単純なものであるならば、あなたはその2つの間の本当の類似点が理解できるでしょう。
メタモデルの生成
あなたのメタモデルを手に入れることは自由です。openArchitectureWareのXtextツール(現在は、Eclipseモデリングフレームワークの一部です。)は、ツールのアーティファクトとして、あなたのメタモデルを自動的に生成します。あなたがおこなうすべてのことは、Xtextを使用して形式的文法を定義し、異なるパーサー生成ツールを取得し、そこからアーティファクトを生成することです。あなたのメタモデルは、パーサーと共に生成されます。結果として得られたパーサーがDSLのもとのモデルをパースすると、パーサーは対応するメタモデルの一部を生成します。そして、バリデーション、インタープリト、コード生成の準備を整えます。これは非常に便利なものです。
パーサーを定義する
私は、意図的に、この節を「パーサーを開発する」でなく「パーサーを定義する」というタイトルにしました。複雑な外部DSLには、パースが容易でない複雑な構文があるため、手作業で設計したり言語パーサーを実装しようとするのは、無駄足になると信じているからです。多くの言語において、パースは容易です。カスタム言語の字句解析プログラムやパーサーを作ることのできる開発者はほとんどいません。たとえそれができる人でも、優れたパフォーマンスが必要でない限りは、パーサーを手書きするよりも、それらを定義することのできるツールの方を選択するでしょう。それを行うのがより単純で、短期間で、効果的に作れて、エラーが少ないのですから。
大部分のパーサー発生ツールは、Backus-Naur形式(BNF)やその拡張(EBNF)を使用した言語の形式文法(構文)の記述をサポートしています。BNFは、形式的な構文を定義するための形式的な方法を提供します。そして、EBNFは、それをさらに容易なものにします。
INTEGER_LITERAL : ('-')? ('0'..'9')+ ;
上記は、オプションで繰り返しの仕様をサポートすることでBNFを拡張したEBNFです。INTEGER_LITERALは、マイナス記号があるかどうかが定まらず、1つ以上の0から9の間の数で構成されるという仕様です。あなたが正規表現を知っているのなら、大きな優位点があると言えます。EBNFは正規表現と同じではありませんが、それらは似ているからです。
汎用プログラミング言語に対する典型的な言語文法は、次のように最高レベルで定義されるでしょう。
prog : expr+ ;
これは、単純に、プログラム(prog)が一つ以上の表現(expr)から構成されることを示しています。もちろん、これで終了ではありません。あなたは、表現(expr)が何かを定義しなければなりません。そして、そこから楽しみが始まります。
expr : ... ;
最高の抽象度から最下層の詳細部分に至るまで、さらに言語構文を分解することで、学習曲線が得られます。それにもかかわらず、このアプローチは、バグの多いレキサやパーサーをハンドコードするよりも、非常に簡単で、より短期間でできます。あなたの適切に定義された言語の形式文法の定義に対して、パーサー生成ツールを実行してください。そして、あなたは1、2秒で作業を終え、バグの無いレキサとパーサーを得ます。さらに、あなたのツールがサポートしているEBNFのこつをつかんだら、あなたの生産性は大幅に上昇するでしょう。
オープンソースや低価格で利用可能なパーサー生成ツールがいくつかあります。私は、いくつかの理由からオープンソースツールであるANTLR(「antler(アントラー)」と発音します)が気に入っています。ANTLRは、さまざまな対象言語でパーサーの生成をサポートします。ANTLRバージョン3では、文法の曖昧さを解決する中でのフラストレーションの時間を削減する(あるいは日単位で削減!)ために、とても洗練されたソースストリームという将来を見越した技術があります。あなたの言語の様々な部分がパーサーと衝突し混乱させられたとき、文法的なあいまいさが発生しています。これは、それ自身が複雑な話題で、現時点で、私はより詳細な情報を提供することができません。しかし、あなたが望んでいないこの問題についてまだ経験していないのならば、私を信用してください。そして、もしあなたがそれを経験しているのであれば、是非、ANTRL 3をダウンロードしてそれを断ち切りましょう。ANTRLは、要素の定義に渡すパラメータや、単純、複雑な戻り値もサポートしています(専門的な機能ですが)。話は変わりますが、ANTRLのEBNFは複雑な外部DSLの大変貴重な例です。
現在の大部分のパーサー生成器は、カスタムコードと言語要素の定義を関連付けることができます。これは詰まる所、パーサーが要素の条件に適合しながらソースストリームを評価するポイントに、あなたのカスタムコードを挿入するということです。DSLの用語では、カスタムコードがEBNF言語とは無関係であることから、Martin Fowler氏はこれをForeign Codeパターンと呼んでいます。あなたのカスタムコードがあなたが許可するパーサーへと書き込まれるので、何よりも、適合したイベントでメタモデルを具体化することができます。先のオブジェクト指向言語の例に戻って考えると、以下のとおりになります。(完全なものではありませんが、単純化した内容になっています。)
classDef[MetaObjectRepository repo] : 'class' (ident = identifier) '{' cb = classBody '}' { MetaClass metaclass = new MetaClass($ident.text); // ... repo.addClass(metaclass); } ;
classDef要素は、字句ストリームが以下の順序に従い、生成されたパーサーによって適合します。
-
テキスト文字列 “class
-
identifier要素 (他で定義されたもの)
-
テキスト文字列 “{”
-
テキスト文字列 “{”
-
テキスト文字列 “}”
生成されたパーサーコード(この場合はJava)では、classDefに対して適合するものは、(引用終了の)カーリーブラケットの間にカスタムコードを持ちます。適合するとカスタムコードが実行されます。これにより、新しいメタクラスがident変数のテキスト文字列の値を設定したそのクラス名称でインスタンス化できるようになります。そのident変数は、適合したidentifier要素が割り当てられます(classBodyに適合するcbの割り当ても同様です)。MetaObjectRepositoryインスタンスのrepoは、classDef要素の引数として渡され、私たちのカスタムコードとして利用可能であるということに注意してください。新しいMetaClassがインスタンス化され、完全に構成される(コードでは示していませんが)と、それはリポジトリに追加されます。
ここに魔法はありません。これが全て一緒に適合する方法を理解するためのポイントは、それぞれのEBNF要素がJava(または、別のパーサーの対象言語)メソッドとして生成されるという事実にあります。それ故、classDef、identifier、classBody、文法内で定義されたその他全ての要素に対して生成されたメソッドがあります。
1つ以上のモデルをパースする
あなたは、1つか複数のDSLモデルのソースが単一のメタモデルインスタンスにパースされるかどうかを決めなければなりません。たとえば、DSLソースが対象アプリケーションのソースの抽象化を表現しているのなら、DSLソースの作成者は、おそらくそういったたくさんのソースを作るでしょう。その場合、あなたのパーサーは複数のソースを探す方法を知らなければなりません。
この要件により、単純なディレクトリ-ファイルのクローラーを定義します。DSLツールは、プロジェクトの基底パスを受け入れ、あなたの言語仕様で定義されたファイル拡張子に適合するすべてのファイルのディレクトリ構造を探すでしょう。適切な名前を付けられたそれぞれのファイルが突き合わされると、それは読み取られ、パースされ、その結果、単一のメタモデルインスタンスに置き換えられます。ソース間の関連はメタモデルで表現され、グラフを形成します。単一のメタモデルインスタンスが全てのDSLソースモデルから構成されると、3つの一般的なアクションの一つを受け入れる準備が整います。それは、解釈して生成、インタープリト、あなたのアプリケーションに適したデータフォーマットなど異なる種類のモデルへの変換です。
あなたのDSLやツールが、あなたのメタモデルを構成するための単一のDSLソースモデルだけをサポートするなら、あなたのパーサーは少し単純になるでしょう。何故なら、単一のモデルソースを受け入れ、それをパースし、メタモデルをビルドし、先の3つのアクションの一つを受け入れるためのツールを設計するだけだからです。
あなたがDSLからコードやあるデータ形式を生成するのなら、次の節は興味深いものとなるでしょう。
コードを生成する
コード生成を議論する際に、ここでの原則がデータ生成にも当てはまることに注意してください。データ生成は、通常、コード生成よりも少しだけ、さもなくば大きく単純です。従って、私はコード生成について正面から対処します。ここで、良い知らせがあります。あなたが必要とするすべてのことが、あなたのDSLからある種のデータフォーマットを生成することならば、私がここで説明したものよりも単純な戦略を利用することが可能です。
初めに、メタモデルからのコード生成は単純な練習のように見えるかもしれません。実際、あなたのDSL、メタモデル、対象アーティファクトの単純さに依存することが、ちょうどそれかもしれません。しかし、私の経験上、複雑な外部DSLは対応するメタモデルからのコード生成が見事に単純になります。私は、一般的なコード生成の戦略からより強力なものへと進歩するでしょう。そして、それぞれのアプローチで競合するものを説明します。
モデルから直接生成
コード生成のための1つのアプローチは、メタモデルを全く作ることなく対象のアーティファクトの出力を解釈することです。この技術は、対象のアーティファクトを直接出力したコードであなたのメタモデルを構成するのに使用された、上記で説明したカスタムコードのすべて、または大部分を置き換えます。あなたがこの戦略を利用することができるのなら、何故そうしないのでしょうか?確かに、あなたがメタモデルを設計するのに骨を折らずに、それを構成するためのカスタムのパーサーコードの断片を書くならば、あなたの全体的な作業を完成し、先へ進むために必要なレベルよりもずっと先へ進んでいます。
あなたが自身のDSLがメタモデルを必要としていないと考えているならば、私はこのアプローチを初めに検討することを提案します。私からの唯一の警告は、あなたのDSLソースが対象アーティファクトの出力にとても近い抽象化ならば、あなたは最初に対象アーティファクトを直接コーディングすることが可能でないのか?ということです。あなたはこの質問に答える必要があります。そして、その答えが「No」という可能性もあり、その場合はあなたにとって良いことです。
あなたは単一のソースから複数のアーティファクトを対象としていますか?あなたは複数のソースから複数のアーティファクトを対象としていますか?もしそうならば、モデルから直接生成するアプローチを使用することは、おそらく実用的ではありません。
メタモデルを見直す
あなたのメタモデルがDSLモデルソースから十分な構成が得られたら、あなたはどんな対象アーティファクト(ソースコード、設定ファイル、など)を生成すべきかを決定するために、「メタモデルをチェックする」必要があります。これは、主要な集約ルートの開始や主要な集約ルートオブジェクト全体の繰り返しを含みます。そして、それらが重要なメタデータを探し、必要なメタドメインの振る舞いを実行します。
このアプローチの主要な問題は、あなたがあるメタモデルコンテキストに到達したとき、与えられた対象のアーティファクトを適切に生成するために必要なすべてのコンテキストがないかもしれない(おそらくないだろう)ということです。必要なメタデータがメタモデルの至る所に広く広がる傾向になるでしょう。
あなたがそれぞれの対象アーティファクトのタイプに応じて個別の専用コード生成器をもっているのなら、それぞれの対象アーティファクトを生成するために必要な全てのメタデータを探すために、何度もモデルをチェックする必要があるということが分かるでしょう。そして、あなたはそれぞれのアーティファクトのタイプに応じて複数の複雑なナビゲータを開発することになるでしょう。あるいは、必要なコンテキスト全てに依存するメタデータを関連づけることができるように、あなたはより複雑なグラフとしてメタモデルを設計するでしょう。ナビゲーションの必要性を解決するためにこれらのアプローチのいずれかを使用することで、達成が困難になる可能性があります。
リージョンを意識したアーティファクトとメタモデルのイベント
メタモデルを何回もチェックする複雑さに対する非常に現実的なソリューションは、メタモデルのイベントを設計することです。これは、単純なパブリッシュ/サブスクライブパターンで可能です。重要なメタモデルの一部に遭遇したときに、イベントを発行する単一のメタモデルチェッカーを設計してください。サブスクライバは発生したイベントを受け、まさにその瞬間にコードを生成することでイベントに応答します。このパターンは、私のDSLのコード生成の取り組みの中でとても有益であるということが分かっています。
メタモデルのイベントは、リージョンの意識とワークスペースを持ったアーティファクトのサポートが不可欠です。あらゆる種類のソースコードアーティファクトにはリージョンがあります。
特定のイベントがそれぞれのコード生成リスナーに発生すると、新たに作ったソースコードを適切なアーティファクトのリージョンに挿入します。リージョンは、名前やインデックス(あるいはその両方)で管理することができます。ネストしたリージョンアーティファクトの管理は、必要に応じて適切な場所へ生成したアウトプットを追加することができるのでより強力です。
しかし、あなたの特定の生成器がイベントを受けるならば、そして、再度、現在のメタモデルのメタデータに十分なコンテキストがない場合はどうなるでしょうか?お決まりの反応は、今何が必要かを見つけるためにすぐにモデルを確認することです。しかし、少しの忍耐とアーティファクトのワークスペースで、必要なコンテキストを得て、コードをよりエレガントに生成することができます。イベントが、不完全なメタデータコンテキストを届けたとき、矛盾のない(唯一の)アーティファクトのワークスペースエリアにコンテキストの一部を単純に格納してください。次のイベントが発生したら、保存したワークスペースのメタデータで構築します。最終的に必要なコンテキストが全て届いたら、ソース部分を完全に構築し、唯一のワークスペースエリアからソース部分を取り出し、ワークスペースエリアを削除し、完成したソース部分を適切なアーティファクトリージョンに保存してください。
対象のアーティファクトが完成すると、それは適切な出力ファイルに保存することができます。
あなたは、ネストしたアーティファクトリージョンを正しいアーティファクトファイルに変換する方法を知るために、アーティファクトの永続化方法を設計する必要があります。
コードテンプレート
あなたが生成するソースコードが非常に単純なものでない限り、あなたはコードテンプレートとテンプレートエンジンの利用によって利益を享受するでしょう。まず、コードテンプレートの使用の代替案を検討してください。あなたが個別のソースコード生成イベントまで及んでいるのなら、以下のC#のプロパティを作る必要があります。
private string _address; public string Address { get { return this._address; } set { this._address = value; } }
あなたは一般的なアプローチをとり、以下のようなC#のソースのテキスト文字列を作成します。
string propertyDef = “private” + propertyType + “ ” + hiddenPropertyName “;\n” + “public ” + propertyType + “ ” + propertyName + “\n” + “{” + “\n” + indent + “get { return this._” + hiddenPropertyName + “; }\n” + indent + “set { this._” + hiddenPropertyName + “ = value; }\n” + “}” + “\n”;
正直なところ、私がこの例を書いた時に、生成方法について考えていたところで私は何回か混乱しました。私はC#のプロパティを作成する方法を知っていますが、これらの生成した断片を同時に合わせることで、混乱を引き起こし、コードが正しく生成されるまで何回かやり直しをしました。実際、私は現在上記の断片が正しいと思っていません。とは言え、この例はあなたがC#のクラスを生成するのに必要な最も単純なコードの断片の一つです。
次に、コードテンプレートとテンプレートエンジンの使用を検討してください。まず第一に、C#のプロパティを生成するためのテンプレートは、次のとおりです。
property(propertyType, propertyName, hiddenPropertyName) ::= [[ private $propertyType$ $hiddenPropertyName$; public $propertyType$ $propertyName$ { get { return this.$hiddenPropertyName$; } set { this.$hiddenPropertyName$ = value; } } ]]
このテンプレートは、とても分かりやすいです。初めの記述は、テンプレートの定義が名称やプロパティを持ち、パラメータ、プロパティタイプ、プロマティ名のセットをとっています。それ故、それはファンクションやメソッドのような良く知られたものに見えます。::=<< から >> が、テンプレートの全体部です(注:フォーマットエラーを防ぐ理由から、上記のコードサンプルでは代わりに[[ と ]]の文字を使用しています)。あなたがマニュアル通りにプロパティを作成しているのならば、字句のベースはあなたが書くであろうC#のソースのように見えるでしょう。しかし、ちょっとした違いがいくつかあります。テンプレート部はパラメータ化されています。パラメータ化された値を$文字で囲むことによって、テンプレートエンジンが、適合する$propertyType$を検索して置換します。
上記のテンプレートを使用すると、次のようになります。
StringTemplate template = getTemplate(“property”); template.setAttribute(“propertyType”, “string”); template.setAttribute(“propertyName”, “Address”); template.setAttribute(“hiddenPropertyName”, “_address”); String code = template.toString();
実際のコード生成器では、テンプレートの属性としてセットするパラメータの値が柔軟なので、与えられたDSLのソースモデルによって指定された形で、いくつものC#のプロパティを生成するために、上記のコードを再利用することができます。
私は、あなたが使用するテンプレートエンジンがパラメータ化された値だけでなく、条件文、繰り返し表現(コレクション)、自動インデントをサポートすることを勧めます。上記のテンプレートの例は、ANTLRのStringTemplateサブプロジェクトの構文とAPIで、テンプレート機能の豊富なセットをサポートしています。
あなたが単純なコード生成にしたいのならば、明らかに、テンプレートとテンプレートエンジンを利用すべきです。
結論
何が一般的なDSLであるか、そして、少し具体的に内部、外部DSLとは何かについて、ハイレベルな概説を提供しました。私は、複雑な外部DSLを開発することを含めた主要な課題やパターンについてもカバーしました。今回の内容は、短いものですが、意味のあるDSL開発のための確固たる基盤を提供しています。パーサーやメタモデルを定義したり生成するための適切なツールを手に入れることで、飛躍的進歩を得る助けとなるでしょう。しかし、あなたの言語の形式文法、メタモデル、コード生成となる考えや設計を置き換えるようなツールは存在しません。
あなたが複雑な外部DSLの開発にまだ挑戦していないのならば、今回の情報によってあなたがそれに近づくためのいくつかのステップを得られたであろうことを願っています。私はあなたのフィードバックを歓迎しています。そして、この話題に関する詳細をあなたと議論できることを楽しみにしています。
著者について
Vaughn Vernon氏は、ソフトウェア開発者、アーキテクト、設計者として26年以上の経験を持つ独立コンサルタントです。彼は、DomainMETHOD(迅速な設計とドメイン駆動設計のパターンをベースとしたドメインモデルの生成をサポートするDSL)を含むいくつかのソフトウェア開発ツールを概念化し、開発してきました。Vaughn氏は多くのパターンを考え、多くの記事を書き、多くの技術カンファレンスで講演しています。彼の取り組みに関する詳細はwww.shiftmethod.com で参照することができます。彼へのコンタクトは、vvernon at shiftmethod dot comで可能です。