1. Dってどんな言語?

初出: 2007/01/13
最新: 2009/01/30
../表紙に戻る

目次 / 概要

まずは、D言語の言語的な特徴について、思いっきり大ざっぱに紹介します。 細かい話は全部はしょって、「D言語ってこんな感じなんだぜー」「へー」「ふーん」 という茶飲み話をするような勢いでいきます。D って名前だけは聞いたことあるけど、 実際のところどうなんよ?という方向けの紹介です。

逆に、さあ D を使い始めるぞ!と思った人が勉強用のテキストとするにはたぶん向いていません。 その方向では、haruさんによる「C/C++に疲れた人のD言語」で言語機能がひととおり丁寧に解説されていて、とてもおすすめです。 皆さんぜひぜひご覧ください。

基本は C

まずは、標準出力に "Hello, world!" と表示するだけのプログラムです。

import std.stdio;

void main()
{
    writeln( "Hello, world!" );
}

プログラムは main() 関数から始まります。関数の書き方や、中括弧 { } を使う辺りはC言語の系列に属する言語に共通の特徴ですね。 他にループや条件分岐、例外処理のための構文はどれもC系に共通のおなじみの文法です。 次のHelloWorld別バージョンをご覧下さい。

import std.stdio;

void main()
{
    writeln( hello('H') );
}

char hello( char firstLetter )
{
    write( firstLetter );

    try
    {
        for(int i=0; i<100; ++i)
        {
            char ch;
            switch(i)
            {
            case  0: ch = 'e'; break;
            case  1: ch = 'l'; break;
            case  2: ch = 'l'; break;
            case  3: ch = 'o'; break;
            case  4: ch = ','; break;
            case  5: ch = ' '; break;
            case  6: ch = 'w'; break;
            case  7: ch = 'o'; break;
            case  8: ch = 'r'; break;
            default: break;
            }
            if( i == 9 )
                throw new Exception("l");
            else
                write( ch );
        }
    }
    catch( Exception e )
    {
        write( e.msg );
    }
    finally
    {
        write( "d" );
    }

    return '!';
}

ええと、現実にこんな病的なハローワールドを書く人がいたら頭を抱えますが、 まあ、ともかく、基本的な構文はすべて C/C++/Java/C# と同じものを採用していることが おわかりいただけると思います。

ネイティブコンパイル

D は、C/C++ と同様、CPUの機械語に直接コンパイルされ実行される 「ネイティブコンパイル型」の言語です。

C:\Temp> dmd hello.d       // まずコンパイル
C:\dmd\bin\..\..\dm\bin\link.exe hello,,,user32+kernel32/noi;

C:\Temp> hello.exe         // 作った実行ファイルを実行
Hello, world!

ネイティブコンパイルの利点としてよく上がるのが、動作速度が速いことです。実際Dは "The Computer Language Shootout Benchmarks (CPU Time)" などでも最上位付近に います。でも、私としては多少の速度差はどうでもいいかなあと思っているので、 これはどうでもいいです。 (※ そもそも Shootout Benchmark は、未実装のベンチマークがある言語はスコアが 低くなっちゃいますし、言語処理系そのものの能力だけでなく、どれだけちゃんとした コードで実装されてるかも影響してきます。むしろ、「上位の言語はこういうのを押さえて やろうと気合いを入れてしまうようなユーザーがいる言語である」という指標として見るべきと思っています。 って、話がそれました。)

どちらかというと、ネイティブコンパイルの魅力は、OS や各種ミドルウェアとの互換性でしょう。たとえば

extern(System) int MessageBoxW( void*, const(wchar)*, const(wchar)*, int );

void main()
{
    MessageBoxW( null, "Hello, world!", "Title", 0 );
}

/* 実際は import std.c.windows.windows;  とimportすれば、
 * よく使うWindows APIを全部まとめて宣言できます。ここでは、他に特殊なツールとか要らないよ、
 * という説明のためにあえて自分で API を宣言して使う例を出しました。 */

