目次

概要

Domain Specific Language、略して DSL。 ドメイン特化言語、ドメイン固有言語、あるいは、特定領域記述言語などと訳されます。 名前どおり、特定の用途向けに特化した言語のことです。 (この場合、ドメインという言葉は、「開発の対象領域」という意味です。) 汎用言語の対義語。

最近、DSL の考え方はモデリング言語の分野でよく使われるようになってきたので、 DSL = ドメイン特化モデリング言語(Domain Specific Modeling Language)の略だと認識する場合も多いみたい。 (DSL という用語自体、ドメイン特化型の MDD(モデル駆動開発)で注目を浴び始めた言葉っぽい? 思想自体は、LISP の時代からあるメタプログラミング的なもので、 大昔からありますが。)

また、「モデル化」で述べたように、 モデリング言語にはビジュアル言語なものが多い(2000年代前半はそれが多かった)ので、 DSL = ビジュアル言語というようなイメージも少しあったりします。

でも、domain specific (ドメイン特化)という考え方はモデリング言語とかビジュアル言語とは独立なので、 とりあえずこのページではモデリング言語とかビジュアル言語からは離れて、 「汎用 VS ドメイン特化」という視点で DSL についての話をします。

また、「言語を作る」という側面からの話もします。 「まずライブラリを作って、そのライブラリを使ってアプリケーション開発」というのと同じで、 「まず DSL を作って、その DSL を使って開発」というノリになるわけですが、 こういうのを言語指向プログラミング(langage oriented programming)とか言ったりするようです。

ドメイン特化

モデル化」の章では、 「よいモデルとは、問題の要件を必要十分に、過不足なく表せるモデル」という話をしました。 何でもかんでも扱おうとするのではなく、 必要な部分だけ抜き出したコンパクトなモデルを使った方が理解がしやすいということです。

そういう考え方からすると、 「C/C++ とか Java とか C# みたいな汎用型言語だと用途が広すぎる」、 「用途に合わせた専用言語を作った方が後々楽ができるんじゃないか」という発想にいたります。 それが DSL、ドメイン特化言語になるわけです。

汎用 VS ドメイン特化

汎用(general purpose)言語にもドメイン特化(domain specific)言語にも、 それぞれ利点があります。 両者の特徴を比べて見ましょう。

汎用 VS ドメイン特化
汎用言語 ドメイン特化言語
文法の多様性 利点:用途ごとに覚えなおす必要がない 欠点:用途ごとに文法が異なる
機能の量 欠点:多機能すぎて覚えることが多い 利点:必要最小限の機能のみを持つ
記述の手間 欠点:往々にして冗長な記述が必要になる 利点:記述の手間を最小にできる

(注意: ただし、最近の汎用プログラミング言語は IDE(Integraged Development Environment: 統合開発環境)などのツールのサポートを受ける前提で作られていて、 ツールが優秀になってきているので学習も利用もずいぶん楽になっています。 ドメイン特化型の言語を作る場合にもツールの利用を意識する必要があり、作成のハードルが高くなっています。 また、「せっかく手間や学習コストを減らすために DSL を作ったのに、ツールのサポートがないから結局汎用言語の方が楽」という事態もあり得ます。)

最近は、互いの利点・欠点を補うために、 両者を混在させた開発というのがはやりつつあります。

言語といっても・・・

まあ、専用言語といっても、そんなたいした話ではないんですね。 そりゃ、中には、コンパイラ作りからやるような本格的な人もいますけど。 多くの場合は、「設定ファイル」とか「ライブラリ」程度のものです。

有名なところでは、Apache の設定ファイルなんかは結構立派な構文を持っていますし、 emacs の設定にいたっては LISP 言語で書きます。 ああいうのも、一種の DSL です。

ということで、 まず、「設定ファイル」とか「ライブラリ」が DSL の第1歩という話から始めてみたいと思います。

抽象定義と具象定義

DSL と設定ファイル、ライブラリの関係性を話す前に、 ちょっと補足的な説明をしておきます。

