2011-05-09
関数型Scala(7):ラムダとその他のショートカット - Mario Gleichmann
この記事はMario Gleichmann氏による、「Functional Scala」シリーズの第7回「Functional Scala: Lambdas and other shortcuts | brain driven development」を、氏の許可を得て翻訳したものです。(原文公開日:2010年12月5日)
前回は、関数型プログラミングの世界における最も強力な概念のうちの1つ、すなわち高階関数を見てきました。本質的に、ある関数が高階と呼ばれるのは、他の関数を引数として受け取る場合か、結果として関数を出力する場合でした。この考え方により、抽象化の新しいやり方がもたらされるだけでなく、ある種の状態やいわゆるコンビネータ(関数ビルダと呼んでも構いません)を捕捉したり受け渡したりするといった、かなり便利なこともできるようになるのです。なお、コンビネータとは入力された関数もしくはその他の入力を元に新しい関数を構築するもののことです。
特に、新しい抽象化の形式に関して言えば、ユースケースに特化した変更されるロジックを関数から抜き出し、後には純粋な機能だけを残す方法について見てきました。特殊なロジックは独自の関数で定義され、引数として渡されます。このようにして、フィルタリング用の単一の関数と、それぞれでリストがどのようにフィルタリングされるかを決定する多くの述語関数が作られました。この種の抽象化が抽象的であるように思えるなら、もう一度フィルタ関数を掲載しましょう。
val filter = ( predicate :Int => Boolean, xs :List[Int] ) => { for( x <- xs; if predicate( x ) ) yield x } … val even = ( x :Int ) => x % 2 == 0 // ★1 val odd = ( x :Int ) => x % 2 == 1 // ★2 … val candidates = List( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ) val evenValues = filter( even, candidates ) val oddValues = filter( odd, candidates )
これはもう知っています!1行目から始まる高階関数と、5行目(★1)6行目(★2)の述語関数が2つですね。これらの関数を背景として、リストと任意の述語関数をフィルタ関数に適用し、フィルタリングされたリストを受け取ることができます。そして、フィルタリングによって素数のリストを作りたいと思えば、適切な述語関数をもう1つ定義するだけです。さて、ここで疑問なのですが、関数フィルタに渡すためには、常に予め述語関数を定義しなければならないのでしょうか?何のための質問でしょうね?もちろん、フィルタに渡すためには関数が必要ですし、関数が天から降って来るわけではありません!もちろん、もちろんです。しかし、覚えていらっしゃるでしょうか。関数を扱った第2話で純粋な関数リテラルについて見ています:
( x :Int, y :Int ) => x + y
あー、思い出しました?ここに書いたのは関数の中核です。つまり、引数のリストがあって、その後に関数矢印( => )と関数の本体が続きます。これは完全な関数で、ただ名前が与えられていないだけです。このようなやり方で関数を定義したら、後でその関数を参照する方法はなく、したがって後で呼び出すことはできません。名前のない、無名関数だからです。そしてここに、いわゆるラムダ式が登場します!これはまさに、純粋で無名の関数定義です。
これは私たちの疑問に対して、どう応えてくれるのでしょう?そうですね、無名の関数定義であっても、値を表します。これは他のあらゆる型のあらゆる値を定義し、それを名前と関連づけない場合と変わりません(後で参照するために、その値に別名をつけることをやらない、ということです):
val almostPi = 3.14159265 // Double 型の値で、後に名前 almostPi を使って参照できる 2.71828182 // Double 型の別の値。今度は「無名」 val mult = ( x :Int, y :Int ) => x * y // ( Int, Int ) => Int 型の値で、名前 multを使って参照できる ( x :Int, y :Int ) => x + y // ( Int, Int ) => Int 型の別の値。今度は「無名」
Double 型の値を参照している別名を用いて関数を呼び出すことができるのと同様に( double( almostPi ) のように)、Double 型のリテラルを使ってその関数を呼び出すことも当然できます( double( 2.71828182 ) のように )。ふぁーあ。いつもの通りですね。その通りです。関数型プログラミングにおいて、ラムダ式を直接高階関数に渡すのは完全に普通のことなのです!そしてこれが、私たちのいくぶんわざとらしい質問に対する答えとなります。つまり、高階関数の呼び出しに使う前に、述語関数を名前と関連づけて導入する必要は必ずしもないということです。その代わり、関数を呼び出す際に都度定義することができるのです:
val evenValues = filter( ( x :Int ) => x % 2 == 0, candidates ) ... val positiveValues = filter( ( x :Int ) => x > 0, candidates )
OK、素晴らしい。しかし、一度しか使わない関数を前もって定義しなくてよくなるということ以外に、どういうメリットがあるのでしょうか?そうですね。儀式的なコードの量をさらに減らすことができるということがわかります。Scalaの型推論メカニズム万歳!関数フィルタの型を細かく見ると、( Int => Boolean, List[Int] ) => List[Int] という型であることがわかります。そして、その型がわかるのはあなただけではありません。Scalaコンパイラも、関数の型を見抜くことができるのです!そして、コンパイラが述語関数の型を知っているので、引数の方を関数リテラルを使って明示的に註釈をつけなくてもよいのです(したがって、引数の型は、高階関数の呼び出しで関数定義をする際に、その都度決定されます)。
val evenValues = filter( x => x % 2 == 0, candidates ) ... val positiveValues = filter( x => x > 0, candidates )
さらに、お望みであればもっと簡潔に書くこともできます。すべての引数を関数本体内で一度しか参照しないのであれば、関数リストの宣言を省略することができるのです!うーん、えっと、それでは、関数本体の中でどうやって引数を参照するのでしょうか?ここで、不思議なアンダースコア( _ )がはじめて機能するようになるのです。この記号は(後の回でも見るように)何でも屋です。今回は、与えられた引数リストを順番に参照するためのショートカットになります。関数本体で登場するアンダースコアは、与えられた引数の値と置き換えることができます。つまり、最初のアンダースコアは最初の引数の値を参照し、次のアンダースコアは2番目の引数の値を参照する、といった具合です。
val evenValues = filter( _ % 2 == 0, candidates ) ... val positiveValues = filter( _ > 0, candidates )
人によっては、この形式は簡潔すぎると感じるようです。趣味の問題だと思いますけどね。しかし、可読性の高いコードという観点からすると、この書き方はあまりに多くの情報を隠しているかもしれません(少なくとも、私のように静的型付け言語から来た人間からすると)。そこで、簡単なアドバイスですが、アンダースコアの記法を適用するのはきわめて「経済的な」場合だけとし、裏にある型がコンテキストから容易に把握できる状況に限定した方がよいでしょう。
実際にはアンダースコア記法は、正統的な関数定義において、コンパイラが与えられた引数の型を推論できる限りは使うことができます。コンパイルエラーが起きる例を示しましょう。
val mult = _ * _ // compile error : missing parameter type ...
次に示す、若干変更したバージョンはスムーズにコンパイルされます。関数本体においてアンダースコアで示されている各引数の型を明示的に記述できているからです:
val mult = ( _ :Int ) * ( _ :Int )
この場合、コンパイラには、関数の値を導き出すのに必要なものがすべて与えられています:アンダースコアは2回登場しますが、どちらもInt 型として註釈がつけられています。したがって、この関数の引数リストは2つの引数からできていて、どちらも Int 型となります。関数をこのかたちで定義するのがわかりやすいかどうかの判断は、あなたにお任せします。しかしながら、(少なくとも私には)もう少し読みやすいと思える妥協点があります。関数リテラルを別名と関連づける際に、式全体の型を明示的に宣言してもよいのです:
val mult : (Int, Int) => Int = _ * _
いいでしょう。ここでは、2つのアンダースコア両方のコンテキストは、関数の型註釈内で見事に記述されています。引数リストを見さえすれば、2つのアンダースコアの並びが混乱を招くことはありません。関数の本体が、例で示したように短い場合には特にそうです。
まとめ
今回は、関数リテラルの、ちょっとかっこいい新しい用語を学びました。それが、ラムダ式です。ここまで、その都度定義されて使い終わったら捨てられることになる関数を使うのが適切な状況をとりあげ、ラムダ式が役に立つところを見てきました。これにより、特殊な形式の関数につけられた奇妙な名前がレパートリーに加わったことになります。それに加え、関数の引数に対するプレースホルダとしてアンダースコアを用いることにより、儀式的なコードを減らす方法にも触れました。おわかりの通り、使い方は好みの問題かもしれません。こうした過程で型情報が失われてしまうかもしれないからです。しかしながら、コンパイラが型情報を見失うことは決してない、ということは本質的です。Scalaは静的型付け言語ですから(型情報を省略できる時もあるので、Scalaが動的型付け言語であるように見えるかもしれませんが)。そのような場合には、失われた型情報を私たちが提供しなければなりません。やり方は、関数本体でアンダースコアを書くたびに型情報を添えても、関数式全体に型を明示的に書いても構いません。いずれにしても、こうしたショートカットを活用する場合には、特に可読性という観点から見た結果について、明確に意識しなければなりません。
2010-04-10
DSL開発:ドメイン駆動設計に基づくドメイン固有言語開発のための7つの提言 - Johan den Haan
この記事はJohan den Haan氏のブログ記事「DSL development: 7 recommendations for Domain Specific Language design based on Domain-Driven Design」を氏の許可を得て翻訳したものです。(原文公開日:2009年5月6日)
ドメイン固有言語(DSL)という用語は今日多く聞かれる。DSLとは与えられたドメインの要求に対処するために開発される言語である。ドメインは問題領域(例えば、保険、健康管理、運送)である場合もあれば、システム的な側面(例えば、データ、プレゼンテーション、ビジネスロジック、ワークフロー)である場合もある。これは制限された概念を用いて言語を作るという考え方であり、これらの概念は特定のドメインに焦点を絞ったものである。この考え方により、開発者の生産性とドメインエキスパートとのコミュニケーションを改善するような、より高次の言語が導き出される。多くの場合においては、ドメインエキスパートにDSLを用いてアプリケーションを開発してもらうことすら可能になるのだ。
この記事が提示する問いとは、どうやってドメイン固有言語を開発するのか?である。
まず最初にDSLのライフサイクルについて解説しよう。これは次のフェーズから成り立っている。すなわち、決定("decision")、分析("analysis")、設計("design")、実装("implementation")、配備("deployment")、保守("maintenance")である。その後で、これまである程度大掛かりなDSLを開発してきた経験に基づき、DSLを開発するための7つの提言を行う。
DSLのライフサイクル