特殊なラッパーライブラリに頼ったり、 呼び出し部分だけC言語で書いたりする必要もなく、標準のD言語一本槍で OS の API を 呼ぶことができます。上のコードは、Windows の MessageBoxW API を使っています。 逆に、拡張モジュールとしてシステム側から呼ばれるための、.dll や .so をDで作るのも簡単です。

だけどスクリプトっぽく

ただ単にネイティブな作業をしたいだけなら既にC言語が存在するのです。 D は、今時の言語にさまざまな形で入っている 「プログラミングを楽にする」 ための機能を取り入れたネイティブコンパイル型言語、という位置づけです。

例えば、サクサクっとお手軽テキスト処理してみましょう。

#!/usr/local/bin/rdmd
import std.regexp;
import std.stdio;

void main( string[] arg )
{
    // プログラムの1個目の引数を、パターンとする
    RegExp pat = new RegExp( arg[1] );

    // 標準入力から1行ずつ読んで
    for( string line; (line=readln()) != null; )
        // マッチしたら
        if( pat.match(line) )
            // 表示
            write( line );
}

引数に正規表現を渡すと、標準入力のうち、その正規表現にマッチする行だけを 表示するプログラムです。いわゆるgrepですね。正規表現ライブラリが標準で 用意されてる点、foreach で1行ずつ読み込む機能などがポイントです。

C:\Temp> dmd grep.d
C:\Temp> grep a.*a < grep.d    // aを2個以上含む行だけ表示
void main( string[] arg )
    RegExp pat = new RegExp( arg[1] );
        if( pat.match(line) )

あ、でも、いちいちコンパイルしてから実行するのって面倒ですよね。 そういう時のために、コンパイラ(dmd)の他に、ソースコードを渡すと直接 実行してくれる rdmd コマンドというのも用意されています。

C:\Temp> rdmd grep.d a.*a < grep.d
void main( string[] arg )
    RegExp pat = new RegExp( arg[1] );
        if( pat.match(line) )

実際は内部でテンポラリフォルダにコンパイルしてから実行しているだけなんですが、 使う分には、ruby コマンドや perl コマンドのようにインタプリタっぽい使い勝手で使えます。 さらに。D言語の普通の行コメントは // なのですが、 先頭行に限り、#! 以降を無視することになっています。 これを利用すると、Unix風のOSでは、いわゆるshebang

#!/usr/local/bin/rdmd
import std.regexp;
...

で Dスクリプト を動かすことまでできちゃいます。

(※ もちろんPerlやRubyやPythonなら、例として使ったgrepなどはもっとずっと短く 記述できます。本当にこういう小さなスクリプトだけを記述するならば D の出番はなくて、それらの言語を使うのが最適解でしょう。Dの使いどころは、 ネイティブネイティブしいプログラムの中で自然にこういうテキスト処理ができたり、 逆にスクリプトっぽいお手軽プログラムを静的型チェックありでコンパイルして高速実行しつつ OS の API を叩けたりする、そういうどっちにも行けるフットワークの軽さにある、と思います。)

以下の数節は、主に C/C++ と比べて、D がどんな感じに楽ちん手抜きプログラミングできるように なっているかの紹介になっています。メインに使う言語が例えばRubyのひとは、 「C++よりも多少はRubyっぽく書けるC++」としてDを使ってみて、メインに使う言語が例えばC++のひとは、 「RubyよりもかなりC++っぽく書けるRuby」としてDを使ってみると幸せ、かもしれません。

ガベージコレクション

手動でfreeやdeleteしなくても、 要らなくなったメモリは勝手に解放してくれますよーというあれです。

void main()
{
    for(;;)
    {
        Object obj = new Object();
    }
}

がメモリ不足で落ちたりせず、ちゃんと無限ループします。便利便利。

Dは基本はガベコレで便利なのですが、一方でガベコレの難点は、勝手に解放と言っても いつ解放されるのかがはっきりしないこと。メモリ以外の解放には ほとんど使えません。メモリ以外のリソースの後始末は… C#のusingやPythonのwith、Rubyだとブロック付きメソッドを使うところでしょうか。 Dでは、scope 属性というのがあります。

import std.stream;

