DSL (Domain Specific Language)
DSL は特定用途向けの言語である。 特定用途の概念を直接表現でき、プログラマはいちいちその用途の概念をプログラミング言語の概念に翻訳して解釈する必要がない。
つまり、プログラムを書くときにはその用途の概念を直接用いて記述することができ、 プログラムを読むときにはプログラムの字面からその概念を直接読み取ることができる。 その際に、実装内部がどのように行われているかは気にする必要がない。
概念を表現するために、 専用の言語処理系を用いる場合と、既存の言語処理系内で実現する場合がある。 前者を外部DSL、後者を言語内DSL (内部DSL) という。 言語内DSL において使用する既存の言語をホスト言語という。
外部DSL
外部DSL は独立した言語処理系であるが、その言語が DSL かどうかは以下のような要素で判断できる。
- その言語処理系が DSL を名乗っているかどうか
- 特定用途にしか役に立たない機能が多いかどうか
しかし、Prolog が対象とする述語論理など、対象とする用途が広い場合には DSL と汎用言語の区別は曖昧になる。
外部DSL の利点は自由に言語を設計できることである。 しかし、このことは、プログラマはまったく新しい言語を学習しなければならないということも意味する。
また、対象とする用途から外れた部分をどのように扱うかという点も考えなければならない。
ひとつの方法は、対象用途から外れたものは記述不可能とする、というものである。 この方法をとると、DSL で記述されたプログラムを処理することがやりやすいという利点がある。 たとえば、yacc 用に記述されたソースから、BNF を取り出すとか、さらにそれからトークン列を導出するなどである。 あるいは、ページ記述言語で記述された文書からページ単位に切り出したり 2up したりすることも考えられる。 このような、DSL プログラムを処理するプログラム (DSL メタプログラム) を実現するには、 DSL でジェネリックな記述ができると都合が悪いことが多い。 たとえば、ページ記述言語 PostScript はループの中でページを生成することもできるため、一般にいえばページ単位に切り出すことはできない。
また、対象用途から外れたものも記述可能なようにするやりかたも考えられる。 これを行うためには、そのようなものををどのように記述するかが問題となる。 たとえば、yacc では、文法という対象用途を BNF で記述するとともに、C でセマンティクスを記述する。 このように、対象用途以外の部分を埋め込むことや、拡張を行うインターフェースが必要になる。
言語内DSL (内部DSL)
言語内DSL の場合、その記述から意味を読み取るのに、 言語のセマンティクスを経由することなく、 対象の概念を直接読み取ることができる。
DSL であるかどうかはプログラマが意味を考えるときの手間の大きさによって決まるため、 あるライブラリが言語内DSL であるかどうかは曖昧である。 メソッド名などに適切な単語を使うことは意味理解を助けるので、広く意味をとれば、それだけでも DSL とみなせる。
言語内DSL は外部DSL に比べると、一般に言語設計の自由度が低い。 具体的にどの程度低くなるかはホスト言語の自由度に依存する。 たとえば、Lisp は強力なマクロが提供されるためかなり自由度が高い。 しかし、自由度の低さは、プログラマはホスト言語の知識を利用でき、学習が容易にあるという利点も意味する。
また、対象とする用途から外れた部分を記述するには、ホスト言語を利用できる。 言語内 DSL はホスト言語のライブラリともみなせるので、ホスト言語の機能を混ぜて使うのは容易である。
例
- 外部DSL (専用の言語処理系)
- make は依存関係を記述する。その際、その関係がどのようにたどられるかはあまり気にしなくてよい。
- yacc は BNF で文法を記述する。その際、LALR についてはあまり気にしなくてよい
- sed は行単位のストリームを編集する
- 正規表現は文字列に対するパターンを記述する。その際、バックトラッキングの動作などはあまり気にせずにパターンを記述できる
- PostScript はページ記述に特化した言語である。
- SQL はデータベースアクセスのための言語である。
- Asymptote は作図のための言語である。
- 言語内DSL (既存の言語処理系内)
- Haskell 上のパーザコンビネータは BNF に近い形式で文法を記述する。その際、パーザの動作についてはあまり気にしなくてよい
- Ruby 上の rspec はテストを英語に近い形で記述する
- C 上の printf, scanf, strftime などは文字列でフォーマットを記述する
- Lisp のマクロは DSL の実現に使用されることが多い
例: Prolog
Prolog は一階述語論理を対象とした DSL とみなせる。 事実と規則を定義し、ある問い合わせがそれらから導けるかどうかを判定する。
Prolog に特徴的な機能として、入力から出力を求めるだけでなく、出力から入力を求められるという点があげられる。 たとえば、以下の append はリストの連結の関係を表現する。
append([],X,X). append([X|Y],Z,[X|W]) :- append(Y,Z,W).
これは以下のように連結に使用できるのに加え、連結の結果から入力を求めることにも使用できる。
?- append([1,2,3], [4,5,6], X). X = [1, 2, 3, 4, 5, 6]. ?- append(X, [4,5,6], [1,2,3,4,5,6]). X = [1, 2, 3]
Prolog には算術演算を行う is というプリミティブが用意されており、以下のように使用できる。
add(X,Y,Z) :- Z is X + Y.
この add は加算を行うのに使用できるが、加算の結果から入力を求めることには使用できない。
?- add(1,2,X). X = 3.
これは is が Prolog が扱う一階述語論理の外で定義されているためである。 つまり、一階述語論理の中では効率的には実現しにくい算術演算を無理に導入した結果、 算術演算については、出力から入力を求められるという機能を失ったとみなすことができる。
手間の節約
- 対象に関する以外の余計な記述が含まれていないぶん記述が少なくなる
思考の節約
- 言語のセマンティクスを経由せずにプログラムの意味を理解でき、また記述できる
参考文献
- Domain-Specific Languages: An Annotated Bibliography
- Arie van Deursen, Paul Klint, Joost Visser
- http://homepages.cwi.nl/~arie/papers/dslbib/
- DSL '09
- Domain Specific Languages ('09)
- http://dsl09.blogspot.com/