まあ、アプリケーションの設定を外部ファイルに持ったりすることは結構あるわけですね。 まず、簡単な設定ファイルということで、 GUI アプリのウィンドウサイズ設定を例にとってみましょう。

一昔前だと、 ini ファイルに保存することが多かったです。

[window]
width=480
height=360

あまり複雑なことをしないならシンプルでいい形式だと思います。

一方、最近だったら XML なんかが流行りですね。

<configuration>
  <window
    width="480"
    height="360"
    />
</configuration>

設定ファイルに階層的なデータ構造が必要になるなら ini よりも XML の方がいいですね。 たいていの環境で読み書き用のライブラリがそろってるのが強みです。

最近だと Json なんていう形式も流行っています。 JavaScript の文法そのままで記述するので、JavaScript ならそのまま読めるというのが利点です。

{
  "window":
  {
    "width": 480,
    "height": 360,
  }
}

あと、設定変更するたびに再コンパイルが必要ですが、 以下のように、C# ソースファイル中に埋め込んでもいい。

public static class Configuration
{
  public static class Window
  {
    public const int Width = 480;
    public const int Height = 360;
  }
}

C# 3.0 以降なら以下のような書き方もありかと思います。

var configuration = new
{
    Window = new
    {
        Width = 480,
        Height = 360,
    }
};

この方法の最大の利点は、設定読み込みのためのコードを一切書く必要がないことですね。 まあ、用途によっては、「設定変更するたびに再コンパイルが必要」ってのがかなり致命的になりますが。

その他にも、昔だったら Windows のレジストリに設定を保存したりしたし、 データベースやディレクトリサービスを使って保存する手なんかもあります。

このように、設定の保存のしかたにはいろいろあります。 でも、「どれにしたって設定ファイルには違いない」という視点でいうと、 重要なのは window が width と heigth を持ってるって事です。 何で保存しようと、持ってる情報は同じです。

要するに、この手の情報の保存形式には2段階の定義があります。

  • 抽象定義(abstract definition): 「window が width と heigth を持ってる」とかの取り決め

  • 具象定義(concrete definition): 「ini ファイル使うか、XML 使うか、それともデータベースか」とか、 具体的な保存方法

本当に重要なのは、抽象定義の方なんですね。 抽象定義が同じなら、具象定義が違っても互いに変換できます。

で、抽象定義が同じ(要するに本質的に中身は同じ)情報に対して、 具象定義の方は自由に決められます。 実のところ、具象定義の仕様を考える作業ってのは、 言語の文法を定めるようなものです。 次節でもっと「それっぽい」例を挙げて説明しますが、 これがこのページでいう「DSL(の文法を考える) ≒ 設定ファイル(の具象定義を考える)」という話になります。

設定ファイルと DSL

ということで、設定ファイルの構造がプログラミング言語っぽくなってくる例をあげてみます。

要するに、細かい設定をしようとこだわってるといつの間にやら、 設定ファイルが1つのプログラミング言語みたいになってくることがある。 設定ファイル中に条件分岐だとかループ文だとかが書かけるようになってきたり。

例えばですけど、 サークルとか同好会で、同人ゲームを作ることになったとします。 まあ、素人でも作りやすいものってことで、 とりあえずサウンドノベルでも作ることを考えます。 で、シナリオなんかはプログラム中に埋め込むよりは、 外部設定ファイルに書いておきたいと思います。 例えば以下のような感じで。

主人公:
今日はもうクタクタだ。
でも、何か忘れているような・・・

主人公:
ある。
そうだ、○○さんとの約束が。
待ち合わせの時間は9時。もう2時間も過ぎてる・・・

ストーリーが某漫画のパクリ臭いのは気にしないでください。 っていうか、インスパイアです。 (このシナリオファイルはフィクションであり、実在の人物・団体、または、某少年誌連載の執事漫画は関係ありません。)

とにかく、プログラム的には、 この設定ファイルからシナリオを読み込んで表示するだけとかにしたいわけです。 シナリオ修正のたびにプログラム本体を修正したくないですし、 シナリオライターとプログラマは分業することが多いですし。