void main()
{
    for(int i=0;; ++i)
    {
        scope File fp = new File("out.txt", FileMode.Out);
          // scope == 変数fpのスコープが終わると同時に、Fileのデストラクタ呼び出し
        fp.seekEnd(0);
        fp.writefln(i);
          // 要するに毎回ここで必ずファイルを閉じる
    }
}

変数宣言の前に scope と書くと、その変数のスコープが終わる瞬間に必ず、 束縛されたオブジェクトのデストラクタが呼び出されるようになります。C++ のローカル変数と 同じ挙動ともいいます。

あるいは、finally 節で後処理を書く、という手もありますね。 例外で出ても普通に終わっても、その後処理が必ず実行されます。 特別な終了処理メソッドをもったオブジェクトをわざわざ作る手間がいらないので、 その場限りの処理には楽な手段です。Dにもfinallyはありますが、 scope文という別の手段もあります。

import std.stdio;

void main()
{
    write("<tagA>");
    scope(exit) { write("</tagA>"); }

    write("<tagB>");
    scope(exit) { write("</tagB>"); }

    write("Hello");
}
// <tagA><tagB>Hello</tagB></tagA>

scope(exit) 文は、スコープを抜ける時に必ず実行されます。 finallyと違い、初期化と終了処理を近い場所に書けるので読みやすいのが利点。 他に、例外発生時のみ呼ばれる scope(failure)、正常フローで抜けるときだけの 呼ばれる scope(success) もあります。

配列いろいろ

Dでは文字列は文字の配列です。テキスト処理を充実させるための必然として、 他のC系言語と比べて、配列処理が非常に充実しています。

配列の結合

import std.stdio;

void main()
{
    int[] a = [1, 2, 3];
    int[] b = [4, 5, 6];
    int[] c = a ~ b;
    c ~= 7;
    c ~= [8, 9];

    writeln( c ); // [1 2 3 4 5 6 7 8 9]
}

演算子 ~ が結合演算子です。~= で末尾に追加。 要素1個をくっつけるのも、別の配列とくっつけるのも同じ演算子です。

配列のスライス

import std.stdio;

void main()
{
    int[] a = [0,1,2,3,4,5,6,7,8,9];

    writefln( a[1..3] ~ a[6..$] ); // [1 2 6 7 8 9]

    a[$-3..$] = 99;
    writefln( a ); // [0 1 2 3 4 5 6 99 99 99]
}

配列の一部を別の配列として取り出す「スライス」です。 演算子 [ .. ] はRubyで言う [ ... ] ですね。 最後の要素は含みません。スライスや添え字のなかでのみ特殊変数 $ が使えて、配列の長さを指します。

連想配列

「ハッシュ」「マップ」などとも呼ばれる、連番の整数以外でも添え字にできる 配列みたいなものです。

import std.stdio;

void main()
{
    int[string] number;  // stringをキー、intを値とする連想配列

    number = ["one" : 1, "two" : 2]; // 初期化したり
    number["three"] = 3; // 追加したり

    writeln( number["three"] );
    if( "four" in number )
        writefln( number["four"] );
    else
        writeln( "not found" );
}

配列のメソッド風関数

import std.stdio;

int findComma( string str )
{
    foreach( int i, char ch ; str )
        if( ch == ',' )
            return i;
    return -1;
}

void main()
{
    writeln( "Hello, world!".findComma() );
}

Dは動的にクラスの定義を変えたりできるような言語では(残念ながら)ないので、 既存のクラスに後でメソッドを足すようなことはできません。ですが、 配列についてだけは特別に、「配列を第一引数にとる関数」をメソッドのような ドット記法で呼び出すことが許されています。文字列も文字の配列なのでこの記法が使えます。 地味に便利です。

やや関数型プログラミング

Dでは、関数を別の関数の引数として渡したりすることができます。無名関数もあります。 (厳密に言うとこの例で渡しているのは関数ではなく "delegate" で、周囲の環境と 関数をセットにしたものです。)

import std.stdio;

void doThreeTimes( void delegate() dg )
{
    dg(); dg(); dg();
}

void main()
{
    void hello() { writeln("Hello"); }
    doThreeTimes( &hello );

    string msg = "World!";
    doThreeTimes(  {writefln(msg);}  );
}