Mernikらによれば[1] DSLのライフサイクルは5つの開発フェーズからなる。それが、決定、分析、設計、実装、配備である。Eelco Visser氏[2]が保守をDSLの6つ目のフェーズとして付け加えている。なお実際にはDSL開発は、シーケンシャルなプロセスではなく、これらのフェーズはイテレーティブに適用される。
それぞれのフェーズについてより詳細に見ていこう。
1. 決定("decision")
DSLの開発は、DSLを開発するのか、既存のものをつかうのか、汎用言語(GPL)を使うのかを決定するところから始まる。ドメインがきわめて新しいもので、それに関する知識がほとんど得られない場合、DSLの実装を始めることにあまり意味はない。対象領域における基本的な概念を見極めるため、最初は標準的なソフトウェア開発プロセスを適用し、ライブラリによってサポートされるコードベースを開発すべきである。
換言すれば、特定のドメインに関するアプリケーションを手書きで開発したことがなく、現行のコードベースが存在しないならば、DSLとそれに関連するコードジェネレータないし実行エンジンを実装することから始めるのは好ましくないということだ。
非実行性のDSLに関してはもちろん話が変わる。しかし、実行性のDSLのために現行コードに関する経験が必要なのと同様に、非実行性のDSLのためにはモデリングしているドメインについての深い理解が必要になる。
2. 分析("analysis")
分析フェーズにおいては、問題となっているドメインが識別され、それに関する知識が集められる。正式なドメイン分析のアウトプットは、以下のものからなるドメインモデルである。
- ドメインのスコープを定義するドメイン定義("a domain definition")
- ドメイン用語集 ("domain terminology")(ボキャブラリー, オントロジー),
- ドメイン概念の記述("descriptions of domain concepts")、それに
- ドメイン概念と相互依存性の共通性("commonality")と可変性("variability")を記述したフィーチャーモデル。
このフェーズにおいて集められた情報は実際のDSL開発に使用される。可変性が示しているのは、どの要素がDSLにおいて規定されるべきかであり、共通性は実行エンジンやドメインフレームワークを定義するのに用いられる。
例えば、もしあるドメインにおいて現行のコードベースを2、3分析するとすると、そのコードに含まれるある要素はコードベースごとに異なる場所と共通の場所の2つに分けることができる。スタティックな場所(共通性)は、実装手法に応じて、DSLを解釈する実行エンジンの一部になるか、自動生成されたコードによって利用されるドメインフレームワークの一部となる。異なる場所(可変性)はDSLによって規定されるべきである。つまりこれらはDSLの利用者が「設定」する必要がある場所なのだ。
Eelco Visser[2]は帰納的な手法を提案している。これは実装前に完全なDSLを設計するのではなく、徐々に抽象を導入し、それによって特定のドメインのためのソフトウェア開発における共通のプログラミングパターンを把握できるようにするというものだ。同様にVisser氏はイテレーションの中でDSLを開発することで、失敗するリスクを軽減できると論じている。最終的に機能的なDSLを作り出すような巨大プロジェクトとは異なり、イテレーティブなプロセスは早い段階でサブドメインのための便利なDSLを作り出すのだ。
この記事の後半で、私は自らの経験に基づきDSLの分析および設計フェーズのための提言を7つ付け加える。
3. 設計("design")
DSLを設計するためのアプローチは2つの直交する次元によって特徴づけられる。すなわち、DSLと既存の言語との関係と、設計を記述することが持つ形式的な性質である[1]。DSLはスクラッチから設計することもできるが、既存の言語を元にした方が簡単である。
Mernikら[1]は、既存の言語に基づいた3つの異なる設計パターンを識別している。
- 抱き合わせ("piggyback"): 既存の言語が一部に利用されている。
- 特殊化("specialization"):既存の言語が制限されている。
- 拡張("extension"):既存の言語が拡張されている。
既存の言語との関係に加え、形式的な性質にも幅がある。
どの手法を取るのかを決定するのは重要だが、次の教訓を心に留めておくことがより重要だろう[3]:
教訓その2: あなたはこれまでにプログラミング言語を設計したことがない
ほとんどのDSL設計者は言語設計のバックグラウンドを持っている。そこでの直交性や形式の経済学についての原則には敬意を評すべきだが、それらが必ずしもDSLの設計にうまく当てはまる訳ではない。既に存在するジャーゴンやドメインの記法に合わせる際には、言語を飾り立てたり過度に作り込んだりしないように注意しなければならない。
教訓その2の帰結:必要なものだけを設計せよ。過剰な設計("over-design")をしがちな自分の傾向を認識することを学ぶべきだ。
4. 実装("implementation")
実行性のDSLにおいては、最も適切な実装手法が選択されるべきだ。Mernikら[1]は7つの異なる実装パターンを識別しており、それぞれが異なる性質を持っている。
- インタープリタ:DSLの構造は通常のフェッチ-デコード-実行サイクルを用いて認識され、解釈される。このパターンを用いると、変換は一切行われない。モデルが直接実行可能なのだ。
- コンパイラ/アプリケーションジェネレータ:DSLの構造は基礎となる言語の構造とライブラリ呼び出しに変換される。この実装パターンを指す時には、ほとんどコードジェネレーションが話題になる。
- プリプロセッサ:DSLの構造は既存の言語(基礎となる言語)の構造へと変換される。スタティックな分析は基礎となる言語のプロセッサによって実行されるものに限られる。
- 組み込み:DSLの構造は、新しい抽象データ型と操作を定義することにより、既存の汎用言語(ホスト言語)の中に組み込まれる。これについての初歩的な例はアプリケーションのライブラリである。このタイプのDSLは主に内部DSLと呼ばれる。
- 拡張可能なコンパイラ/インタープリタ:汎用言語のコンパイラ/インタープリタがドメイン固有の最適化ルールやドメイン固有のコードジェネレーションを用いて拡張される。通常、インタープリタの拡張は比較的簡単なのが普通であるのに対して、コンパイラの拡張はそれを想定して設計されていない限り困難である。
- 商用既製品:既存のツールや記法は特定のドメインに適用される。DSLやエディタ、DSLの実装手法を自分で定義する必要がなく、ただモデル駆動ソフトウェアファクトリーを利用すればよい。例えば、サービス指向ビジネスアプリケーションのドメインを対象とした、Mendix モデル駆動エンタープライズアプリケーションプラットフォームを利用することができる。
- ハイブリッド:上記のアプローチの組み合わせ。
手法が異なればDSL開発に費やされる全体的な労力が大きく変わってくるので、特定の手法を選択することが非常に重要である。
5. 配備("deployment")
配備フェーズにおいては、DSLとそれを用いて構築されたアプリケーションが利用される。開発者やドメインエキスパートはモデルの仕様を定めるのにDSLを使う。これらのモデルは前節で紹介した実装パターンの1つを用いて実装される(例えば、モデルがエンジンによって解釈される)。このような実装により、エンドユーザによって利用される実際に機能するソフトウェアができあがる。
6. 保守("maintenance")
DSLで表現されたモデルに順応することで、ドメインエキスパート自身がソフトウェアを理解し、妥当性を評価し、修正することができるが、修正する方が作るより容易であるし、そのインパクトも理解しやすい。しかし、ソフトウェアにおけるより本質的な変更により、DSLの実装が変わることもあるかもしれない。ソフトウェアにおける他の要素と同じく、DSLも時間を経て進化するのだ。したがって、DSL移行戦略を持つことが極めて重要である。
移行戦略に加え、DSLを保守する際のリスクを軽減するための提言が2つある。
- 優れたDSLツールを使う。少なくとも言語定義からエディタやジェネレータもしくはインタプリタを生成できるもの。
- 異なるDSLにおいて定義されたモデルを、異なる疎結合のエンジンによって実装する。このやり方によって、特定のDSLやそのコンパイラの保守はシステム全体に影響を与えることなく、エンジンは異なる技術を活用できる。ビジネスアプリケーションの場合、これに必要とされるのが、サービス指向ビジネスアプリケーションを推進するアーキテクチャすなわち、複数の疎結合なサービスから構成されるアプリケーションである。
ドメイン駆動設計に基づく、DSL開発のための7つの提言
今やDSLのライフサイクルが明確になったので、分析と設計フェーズにおける経験のいくつかを共有したい。その他のフェーズについては別の記事で書くことにする。
DSL設計の詳細に踏み込む前に、これらの経験のコンテキストについて理解を試みよう。まず、これらは複合的に接続されたDSL("multiple connected DSLs")を生成することに注力している。つまり相互に参照する別々のDSLによって表現されたモデルを生成できるのだ。例えば、フォームモデルにおいては、データモデルに由来するエレメントに参照できる。より限定すれば、ここで語っているのはサービス指向ビジネスアプリケーションが持つ全てのシステム的な側面をカバーする一連のDSLなのだ。
ここで語られているDSLにおけるもう一つの重要なポイントは、これらがどれもプログラマではないドメインエキスパートを対象にしたものであるということだ。ほとんどのケースにおいて、これが意味しているのはドメインエキスパートがこういったDSLで表現されたモデルを作れるということであり、少なくとも読むことはできるということである。もちろんこのためには、常に柔軟性と複雑性の間でバランスを取る必要がある。
Domain-Driven Design[4]の概念に影響を受けた私の経験に基づき、DSL開発の為に以下の7つの提言を行う。
1. ドメインの知識をメタモデルにおいてとらえよ
DSLのためのモデルについて語ろうとすれば、メタモデルという用語に行き着くだろう。多くの人にとって、この単語は読むのを止めるのに十分なほど恐ろしいものだ。しかし、これは単に言語の抽象的構造のモデルにすぎない。換言すれば、メタモデルのモデルは言語の概念とその関係なのだ。注文入力ポータルのようなソフトウェアを構築する際に「注文」「製品」「顧客」といった概念をモデリングするのと同様である。
メタモデルはDSLを構築する上で本質的なものだ。これはDSLが対象とするドメインの知識をとらえるのである。このモデルが反映しているのは、DSLを開発しているチームがどのようにドメインの知識を構成しているか、最も重要な要素が何であると考えているかである。モデルと実装との結びつきにより、初期バージョンのDSLによって得られた経験はモデリングプロセスにおいてフィードバックとして利用される。
2. ユビキタス言語を用いてコミュニケーションせよ
メタモデルはコミュニケーションという目的のためにも重要である。DSLを設計する際には、言語のユーザ(ドメインエキスパート)と開発者との間に多くのコミュニケーションが必要になる。メタモデルはチームメンバ全員によって用いられる言語の根本となる。モデルが実装と結びつけられるので、開発者はDSLについて語る時にこの言語を用いることができる。つまり、ドメインエキスパートと翻訳することなくコミュニケーションすることができるのだ。
DSLについて語る際には、モデルと戯れなければならない。シナリオについてモデルの観点で語ることができなければ、それができるようになるまでモデルを適合させなければならない。ドメインエキスパートがモデルを理解できなければ、そのモデルには何か問題があるのだ。ドメインエキスパートはドメインに関する理解を伝える上で、ぎこちなかったり不十分であったりするような用語や構造に対して抵抗しなければならない。開発者はあいまいさや不整合に注意しなければならない。こういったものは設計をつまづかせることになる。
3. メタモデルに実装を駆動させよ
言語定義が単なるメタモデル(抽象シンタックス)に留まらないことを忘れないように。言語定義は具象シンタックスとセマンティクスも含んでいるのだ。DSLを設計、実装する際、具象シンタックスはソリューションワークベンチにおいてとらえられる。これはモデルを記述する際にテキストや図による具象シンタックスによってDSLを用いることができるような環境である。言語のセマンティクスは変換ルールやモデルインタプリタにおいてとらえられる(これは用いられている実装パターンによる。上記参照)。
重要なのは、ソリューションワークベンチの実装やインタプリタをメタモデルが駆動させることである。つまり、メタモデルがDSLの実装を駆動させるべきなのだ。実装がメタモデルにマッピングされなければ、そのメタモデルにはほとんど価値がない。同時にメタモデルと実装の間にある複雑なマッピングは理解が難しく、実践的に見ても設計の変更に合わせた保守が困難である。メタモデルと実装が著しく乖離していたら、それぞれの活動において得られる洞察が互いにフィードバックを与えることがない。
したがって、メタモデルを設計する時には、実装をまさに文字通りの仕方で反映するようなやり方にするべきだ。しかし同時に、ある特定のメタモデルはユビキタス言語をサポートするという目的に従わなければならない。実装はメタモデルの表現にならなければならず、コードに対する変更はメタモデルに対する変更であり、逆もまたしかりである。DSLの実装とメタモデルとをこのようなやり方で結びつけるためには、通常、メタモデルからのDSL実装のうち大部分を生成してくれるDSLツールが必要となる。図1は、ソリューションワークベンチとインタプリタの一部がメタモデルから生成されるシナリオを示している。