でも、サウンドノベルなんで、条件分岐くらいは欲しくなります。 なので、設定ファイルで条件分岐できるようにします。

主人公:
今日はもうクタクタだ。
でも、何か忘れているような・・・

if ひな祭りflag = ON
  and 体力 > 300
{
主人公:
ある。
そうだ、○○さんとの約束が。
待ち合わせの時間は9時。もう2時間も過ぎてる・・・
}
else
{
主人公:
気のせいかな。
いや、もう今日はこれ以上考え事するのはよして寝よう。
}

他にも、選択肢を表示したり、 高感度パラメータなんかも持ったりしたいですよね。 とかやってると、どんどんプログラミング言語チックになってきます。

[scene 9-11]
主人公:
今日はもうクタクタだ。
でも、何か忘れているような・・・

if ひな祭りflag = ON
{
[選択肢 A]
  1 何かあったはずだ
  2 気のせいだよな

if 体力 > 300
  and A = 1
{
主人公:
ある。
そうだ、○○さんとの約束が。
待ち合わせの時間は9時。もう2時間も過ぎてる・・・

nextscene 9-12
}
else
{
[高感度 ○○さん -100]
}
主人公:
気のせいかな。
いや、もう今日はこれ以上考え事するのはよして寝よう。

nextscene 10-1

まあ、この設定ファイルはかなり適当に書いたんで、 解析プログラムを書くのが大変そうではあります。 というか、文法もめちゃくちゃで覚えにくそう。 文法を覚えやすくしようと思うと、 結局、何かのプログラミング言語を参考にする方が早いわけで。 例えば、C# 的に書くなら以下のような感じですか。

scene 9-11
{
  ShowText(characters.主人公,
    "今日はもうクタクタだ。",
    "でも、何か忘れているような・・・");

  if (parameters.flags.ひな祭り == ON)
  {
    A = ShowOption(
      Option(1, "何かあったはずだ"),
      Option(2, "気のせいだよな"));

  if (characters.主人公.体力 > 300
      && A == 1
  {
    ShowText(characters.主人公,
      "ある。",
      "そうだ、○○さんとの約束が。",
      "待ち合わせの時間は9時。もう2時間も過ぎてる・・・");

    nextscene 9-12
    return;
  }
  else
  {
    characters.○○さん.高感度 -= 100;
  }

  ShowText(characters.主人公,
    "気のせいかな。",
    "いや、もう今日はこれ以上考え事するのはよして寝よう。");

  nextscene 10-1
}

ここまでやるんだったら、 scene とか nextscene の辺りも何とかして、 完全に C# のコードにしてしまえそうな気もします。 そうすれば、構文解析プログラムを作る必要もないですし。 要するに、新しい言語を作って構文解析プログラムを作るんじゃなくて、 C# のライブラリとしてサウンドノベルフレームワークを提供。

ここまでくると、 以下の3つの区別があいまいになってきます。

  • 外部設定ファイル

  • フレームワーク/ライブラリの整備

  • 別プログラミング言語 = DSL

特定ドメイン向けに言語を作る(DSL)という発想の原点はここにあります。

ライブラリと DSL

今度は逆に、ライブラリ側から初めて DSL にいたる流れを話してみましょう。

例えば、プログラム中で XML を生成したい場合を考えてみます。

1つ目は、 あくまでライブラリとして提供する方法。 当然、元言語の文法に準拠せざるを得ません。 例えば以下のような感じ。

XDocument doc = 
  new XDocument(
    new XElement("configuration",
      new XElement("window",
        new XAttribute("width", "480"),
        new XAttribute("height", "360")
      )));

元言語の文法どおりに書けばいいのは、新しい文法を覚える必要がないとか、 言語を拡張する必要がないとか、利点もあるんですが、 コードが極めて冗長になるという欠点もあります。 例えば、new とか XDocument とかいう記述は、普通に XML を書くなら必要ないわけで、 いちいち書くのは面倒です。

それでは2つ目、 XML を文字列としてソースファイル中に埋め込んでみます。

XDocument doc = 
  new XDocument(
@"
<configuration>
  <window
    width='480'
    height='360'
    />
</configuration>
";

これも、言語を拡張する必要はないってのは利点ですが、いくつか問題もあります。 この文字列をそのまま出力するだけならいいんですけども、 XML を後から動的に操作したい場合、1度文字列を構文解析して操作可能なデータ化する必要があります。 これは、1つ目のライブラリとして提供する方法と比べるとパフォーマンス面で不利です。 また、C# などでは、"" の中で " を使えなかったりといった言語的な制約も生じます。

あと、この方法では、 XML 自体に対してツールによる文法チェックができません。 コンパイル時ではなく実行時にエラーになったり、 ミスを発見しづらくてバグの温床になる可能性があります。

で、最後、3つ目ですが、 言語を拡張してしまうことも考えられます。 汎用プログラミング言語中に別文法の言語(XML)を含められるようにしてしまう。

XDocument doc = 
  <configuration>
    <window
      width="480"
      height="360"
      />
  </configuration>
;

当然、コンパイラの修正が必要です。 あるいは、スクリプト言語を使ってプリプロセス (XML 記述を元言語として解釈可能なライブラリ記述に置き換える前処理) をかけるとかでもいいですが、それも結構面倒な作業です。 また、モデリングパラダイムの異なる複数の文法が混在することになるので、 覚えづらく、慣れるまで混乱の原因になります。

あと、プログラミング言語の文法に大きな変更を加えると、 過去のバージョンのソースコードがコンパイルできなくなるというような問題がでる可能性もあります。

ちなみに、ここで紹介したような「XML 埋め込み機能」は、 VB9 に導入されることになりました。 VB9 なら、本当に以下のようなコードがかけます。

Dim doc = 
  <configuration>
    <window
      width="480"
      height="360"
      />
  </configuration>

でも、先ほど説明したように、 デメリット・意図しない副作用もあるので、 C# 3.0 (C# の、VB9 と同時期にリリースされたバージョン)には結局導入されていません。

で、この「副作用」を避けるためにはどうしたらいいかというと、 一番簡単な解決策は、別ファイルに分離してしまえばいいんですね。 XML → C# などのコードを生成するツールを作って、 partial キーワードなどを使って、手書きのプログラミングコードとくっつけてコンパイル。

とすると、これはもう、言語拡張でもプリプロセスでもなく、 1つの独立した言語だと考えても差し支えないものになります。 これが、ライブラリから出発して、DSL という発想にいたる道筋です。

余談:破壊的変更の問題

「文法に大きな変更を加えると、 過去のバージョンのソースコードがコンパイルできなくなる可能性も」 と書いたついでにそれの話を。

文法の追加が、過去のコード資産を使えなくしてしまうこともあり得ます。 このような、過去のコードがコンパイルできなくなる可能性のある変更を「破壊的変更」(breaking change)と呼びます。

その際たる例は、予約語の追加です。 プログラミング言語に新しいキーワードを追加すると、そのキーワード名と同じ名前で定義できていた変数が使えなくなります。

このような問題を回避するために、キーワードを「文脈限定」にしてしまうという手もありますが、 コンパイラの実装が大変になります。 C# 3.0 では var というキーワードを追加しましたが、 var という単語は変数名として使われる可能性がそこそこある単語です。 これを無条件にキーワード化してしまうと、高確率で過去のコード資産がコンパイルできなくなる問題を起こします。 そこで、C# 3.0 では、特定の文脈下でしか var をキーワード扱いしないという仕様変更を行いました。

他にも、C# の破壊的変更として有名なのは generics 関係で a.Method(b < c, d > (e + f)) というようなコードは、C# 1.0 と 2.0 で意味が異なることが知られています。 (1.0 だと、大小比較 × 2 + 引数2つのメソッド呼び出し、 2.0 だと、generic メソッド呼び出し + 引数1つのメソッド呼び出し。 この例のようなコードはあまり書かれていないということで、 破壊的変更を許容しましたが、避けれるなら避けたかったのは間違いありません。

このように、文法の追加では、破壊的変更を可能な限り避ける配慮が必要です。 それも、「今回は破壊的変更にはならない」というだけでなく、「将来的に破壊的変更を生みそうなきわどい文法は避ける」 という配慮まで求められます。 このような、将来的な話まで含めて、future breaking change(将来の破壊的変更)なんていう言葉もあったりします。

内部言語的アプローチ、外部言語的アプローチ

DSL にいたる1つのシナリオは、本節で話をしたような、 「設定ファイルがだんだん凝ってきて1つの言語みたいになる」というようなものです。 凝りだすと、ほんとにコンパイラを作るくらいの労力がかかります。 というか、実際にコンパイラを作ってしまう人もいます。 これを外部言語アプローチと言っておきましょう。

もう1つ、 「ライブラリで凝ったことしようとして、元の言語の枠を抜け出そうとして独立する」というようなシナリオも考えられます。 あるいは、一部の動的言語では、「言語内に別言語を構築する」というような能力 (メタプログラミングといいます)を持ったものもあって、 この手の言語では、ライブラリ構築と DSL 作りの境目はかなりあいまいです。 こちらは内部言語アプローチです。

要するに、 ドメイン(開発対象の領域)に特化した開発というのは、 ライブラリ呼び出しのように言語内に埋め込む方法と、 設定ファイルのように外部から読み込む方法とがあるわけです。 (内部言語 VS 外部言語)

また、既存の枠組みを超えた新しい機能が欲しいなら、 言語の構文を拡張してしまうという手もありますし、 独立した言語を作る手もあります。 (言語拡張 VS DSL 化)

一長一短

どちらも一長一短ありますね。 例えばまた、「設定ファイルと DSL」で例に使ったようなゲームのシナリオの場合を考えてみましょう。

まず、外部に設定ファイルとして分離するなら、 設定ファイルのローダを自作する必要があります。 今回想定しているようなゲームシナリオだと、 パラメータ設定や条件分岐などの機能は最低限作りたいわけですが、 それだけでも結構面倒だったり。 また、ファイルの文法も、自作したものとなると開発ツールの補助が受けづらくなります。 (欲しいなら、Visual Studio のインテリセンス機能みたいなのや、 GUI ツールなんかも自作しないといけない。)

外部設定ファイルにする利点は、 設定(この場合はシナリオ)に変更があってもプログラムの再ビルドが必要ないとかがあります。 また、シナリオを書くのに必要な機能だけに絞っているので、 シナリオライターにとっては記述が楽になります。 (ゲームを作ったりする場合、プログラミング担当者とシナリオ担当者は別に立てるのが普通で、 シナリオ担当者はプログラミングに関しては素人なことが多いです。)

ライブラリとして完全に C# のコードに埋め込む場合、 その真逆ですね。 ローダやツールの自作が必要ない代わり、 シナリオ担当者に不要なプログラミング言語の勉強を強いることになります。 また、元言語(今回は C#)の文法に束縛されることになるので、 往々にして冗長な記述が必要になります。

具体例

さて、そろそろ概念的な話だけでなく、具体的な話をしたいわけですが、 長くなりそうなので別ページに分けることにします → 「DSL へのアプローチ(内部言語)」、 「DSL へのアプローチ(外部言語)」、 「DSL がらみのその他諸々」。

予定

      - UML も general-purpose
      - UML は元々はノートやホワイトボード、仕様書に書くことを想定したもの
      - コードの生成はそこまで想定されていない
      - UML を実行可能にする試みもあるけど・・・
      汎用的に作りすぎててなかなか難しい

    

まとめ

昨今、ドメイン特化型のモデル(モデリング言語)を作って、 そのドメイン特化型言語(DSL: Domain Specific Language)を使ってシステム開発を行うという流れがはやりつつあります。

「言語を作って」というと敷居が高そうなイメージがしますが、 このページでは、 DSL 作りといってもそれほど大げさなものではなく、 「ちょっと凝った設定ファイル」、「ちょっと凝ったライブラリ」くらいの位置づけだという話をしました。

もう少し具体例を挙げていきたいところですが、 長くなりそうなのでページを分けて、次節以降で話をします。

更新履歴

ブログ