mainの最後の行のように、式として { ~ } とブロックを書くと、 ゼロ引数の無名関数扱いになります。

import std.stdio;

void poorSort( int[] array, bool delegate(int,int) cmp )
{
    foreach( i, inout ie ; array )
        foreach( j, inout je ; array[i+1..$] )
            if( !cmp(ie,je) )
                {int t=ie; ie=je; je=t;}
}

void main()
{
    int[] a = [3,5,1,2,6,4,7,9,8];

    a.poorSort( (int x,int y){ return x>y; } );
    writeln(a); // [9 8 7 6 5 4 3 2 1]
    a.poorSort( (int x,int y){ return x<y; } );
    writeln(a); // [1 2 3 4 5 6 7 8 9]
}

このように、{ ~ } の前に引数を指定してうけとることも可能です。

"Revenge of the Nerds" のアキュムレータの例は、D言語の場合こうなります。

int delegate(int) foo(int n)
{
    return (int i){ return n+=i; };
}

静的型

D は「静的に型付けされる」言語です。変数には必ず型が与えられて、 型が合わないプログラムを書くと、実行する前、コンパイル時点で検出されて エラーになります。

import std.regexp;
import std.stdio;

void main( string[] arg )
{
    int x = "100"; // エラー! "100" は整数じゃない
    int y = 23;
    writefln( y + "10" ); // エラー! 整数と文字列は足せない

    RegExp pat = arg[1]; // エラー! arg[1] は正規表現オブジェクトじゃない
    RegExp pat = new RegExp( arg[1] ); // OK
}

C++やJavaなんかと同じで、Rubyなどとは違っている特徴です。 実行前の早い段階で間違いが検出されるというのは、まあ嬉しいことです。 嬉しいのですが、問題もまたあります。

これに関しては、変数宣言の際に、型の代わりに auto と書くことで 初期値から型が適当に推論されます。scope など他の修飾子が もともとついている場合は、auto すらも省略できます。 あと、foreachの変数の型宣言も省略OKです。

auto y = 23;  // y は int 型になる
auto pat = new RegExp( arg[1] ); // pat は RegExp 型になる

scope fp = new File("readme.txt"); // fp は File 型になる

auto a = ["aaa", "bbb", "ccc"];  // a は string[3] 型になる
foreach( elem ; a ) { ... }      // elemはstring型
foreach( idx, elem ; a ) { ... } // idxはulong型、elemはstring型

ただし、Haskell や OCaml で有名な型推論とは違って、関数の引数や返値には型を書かないとダメです。 他に型があると面倒な点として

これは「テンプレート」を使います。

import std.stdio;

// 何型のeでもいいけど、とにかくn個並べた配列にして返す
T[] repeat(T)( int n, T e )
{
    T[] arr = new T[n];
    arr[] = e;
    return arr;
}

void main()
{
    writeln( repeat(10, 1) );    // 整数でも
    writeln( repeat(10, 2+3i) ); // 複素数でも

    writeln( repeat(10, "aa") );  // 文字列でも
    writeln( repeat(10, [3]) );  // 配列でも
}

repeatが「関数テンプレート」です。 型は何でもいいよ、という部分をパラメータ (T) 化しています。 JavaやC#のGenericsよりも、C++のテンプレートに仕組みとしては似ています。 クラステンプレート class Vector(T) { ... } も勿論あり。 テンプレートについてはかなり興味深い部分なので、「3.D言語各論」でもう少し掘り下げようと思います。

オブジェクト指向

class があって interface があります。よくある感じのオブジェクト指向です。 interface は多重に実装できますが、class は多重継承できません。 全てのクラスのルートクラスは Object です。 継承するときの記法はclassでもinterfaceでもコロン : で。

import std.stdio;

interface Animal
{
    string how_you_cry();
}
class Dog   : Animal { string how_you_cry() { return "bark"; } }
class Cat   : Animal { string how_you_cry() { return "mew"; } }
class Horse : Animal { string how_you_cry() { return "neigh"; } }
class Cow   : Animal { string how_you_cry() { return "moo"; } }
class Mouse : Animal { string how_you_cry() { return "squeak"; } }