図1 - メタモデル駆動DSL実装
4. ドメインを分離せよ
前述した通り、DSLは時間をかけて進化する。モデルと実装を結びつけることが重要であることは既に見た通りで、モデルが実装を駆動させるべきなのだ。しかしそのためにはドメインを分離する必要がある。メタモデルを表象するドメインのコードが、コード全体の中に散らばっていたら、変更を加えるのが非常に難しくなる。モデリング環境におけるGUIやインタプリタの基盤における変更によって、実はドメインコードが変更されることになり得るのだ。
原則として、「普通の」ソフトウェアについて言えることは、DSL実装においても言える。コードを複数のレイヤに分割し、ドメインモデルに関連するすべてのコードを1つのレイヤに集約せよ。そしてそのレイヤはGUIや基盤のコードから分離されていなければならない。ドメインオブジェクトは、それ自身を表示ないし保存したり、アプリケーションのタスクを管理したりといった責務から解放されていなければならない。ドメインオブジェクトはドメインモデルを表現することに集中するべきなのだ。
モデルが実装を駆動するべきだということは既に述べた。そしてこれは可能な限り文字通りの意味で行うことを意図している。このことはドメインを分離してはじめて可能になるのだ!ジェネレーションギャップパターンを用いることで、その他のコードから分離しつつ全てのドメインコードを生成することができる。
したがって、ドメインモデルを分離し、それによってモデルがドメインを表現すると同時にそのドメインの変更を追跡できるくらい、豊かに進化できるようにしよう。
5. 継続的にリファクタリングせよ
同様に、リファクタリングも常に行うべきだ。知識の咀嚼("knowledge crunching")の間も、メタモデルを使ったコミュニケーションをしている時も、DSLの実装にかかり切りになっている時もメタモデルからコードを生成している時も、リファクタリングをするべきだ。Eric Evans[4]によれば、特にリファクタリングするべきなのは以下の場合である。
- 設計がドメインに対してチームが持つ現在の理解を表現していない。
- 重要な概念が設計の中に隠れている(そしてそれを明確にするやり方が分かっている)。
- 設計の中のある重要な部分をより柔軟にするチャンスがある。
このような手法を採る場合に、ドメインエキスパートを含むチームメンバ全員が深く関わらなければならないということは、言わずもがなである。
6. メタモデルの整合性を保て
複雑なドメインをドメイン固有モデルを用いて抽象化するためには、1つのDSLでは足りない。複雑なプロジェクトにおいては、複合的DSLが別々のコンテキストを扱うために通常必要となる。換言すれば、異なるDSLによって定義される複合的ドメイン固有言語(DMS)が、複雑なシステムを正確に抽象化するには必要なのだ。
巨大なドメインのためのメタモデルを統一すること(メタモデルが設計しているDSLのコンセプトを記述していることを忘れないように)は実現も難しく、コストに見合うものでもない。最大の原因は、単一のメタモデル(したがって単一の言語)によって皆を満足させるよう試みることは、言語の使用を難しくしてしまうような複雑な選択肢へとつながっていくということだ。そしてこれこそが、DSLを設計する理由ではないか。別々のドメインエキスパートはその人の視点から見るシステムを定義するために独自のドメイン固有言語を必要とする。
したがって、複合的なドメイン固有言語が求められるのであり、その結果、複合的なメタモデルも必要になる。しかし、異なるメタモデル間の境界と関係は意識的に可視化する必要がある。複数のDSLを開発する上でいくつかの提言をする。
- 各メタモデルのコンテキストを明確に定義すること。つまりDSLが設計されるドメイン(例えばシステム的側面)を定義すること。
- メタモデルの実装を継続的に結合("integrate")し、他のメタモデルのためのインタフェースを自動化されたテストの一部にすること。
- メタモデル間の連結ポイントをモデリングし、そのモデルをユビキタス言語において使用すること。これらの連結ポイントは、異なるDSLにおいて表現されたモデルが、どのように相互参照できるかを定義している。例えば、GUIの要素はデータモデルの要素を参照できる。
- 参照解決戦略について考えること。インタープリタ/エンジンを用いてDSLによって表現されるモデルを実行しているならば、遅延ビルドを利用することができる(つまり、ソフト参照を用いて、実行時に解決する)。この戦略のメリットは、柔軟性と適応力だ。コード生成と一緒に利用される手法は事前バインディング("early-binding")であり、参照は生成されたコードにおいて明示的に反映される。この戦略に従う理由は性能であると思われる。
7. 人間指向の手法を用いよ
DSL実装プロセスを実行すること、特にこれまでに示してきたやり方で行うことは簡単ではない。これには力のある開発チームとドメインエキスパートが必要になる。最後の、そして最も重要な提言は、DSL開発においては人間を第一に考える手法を用いるべきということだ。DSL開発は高度にクリエイティブで専門的な仕事だ。開発者は技術的な決定をしなければならず、技術的な仕事をどう行うかを決定するのに最適な人間でもある。ドメインエキスパートはドメインに住んでいるので、言語の概念が適用できるのかを決定するのに最適だ。
私はこれまで述べてきた6つのポイントを反映した形で仕事をすることを強く勧めるが、チームはプロセスにおいて意思決定をしなければならない。プロセスを受け入れることにはコミットメントが必要であるが、それ自体チーム全員が積極的に参加する必要があるのだ。
DSL開発のためのキーポイント
- ドメインの知識をメタモデルにおいてとらえよ
- ユビキタス言語を用いてコミュニケーションせよ
- メタモデルに実装を駆動させよ
- ドメインを分離せよ
- 継続的にリファクタリングせよ
- メタモデルの整合性を保て
- 人間指向の手法を用いよ
----------------------
[1] Marjan Mernik, Jan Heering, and Anthony M. Sloane. When and how to develop domain-specific languages. ACM Comput. Surv., 37(4):316-344, 2005.
[2] Eelco Visser. WebDSL: A case study in domain-specic language engineering. In R. Lammel, J. Saraiva, and J. Visser, editors, Generative and Transformational Techniques in Software Engineering (GTTSE 2007), Lecture Notes in Computer Science. Springer, 2008.
[3] Wile, D. S. 2004. Lessons learned from real DSL experiments. Sci. Comput. Program. 51, 265-290.
[4] Eric Evans, Domain Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley, 2004.
Photos by Gail S and Hélio Costa



