お待ちかねD言語の記事です。
D Language
言語の紹介記事なんてありふれていて、D言語にもそういうものは沢山あるわけで、何故今更そんな記事を書く必要があるんだ。車輪の再発明じゃないのか。みたいな事を言われるかもしれません。
でもそんな事はどうでもいいんだよ。D言語の紹介をしたいんだよ。
D言語とD言語くん
なんかD言語くん流行ってるらしいじゃないですか。
「D言語好きです」みたいな話しても「あー、あのマスコットの」という反応が返ってくる事が多くて、うちの会社の人事の人すらD言語くん(とGopherくんとLispエイリアン)知ってたりして、本当にD言語くんだけは人口に膾炙してるんだなぁと実感したりもするのですが、 D言語くんばかりが人気になってD言語の人気が高まらないという問題 とかあったりすると思うのですよ。
えっ、そもそもD言語くんも人気ではない?
というわけで本記事の対象読者は、
- D言語くんは知ってるけどD言語は知らない
- D言語はやったことないけどCとかC#とかJavaとかはやった事ある
- D言語を知りたいけどまだ本を買ってみる程では……
という人達です。
この記事を読んで少しでも興味が出たら、TDPL本(The D Programming Language、邦訳『プログラミング言語D』)を読んで頂ければと思います。
文法と演算
まずハローワールド見てみましょうか。
import std.stdio;
void main()
{
writeln("Hello, world!");
}
CとかJavaとかC#とかやってきた人にとっては比較的見慣れた文法に見えるかと思います。
Dはコンパイラ言語なので、実行するにはコンパイルが必要です。VMとかは特に無くて、ネイティブなバイナリコードにコンパイルされますので、実行環境毎にコンパイルが必要になります。
とりあえずお使いの環境にあったコンパイラを用意しましょう。DMD(公式)、GDC(GCCバックエンド)、LDC(LLVMバックエンド)などが有名です。
ここではDMDを使った例を出します。まず上のコードを hello.d
のような名前で保存します。そしてコマンドラインからコンパイルし、実行します。
$ dmd hello.d
$ ./hello
Hello, world!
いいですね。色々オプションがありますので、 dmd --help
で調べてみて下さい。
また、rdmdを使うと、コンパイルして即実行ができます。
$ rdmd hello.d
Hello, world!
以降は、このあたりのコンパイル――実行の過程は省略します。
次にFizzBazzを見てみましょう。
void fizzBuzz()
{
for (auto i = 1; i <= 100; ++i) {
if (i % 15 == 0) {
writeln("FizzBazz!");
} else if (i%5 == 0) {
writeln("Buzz!");
} else if (i%3 == 0) {
writeln("Fizz!");
} else {
writeln(i);
}
}
}
void main()
{
fizzBuzz();
}
「main関数から始まるのか」とか、「行末にセミコロン必須なのか」とか、「返り値は関数の前に書くのか」とか、書き慣れている反面、ちょっと古臭い印象を受けてしまう感じがあるかもしれません。
Dは2000年代はじめの言語です。確かに、GoとかRustのようなモダンな言語と比較すると、文法も機能もレトロな感じがするかもしれません。
でもそれは置いておきましょう。重要な事じゃありません。
抽象化
上に挙げたFizzBuzzはまるでCで書いたかのようなコードですね。実際、細部を少し変更すればCでそのまま動きます。
それはそれで嬉しいですが(つまり逆に言えば、CのコードをそのままDで動かせるという事です)、しかし我々はDを書いているはずですね。
Dの標準ライブラリPhobosには、配列やリストのようなコレクションを Range
という形で抽象化して扱う為の関数が豊富に用意されています。
これを利用して、fizzBuzzを書き直してみます。
import std.stdio;
import std.range: iota;
import std.algorithm: map, joiner;
import std.conv : to;
string fizzBuzz()
{
return 1.iota(101).map!(i =>
i%15 == 0 ? "FizzBuzz!"
: i%5 == 0 ? "Buzz!"
: i%3 == 0 ? "Fizz!"
: i.to!string
).joiner("\n").to!string;
}
void main()
{
writeln(fizzBuzz());
}
まず最初に言っておきますが、 条件演算子の連続については気にしないでください 。
Dのif文は式ではないのでこれは致し方ないのです……。
ただ気になるその部分を除きますと、先に記述した手続き型的なコードから一変、抽象的なかなりモダンな感じになっているではありませんか。
これは、手続き型的な書き方と、オブジェクト指向や関数型のような抽象的な書き方、どちらの方が優れているとかいないとか、 そういう話ではありません 。重要なのは、D言語を使うと、そのどちらのアプローチでも取る事ができるという事です。しかも自然に!
利点としてよく挙げられる事ですが、D言語は「ウェブアプリケーションのような高度に抽象化されたコードから、インラインアセンブラを用いるような機械語と隣合わせのコードまで」を扱う事ができます。
UFCS
上の例で、
1.iota(101).map!( ... ).joiner("\n").to!string;
のようなコードがありました。
実は、ここに出てくる iota
map
joiner
to
といったキーワードは全て関数です。メソッドではありません。
このコードは、
to!(string)(joiner(map!( ... )(iota(1, 100)), "\n"));
と等価です。
これはD言語のUFCSという機能で、 fun(arg1, arg2, arg3)
という関数呼び出しを、 arg1.fun(arg2, arg3)
と書く事ができるのです。オブジェクト指向のメソッド呼び出しに慣れている人からすると、何となく慣れた見た目に見えてコードが読みやすくなったりしないでしょうか? 私はします。
F#やElixirのパイプ演算子( |>
)を思い出しますね。
ちなみにその逆、メソッドを関数のように呼び出す事はできません。
Rustにある同じ名前のUFCSという仕組みはそのようなものらしいですが(もっともこれは、複数のtraitが同名のメソッドを提供していた場合に曖昧さを無くす為の必要な機能らしいです。D言語のUFCSとは全く関係ない?)。
あ、あと、 map!( ... )
とか to!string
とかの !
は、次の実引数がテンプレート引数である事を表しています。C++とかC#とかJavaで言うところの < ... >
ですね。
コンパイル時メタプログラミング
D言語で頻出の4文字略語といえば、上のUFCSと、CTFEです。これは「コンパイル時関数評価」の事で、その名の通りの機能です。D言語では多くのコードがコンパイル時にも実行可能なように設計されています。
上のFizzBuzzを書き換えてみましょう。
string fizzBuzz()
{
return 1.iota(101).map!(i =>
i%15 == 0 ? "FizzBuzz!"
: i%5 == 0 ? "Buzz!"
: i%3 == 0 ? "Fizz!"
: i.to!string
).joiner("\n").to!string;
}
enum fb = fizzBuzz();
void main()
{
writeln(fb);
}
enumは列挙体を示すキーワードですが、列挙せずに enum xxx = ...
みたいに使う時、明示的な定数の宣言を意味します。
なので、ここでの fb
は定数なのでコンパイル時にその値が決まっている事になります。コンパイラはコンパイル時にこの宣言を見て、そして =
の右側を見て、「お、この値を定めるには関数を評価しないといけないな」と判断するわけです。
そして関数を評価します。何も問題はありません。関数 fizzBuzz
の中で使われている演算子や関数は、全てコンパイル時に評価可能だからです。最終的にコンパイラは、 "1\n2\nFizz!\n...
という文字列をコンパイル時に生成して、そして定数 fb
に束縛します。
プログラムを実行すると、既に生成されバイナリに埋め込まれた文字列が表示されるのです。
一般的に、コンパイル時の計算速度はコンパイルされたコードの速度に比べてかなり遅いらしいですが、コンパイルを1回すればバイナリは使いまわせるので、コンパイル時と実行時、どちらに計算した方がお得かははっきりしています(加えてエラーも、実行時よりコンパイル時に出た方がありがたいですよね)。
D言語のCTFEは本当に強力で、凄腕D言語erはほとんどの処理をコンパイル時に終わらせて、挙句実行はしないなどという事もザラだそうです(誇張表現)。
D言語のCTFEの虜になった方は、是非ともアンサイクロペディアのD言語のページもご参照下さい。
なお、「あんまりメタプログラミングじゃないなぁ……」と感じられた方。D言語の mixin
は文字列をコードとして埋め込む事ができます。そして埋め込む文字列は、CTFEを使ってコンパイル時に生成できます。後はやりたい放題です。
テンプレート
C++のテンプレートの強力さはよく知られていますが、D言語のテンプレートはC++のそれ以上に豊富な機能を持っています。
void swap(T)(ref T a, ref T b)
{
auto tmp = a;
a = b;
b = tmp;
}
よく見るswap関数ですね。
では比較的よく見ないテンプレート関数を。これはコンパイル時テンプレートFizzBuzzです。
import std.stdio;
import std.conv: to;
string fizzBuzz(int i, int max, string fb)() if(i%3 == 0 && i%15 != 0 && i < max)
{
return fizzBuzz!(i+1, max, fb ~ "Fizz!\n")();
}
string fizzBuzz(int i, int max, string fb)() if(i%5 == 0 && i%15 != 0 && i < max)
{
return fizzBuzz!(i+1, max, fb ~ "Buzz!\n")();
}
string fizzBuzz(int i, int max, string fb)() if(i%15 == 0 && i < max)
{
return fizzBuzz!(i+1, max, fb ~ "FizzBuzz!\n")();
}
string fizzBuzz(int i, int max, string fb)() if(i%3 != 0 && i%5 != 0 && i < max)
{
return fizzBuzz!(i+1, max, fb ~ i.to!string ~ "\n")();
}
string fizzBuzz(int i, int max, string fb)() if(i == max)
{
return fb;
}
void main()
{
writeln(fizzBuzz!(1, 101, "")());
}
楽しい。
上のよくあるswap関数との違いを見ていきましょうか。
まず、Dのテンプレートは型だけでなく値も取れます。C++と違って文字列とかも直接渡せるので、色々できます。
また、 シグネチャ制約 を書く事ができます。関数の仮引数リストの後、本文の前に if(...)
という形で条件を書くと、コンパイル時にそれをチェックして条件に当てはまった場合にそのテンプレートを呼び出すようになります。
これを利用して、何となく関数型言語を思わせる上のような書き方ができたりします。
ただあくまでコンパイル時の評価なので、実行時にパターンマッチをする事はできません。残念。
そして、よくある関数型の挙動とは違い、上から評価していくのではなく最適マッチなので、 曖昧な呼び出しがあるとコンパイルエラーになります 。
ちなみにこの辺の機能は標準ライブラリPhobosでは頻出です(例みたいに意味のない変な事はしていませんが)。 map
関数とかで対象にしている Range
という概念は、このシグネチャ制約とコンパイル時関数評価を用いて確かめられています。
テンプレートは用法用量を守って正しく使用しましょう。
不変性
前の投稿で不変性について熱く語りましたが、D言語の immutable
は本当に強力です。強力ながら、stringが immutable(char)[]
のエイリアスだったりしてカジュアルに表に出てくるのがDの良いところです。
immutable
struct Hoge
{
int num;
string str;
int[] arr;
}
void main()
{
auto hoge = immutable Hoge(42, "immutable", [1,2,3]);
// hoge.num = 99; // 不可能!
// hoge.str = "mutable"; // 不可能!
// hoge.arr = [4,5,6]; // 不可能!
// hoge.arr[1] = 100; // これも不可能!
}
きっちりと immutable
を使う事で、参照透過性を守りやすくなり、コードの見通しが良くなるのみならず並列処理も行いやすくなります。
ユニットテスト
テストを書くのは好きですか? 書く事はさて置きましょうか。では、テストのあるコードは好きですか? きっと大勢の方が好きだと言うでしょう。
D言語にはユニットテスト機能が備わっています。
コードの中に、
unittest {
...
}
のように書いて、コンパイル時に -unittest
オプションを付加すれば、 unittest
で囲んだ部分を実行してくれます。一般的に、この中にアサーションを書いてユニットテストを行います。
実際に書いてみましょう。
まずテスト対象が必要ですね。
1から9までの数字の列が与えられます。その中に「含まれていない」数字の一覧を返す関数を書いてみます。パズルを解く時とかに使えそうですね。
import std.array;
import std.range;
int[] findLackedNumbers(in int[] numbers)
{
static immutable allNumbers = 1.iota(10).array;
auto lackedNumbers = allNumbers.dup;
foreach (num; numbers)
foreach (i, restNum; lackedNumbers)
if (num == restNum)
lackedNumbers = lackedNumbers[0 .. i] ~ lackedNumbers[i+1 .. $];
return lackedNumbers;
}
ふむふむ、愚直な実装ですね。
printf目視テストで確かめてみましょう。
import std.stdio;
void main()
{
writeln(findLackedNumbers([1,2,3,5,6,8,9]));
writeln(findLackedNumbers([1]));
writeln(findLackedNumbers([1,9]));
writeln(findLackedNumbers([9]));
writeln(findLackedNumbers([1,2,3,4,5,6,7,8,9]));
writeln(findLackedNumbers([]));
writeln(findLackedNumbers([1,1,1,2,2,3,3,4]));
}
[4, 7]
[2, 3, 4, 5, 6, 7, 8, 9]
[2, 3, 4, 5, 6, 7, 8]
[1, 2, 3, 4, 5, 6, 7, 8]
[]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[5, 6, 7, 8, 9]
何となく正しそうな感じがします(コーナーケースとかカバーできてなかったらごめんなさい!)
このテストを、assertを使って unittest
の中に書いてみます。
unittest
{
assert(findLackedNumbers([1,2,3,5,6,8,9]) == [4,7]);
assert(findLackedNumbers([1]) == [2,3,4,5,6,7,8,9]);
assert(findLackedNumbers([1,9]) == [2,3,4,5,6,7,8]);
assert(findLackedNumbers([9]) == [1,2,3,4,5,6,7,8]);
assert(findLackedNumbers([1,2,3,4,5,6,7,8,9]) == []);
assert(findLackedNumbers([]) == [1,2,3,4,5,6,7,8,9]);
assert(findLackedNumbers([1,1,1,2,2,3,3,4]) == [5,6,7,8,9]);
}
これで -unittest
オプションを付けてコンパイルする度にユニットテストが走り、関数の正しさを確かめてくれます! こんなに嬉しい事はないですね。
ところで、もう一度この関数の実装を見てみましょう。
計算量が O(N^2)
です。酷いですね。 O(N^2)
ではなくない?(lackedNumbersの長さが高々9なので) という意見もありますが、まぁともかく、あんまり良くない計算量だと思います。
もうちょっとアルゴリズムを改善してみましょうか。
int[] findLackedNumbers(in int[] numbers)
{
int[9] appearedCount;
foreach (i; numbers)
++appearedCount[i-1];
int[] lackedNumbers;
foreach (int i, c; appearedCount)
if (c == 0)
lackedNumbers ~= i+1;
return lackedNumbers;
}
二重ループを一重にする事に成功しました。引数の配列が充分に長い場合は、効率化が期待できますね。
しかし代償に、ロジックが少々複雑になってしまいました。果たしてこの修正した関数は、以前と同じように動くのでしょうか?
その為のユニットテストです。このコードを -unittest
オプション付きでコンパイルしてみましょう。
……無事、全てのユニットテストが通りました! つまりこのコードのロジックは以前と変わらないという事になります(テストが正しければ、ですが)。
このように、気軽にユニットテストを書ける事でコードの質を改善しやすくなります。
その他、言いたいこと
D言語は良い言語ですが、足りないものもあります。パターンマッチとか、分配束縛とか。
ですが素晴らしい点がとても多い。ここに書ききれなかった分だと、 pure
属性や @nogc
アノテーション、契約に基く事前・事後条件の設定、そして並列処理のサポートなど。
楽しく書く事ができる言語の1つだと思います。
D言語くんだけでなく、D言語の方にも、もっと興味をもって頂ければ幸いです!