void main()
{
    Animal[] a;
    a ~= new Dog;
    a ~= new Cat;
    a ~= new Horse;
    a ~= new Cow;
    a ~= new Mouse;
    foreach(animal ; a)
       writeln( animal.how_you_cry() );
}

繰り返しになりますが、クラス実装の多重継承はありません。 クラス間で実装を共有したいときは、mixin を使います。 たとえば、クラス A とクラス B で同じ処理内容の関数 map を共有したい場合、 以下のようなコードになります。

import std.stdio;

class A
{
    void each( void delegate(int x) yield )
    {
        yield(1);
        yield(2);
        yield(3);
    }

    // Enumerableテンプレートをmixin!
    mixin Enumerable!(int);
}

class B
{
    void each( void delegate(string x) yield )
    {
        yield("aaa");
        yield("bbb");
        yield("ccc");
    }

    // Enumerableテンプレートをmixin!
    mixin Enumerable!(string);
}

// 手抜き実装
template Enumerable(T)
{
    S[] map(S)( S delegate(T) dg )
    {
        S[] s;
        each( (T t){s ~= dg(t);} );
        return s;
    }
}

void main()
{
    auto a = new A;
    writefln( a.map((int x){return x*2;}) ); 
      // [2 4 6]

    auto b = new B;
    writefln( b.map((string x){return x~x;}) ); 
      // [aaaaaa bbbbbb cccccc]
}

Dでは、テンプレートの形で、複数の宣言(型宣言、関数宣言、変数宣言など) をまとめておくことができます。これをクラスに mixin することで、一気にクラスに 多数の宣言を追加しています。上の例だと手抜きなので1個の宣言しか追加していませんが。

契約プログラミング

関数の引数や、クラスのメンバ変数には常に何かの型がついています。 が、型さえあっていればどんな値を入れてもいいかというと、そうでないことも多いです。 「この char 型引数にはASCII文字しか渡してはいけない」とか、「この int 型メンバ変数は、 別の配列の添え字なので、添え字の範囲に収まってないとダメ」 など。 逆に、関数の返値について「この Object 型の返値は絶対nullではない」と保証できる場面もあるでしょう。

こういう条件を、コメントやドキュメントではなく、コードとしてきっちり表現しておこう、 というのが D の契約プログラミングです。

class Time
{
    private int hour;
    private int minute;
    private int second;

    invariant()
    {
        //「クラス不変条件」
        //   invariant には、クラスのメンバが必ず満たしているべき条件を記述します
        //   public メソッドの呼び出し前後にこれら条件が自動で検査されるようになります
        //   (ちなみにinvariantの中で自身のpublicメソッドを呼ぶとinvariantの
        //     無限再帰が始まって落ちるので注意(^^;)
        assert( 0 <= hour   && hour   <=23 );
        assert( 0 <= minute && minute <=59 );
        assert( 0 <= second && second <=59 );
    }
}

T[] map(T,S)( S[] array, T delegate(S) dg )
  in
  {
      //「事前条件」
      //   in には、引数に課したい条件を書きます。dg に null を渡すの禁止、など。
      assert( dg !is null );
  }
  out(result)
  {
      //「事後条件」
      //   out には、返値に対して保証する条件を書きます。必ず入力と同じ長さの配列を返すよ、など。
      //   return が複数あるときも、1個outを書けば全てのreturn時にチェックが入ります。
      assert( array.length == result.length );
  }
  body
  {
      // 事前、事後条件つきメソッドの本体は body の中に入れます。
      // これは、引数の配列 array の全要素に関数 dg を適用して返すmap関数の実装
      T[] t;
      foreach(a; array)
          t ~= dg(a);
      return t;
  }

単にドキュメントで残しとくだけよりも、実際にチェックコードを走らせる方がより安全だし 仕様の有効活用にもなってるでしょう、という考え方。in はともかく out や invariant 契約は 単に assert を手で並べるだけでは実現が面倒なので、言語としてサポートされているわけです。

「../2.開発環境のインストール」へ

presented by k.inaba (kiki .a.t. kmonos.net) under CC0