概要
前節では、「DSL を作ってから、それを使って開発」というのがはやりつつありますが、 言語作りといってもそれほど大げさな話ではないというような話をしました。 また、DSL のアプローチとして、 「ちょっと凝った設定ファイル」とか「ちょっと凝ったライブラリ」というような流れがあるということも話しました。
このページでは、「ちょっと凝ったライブラリ」の方、 すなわち、内部言語的なアプローチを、 具体例を挙げて紹介します。
内部言語
何かプログラムを作る際、 まずはライブラリを整備してからプログラムを作ることが多々あります。 そして、 ライブラリを作っていると、 元言語の文法の制約を越えたいと思うことがときどきあります。 時には、マクロやプリプロセスを使って無理やり言語の制約を越えたものを作ろうとするライブラリもあります。
でも、そういうやり方の場合、 無理やり感はぬぐえないことが多くて、 最終的には新しい言語や文法を作ろうという流れになります。
ここで、「1つの独立した新しい言語を作る」というのと、 「元言語に新しい文法を追加する」というのの2つの流れがあるわけです。 一般論として言うと、 ライブラリの提供する機能があまり汎用的ではない場合には、ドメイン特化言語(DSL)として元言語から分離して「汎用言語+DSL混在開発」する。 汎用的ならば元言語を言語拡張するのが自然な流れかと思います。
ここでは、そういう流れの過程として、 以下のようなものを紹介したいと思います。
-
あくまで元言語のライブラリとして提供されるもの
-
言語自体がメタプログラミングのための機能を有するもの
-
言語の拡張
-
言語内に別言語を埋め込み
ライブラリ
元言語の文法に準拠して、ライブラリ提供という形で、 元言語のモデリングパラダイムを越えようとしているようなものもあったり。
形としてはライブラリであったりマクロであったりはするものの、 新しいパラダイムを作ろうとしていたり、 1つの言語として扱うに足りるものもあります。
大体こういうのはパラダイムシフトの途中段階で起きる過渡的な現象で、 新しいパラダイムが世に受け入れられれば、 最終的には新しい言語になることが多いです。 (逆に、受け入れられず人知れず消えていくことも多々。)
C++ template
まず、マクロとして提供されていたものが、後に言語拡張として新しい構文になった例として、 C++ の template があります。
C++ 自身も、その起源は C 言語に対するプリプロセスで実現されていていました。
SystemC
SystemC は C++ のライブラリとして提供されてるハードウェア設計言語。 ソフトウェア的に動作検証もできるし、 ハードウェア記述を生成することも可能。
あくまで C++ のライブラリでしかないんですが、 C++ の目的(ソフトウェア開発)と異なる目的(ハードウェア設計)に使うし、 C++ のパラダイム(オブジェクト指向)よりも抽象度の高いモデル(システムレベル設計)を扱おうとしているので、 1つの言語として扱われることが多いです。
最初に書いたとおり、 こういう元言語よりも抽象度の高い新しいパラダイムを目指す物は、 いずれは独立した言語になっていくものなんですが。 それを目指した SpecC という言語が政治的・商業的理由で失敗していたり、 対象とする分野(ハードウェア設計)が開発者人口の少ない所なのが災いして、 SystemC が業界標準の地位に納まりそうな雰囲気。
OpenMP
C/C++ に並列プログラミング機能を追加するためのライブラリ。 ライブラリと、 #pragma (対応していないコンパイラには無視される)擬似命令を使って記述。 OpenMP に対応していないコンパイラでコンパイルすると、 並列化されないだけで動作結果は一致するように書ける。
PS3 とかの、Cell プロセッサ向け開発環境はこれを使ってるらしい。
並列化プログラミングパラダイムなんて、まさに過渡的な状況の真っ只中。 どういう書式がプログラマーにとって書きやすいのかとか、 パフォーマンスがよくなるかとか、 まだ研究段階というかきっちりと固まっていない感じ。 なので、#pragma を使ったりといった、「対応していないコンパイラへの配慮」みたいなものが必要なんだと思います。
メタプログラミング
言語によっては、言語内に別のミニ言語を構築できるような機能を持つものがあります。 その手の言語では、 ライブラリ作りと言語作りの境目があいまいで、 まず目的に応じたライブラリ = ミニ言語(要するに DSL)を構築してから、 そのミニ言語を使ってプログラミングを行うというのが日常茶飯事です。
LISP などの関数型言語や、Ruby などの動的スクリプト言語が好んでこういうアプローチを取りたがるんですが、 こういう機能をメタプログラミングといいます。
メタプログラミングの例
まあ、サイト内に解説があることですし、 ここでは 「PowerShell」 を使った例をあげてみます。 (参考: 「Windows PowerShell」。)
詳細は PowerShell の解説の方を読んでいただくとして、 PowerShell には本来、クラスを定義するための構文がありません。 (.NET Framework のクラスを呼び出す機能だけ持っていて、 自作のクラスを使いたい場合には、C# などを使って書いたものを読み込んで使う。)
ですが、ちょっとしたライブラリを書くことで、 以下のような構文でクラスもどきを作ることができます。
Def-Class Point {
var x
var y
new {
param($x = 0, $y = 0)
$_.x = $x
$_.y = $y
}
method abs {
[Math]::Sqrt($_.x * $_.x + $_.y * $_.y)
}
method scale {
param($a)
$_.x = $a * $_.x
$_.y = $a * $_.y
}
}
メンバー変数として x, y を持っていて、 絶対値を求めるメソッド abs と、定数倍するメソッド scale があります。 また、new がコンストラクタになります。
このクラスを利用する側は以下のような感じ。
$x = New-Instance Point
$y = New-Instance Point 3 4
function show($x)
{
'({0}, {1}), abs = {2}' -f $x.x, $x.y, (Call-Method $x abs)
}
show $x
show $y
Call-Method $y scale 2
show $y
(PowerShell in Action って本にこういう話があるらしい。 ここで例に挙げたコードは、その話を聞いて自作してみたもの。 その本を読んだことはないんでこれであってるかわからないけど。)
これを実現するための“ちょっとしたライブラリ”の中身はこんな感じ → LibClass.ps1。 (あと、上述のクラス定義・利用の例はこちら → TestLibClass.ps1。)
Def-Class や New-Instance、var、method などの正体はいずれも関数だったりします。 まあ、見ての通り、結構無理やりです。
この“無理やり”を面白いと思うか、 単なる自己満足と捕らえるか、 あるいは、 むしろ無茶苦茶なコード生みかねない欠点と考えるか、結構人それぞれなんですが。
メタプログラミングの問題
こういうメタプログラミングという発想は、 非常に熱狂的な信者がいる一方で、 一般にはあまりはやってはいません。 もちろん、ミニ言語作成の難易度が高いという問題もありますが、 メタプログラミングの問題はそれだけではないと思います。
普通のプログラミング言語としては貧弱
1つの問題は、 普通のプログラミングがやりやすい言語と、 メタプログラミングしやすい言語は違うというものです。
LISP なんかは、信者の方曰く「メタプログラミングができる非常に強力な言語」という話ですが、 逆に、普通のプログラミングがしにくい言語です。 元々持っている構文が貧弱で、 むしろ、「メタプログラミングしてからじゃないとろくに使えない言語」という側面もあります。
複数の設計方針の混在
もう1つ、こちらの方が深刻だと思うんですが、 言語内に複数の設計方針が混在してしまうという問題もあります。
例えば、先ほどの例だと、本来は自前でクラスを定義するという概念を持たないはずの PowerShell でクラスが定義できる状態になっています。 まあ、この例の場合、PowerShell は元々 .NET Framework のクラスを使えるので、 それほど変には思わないとは思いますが。 やろうと思えばもっと奇抜な構文を作ったりもできます。
やれることが多いというのは利点であると同時に欠点でもあります。 なんでも自由にさせるよりも、 ある程度のガイドラインで束縛した方が生産性が高かったりします。
方言の発生
上述のような奇抜な構文作りは、さらに悪いことに、 みんな好き勝手に構文を作れるわけです。
人によって、パラダイムも違う、コーディングルールも違う、 下手すると、まったく同じことやるのに2つの別の構文ができるなんてことも起こりうる。 ミニ言語を作った本人はいいですが、 それ以外の人にとってはたまったものじゃない。
新しい言語っていうのは、作るのはもちろん、覚えて使うってのも結構な重労働です。 普通のライブラリの使い方でも覚えるのは大変なのに、 構文からして奇抜なものなんてなかなか覚えられません。
ツールによるサポート
最近では、プログラミングは IDE(Integraged Development Environment: 統合開発環境)などのツールのサポートを受ける前提で作られています。 ツールが優秀になっていて、サポートを受けながらなら学習も利用もずいぶん楽になっています。
このような背景のもと、ツールまで含めた設計が必要なため、 DSL 作成のハードルが高くなっています。 また、「せっかく手間や学習コストを減らすために DSL を作ったのに、ツールのサポートがないから結局汎用言語の方が楽」 という事態も起こっています。
言語拡張
C++ の template なんかがそうですが、 マクロなどを使って構築された言語もどきは、 有用性が認められれば最終的には新しい言語になったり、 元言語に新構文として取り入れられたりします。 内部言語的アプローチの1つの終着点は、このような言語構文の拡張だと思います。
ただ、そういう構文が本当に有用かどうかの判断には長い時間がかかります。 あまりよく吟味せずに新構文を追加すると、 言語仕様が無駄に膨れ上がって逆に使いにくくなる可能性もあります。
「モデル化」で言ったように、 よいモデルというのは、必要十分で過不足のないものです。 何でもかんでもやろうとすると複雑すぎて逆に何もできなくなります。 また、わざわざ言語を拡張しなくても、 別言語にして混在開発する手もあるので、 言語拡張は慎重に行うべきです。
DSL(ドメイン特化言語)という言葉がはやってる昨今、 特定用途向けになりそうなものは別言語化、 汎用的に使えそうなものは言語拡張というのが大まかな方針になりそう。
LINQ
言語拡張で、最近話題のものというとまずは C# 3.0 の LINQ(Language Integrated Query)。
詳細は「C# 3.0 の新機能」を見てもらうとして、 簡単に言うと、 C# や VB などのプログラミング言語に SQL 風のデータベース問い合わせ言語を統合するものです。 (バージョン的には C# 3.0、VB9 から。)
これ、もろに「メタプログラミングの問題」でいったような「言語設計方針の混在」なんですよね。 オブジェクト指向プログラミング(OOP)パラダイムとリレーショナルデータベース(RDB)パラダイムの混在。 いいことずくめでもなく、混乱の原因になるかもしれないものです。
ただ、LINQ の場合、元々「OOP と RDB の壁を取り払いたい」という要望があってのもので、 目指すところは「混在」ではなくて「統合」です。 また、C# や VB のコンパイラ自体にデータベース問い合わせ機能を追加するんじゃなくて、 SQL 風クエリをデータベースライブラリのメソッド呼び出しに変換するだけ、 実際の問い合わせはライブラリまかせ、という仕様です。 さらに、SQL 言語はもうかなり長い歴史を持っていますし、 今後言語仕様に劇的な変化がありそうにもないです。
LINQ は、 「必要性・要望が高い」、 「コンパイラの負担を抑えた実装が可能」、 「SQL 言語の仕様がかなりしっかり固まっている」 という3点があって初めて追加された構文だと思います。
VB9 の XML 統合
VB9 では、(C# 3.0 にも追加された)LINQ に加えて、 「XML 統合」もありました。 要するに、以下のような感じで VB のソースコード中に XML を埋め込むことができます。
Dim doc =
<configuration>
<window
width="480"
height="360"
/>
</configuration>
こういう書き方で、XDocument 型のインスタンスが生成されるみたい。
XML 統合も、LINQ と同じくある程度の要望はあります。 これも、単なる構文糖で、実際はライブラリ呼び出しに変換されます。 また、今のところは、データの表現方式として XML がかなり標準的な地位を持っています。
でも、LINQ(クエリ統合)と比べると需要は小さいですし、 今はともかく将来的にも XML が標準であり続けるか、 あるいは仕様の拡張があったりしないか、 というのを考えると、この言語拡張は結構リスキーなんですよね。 XML 統合が VB9 にはあって C# 3.0 にはないのはそういう理由からです。
インラインコード
C/C++ にはインラインアセンブラといって、アセンブリ言語を C 言語コード中に埋め込める機能があります。 (標準機能ではないですが、ほとんどのコンパイラがこの機能を持っています。) これも一種の言語内言語ですよね。 これはドメイン特化ではなく、 異なる抽象度の混在開発が目的ですが。
(一般に、抽象度が低い方が細かいチューニングができて、ピーク性能はよくなります。 プログラム全体をアセンブリ言語のような抽象度の低いもので書くのは、 労力に見合わないし、人的ミスによってかえってパフォーマンスが落ちたりしますが、 ボトルネックになっている部分だけ抽象度を下げることで性能向上を目指します。 )
まあ、「限界までチューニングを求められる分野」というのを特殊な開発領域だと考えるなら、 この抽象度混在開発もドメイン特化型開発の一種とみなせます。
Perl の inline 文
Perl には、ほんとにソース内に別言語を埋め込める仕組みがあるみたい。
-
まず、
install Inline::言語名
でモジュールをインストールする -
ソース中に、
use Inline 言語名
をいうような記述を書くことで、 ソース中に別言語を含められる
別言語を言語内に埋め込んではあるんですが、 実際には外部のコンパイラ・インタプリタ呼び出すみたいです。 (見た目は内部言語的、実質は外部言語的。) 要するに、 コンパイル → 作った実行モジュールのロード・関数のコールみたいな部分を自動化してくれる機能。
でも、こういう方式は、 やりすぎるとソースファイルの中身が何なのか分からないカオスな状態になります。 異なるパラダイムが1つのソースに混在するのはやはりあまりよいものではありません。
最近は、こういうソースファイル中への別言語の埋め込みよりも、 .NET Framework などのような共通基盤を用意して、 異なる言語間の橋渡しをする方が主流です。
Fortress の構文拡張
Fortress には syntax expander(直訳するなら構文拡張器?)ってのがあるみたい。
Fortress の構文拡張では、まず、 パース(構文解析)用の関数を自分で書かないといけません。 例えば、Fortress プログラム中に SQL ライクな構文を追加するために、 SQL のパーサ parseSql 関数を書くとします。 そして、以下のようにして、syntax expander を定義します。
syntax sql e end = parseSql(e)
すると、 Fortress のソース中に、以下のような構文が書けるようになります。
sql
SELECT x FROM points
end
sql ~ end の間のブロックの中身が parseSql 関数に渡されて、 それに基づいて parseSql 内で処理が行われるっぽい。
これも、やってることはコンパイラの自作&自作言語の埋め込みみたいなものですね。 (見た目は内部言語的、実質は外部言語的。) 「メタプログラミング」機能でもあります。
はっきりいって、数値計算用の言語でパーサ書くなんてナンセンスですけど。 (Fortess は数値計算用。 自称では「general purpose」って言ってますけど、どうみても汎用プログラミングには必要なさそうな機能もてんこ盛り。) 外部コンパイラを呼び出す Perl の inline 構文の方がまだ使いやすそう。
まあ、Fortress の売りは、 「数学記号に近い記法でプログラムを書けること」です。 数学記号は、一般に、プログラミング言語よりも自由な書き方をしますので、 Fortress の組み込みの機能で対応できないような数学記号に対応するために備わっている機能じゃないかと思います。