ミックスイン

ミックスインは、生成されたコードソースコードにミックスインするためのものだ。ミックスインされたコードは、テンプレートインスタンスまたはstringとして生成される。

コードは、文字列インポートとしてプログラムに挿入することもできる。

テンプレートミックスイン

テンプレート とその他のテンプレート"の章で、テンプレートは、コンパイラがそのパターンから実際のインスタンスを生成するためのパターンとしてコードを定義することを学んだ。テンプレートは、関数、構造体、共用体、クラス、インターフェース、およびその他の合法的なDコードを生成することができる。

テンプレートミックスインは、mixinキーワードによって、テンプレートのインスタンス化をコードに挿入する。

mixin a_template!(template_parameters)
D

以下の例でわかるように、mixinキーワードはテンプレートミックスインの定義でも使われる。

特定のテンプレートパラメーターのセットに対するテンプレートのインスタンス化は、mixinキーワードが現れるソースコードのその位置に挿入される。

例えば、エッジの配列と、そのエッジを操作する2つの関数を定義するテンプレートがあるとする。

mixin template EdgeArrayFeature(T, size_t count) {
    T[count] edges;

    void setEdge(size_t index, T edge) {
        edges[index] = edge;
    }

    void printEdges() {
        writeln("The edges:");

        foreach (i, edge; edges) {
            writef("%s:%s ", i, edge);
        }

        writeln();
    }
}

このテンプレートでは、配列の要素の型と数は柔軟にできる。intおよび2に対するこのテンプレートのインスタンス化は、次の構文によってミックスインされる。

mixin EdgeArrayFeature!(int, 2);
D

例えば、上記のmixinは、2要素のint配列と、テンプレートによって生成された2つの関数を、struct定義の内部に挿入することができる。

struct Line {
     mixin EdgeArrayFeature!(int, 2);
}

その結果、Lineは、メンバー配列と2つのメンバー関数を定義することになる。

import std.stdio;

void main() {
    auto line = Line();
    line.setEdge(0, 100);
    line.setEdge(1, 200);
    line.printEdges();
}

出力:

エッジ
0100
1200

同じテンプレートの別のインスタンスは、例えば関数内で使用することができる。

struct Point {
    int x;
    int y;
}

void main() {
    mixin EdgeArrayFeature!(Point, 5);

    setEdge(3, Point(3, 3));
    printEdges();
}

このmixinは、main()内に配列と2つのローカル関数を挿入する。出力:

エッジ
0Point(0, 0)
1Point(0, 0)
2Point(0, 0)
3Point(3, 3)
4Point(0, 0)
テンプレートミックスインはローカルインポートを使用する必要がある

テンプレートのインスタンス化をそのままミックスインすると、テンプレート自体が使用しているモジュールに関する問題が発生する可能性がある: これらのモジュールは、mixinのサイトでは利用できない可能性がある。

aという名前のモジュールを考えてみよう。当然、このモジュールは、使用しているstd.stringモジュールをインポートする必要がある。

module a;

import std.string;    // ← 間違った場所

mixin template A(T) {
    string a() {
        T[] array;
        // ...
        return format("%(%s, %)", array);
    }
}
D

しかし、std.stringが実際のmixinサイトにインポートされていない場合、コンパイラはその時点でformat()の定義を見つけることができない。aをインポートし、そのモジュールからA!intをミックスインしようとする次のプログラムを考えてみよう。

import a;

void main() {
    mixin A!int;    // ← コンパイルエラー
}
D
エラー: 未定義の識別子形式
エラー: ミックスインdeneme.main.A!intのインスタンス化エラー

そのため、テンプレートミックスインが使用するモジュールは、ローカルスコープでインポートする必要がある。

module a;

mixin template A(T) {
    string a() {
        import std.string;    // ←正しい場所

        T[] array;
        // ...
        return format("%(%s, %)", array);
    }
}
D

テンプレート定義内にある限り、上記のimportディレクティブはa()関数の外にも置くことができる。

ミックスインする型の識別

ミックスインは、それをミックスインしている実際の型を識別する必要がある場合がある。この情報は、テンプレートパラメータのthisを通じて入手できる。これは、テンプレートの詳細の章で説明した。

mixin template MyMixin(T) {
    void foo(this MixingType)() {
        import std.stdio;
        writefln("The actual type that is mixing in: %s",
                 MixingType.stringof);
    }
}

struct MyStruct {
    mixin MyMixin!(int);
}

void main() {
    auto a = MyStruct();
    a.foo();
}

プログラムの出力から、実際の型はMyStructとしてテンプレート内で利用可能であることがわかる。

実際にミックスインされている型MyStruct
文字列ミックスイン

Dのもう1つの強力な機能は、コンパイル時にその文字列が認識されている限り、stringとしてコードを挿入できることだ。文字列ミックスインの構文には、括弧の使用が必要だ。

mixin (compile_time_generated_string)
D

例えば、hello worldプログラムは、mixinを使用して次のように記述することもできる。

import std.stdio;

void main() {
    mixin (`writeln("Hello, World!");`);
}

文字列がコードとして挿入され、プログラムは次の出力を生成する:

Hello, World!

さらに、プログラム全体を文字列ミックスインとして挿入することもできる。

mixin (
`import std.stdio; void main() { writeln("Hello, World!"); }`
);

これらの例では、文字列はコードとして記述することもできたため、ミックスインは必要ないことは明らかだ。

文字列ミックスインの威力は、コンパイル時にコードを生成できることにある。次の例では、CTFEを利用して、コンパイル時に文を生成している。

import std.stdio;

string printStatement(string message) {
    return `writeln("` ~ message ~ `");`;
}

void main() {
    mixin (printStatement("Hello, World!"));
    mixin (printStatement("Hi, World!"));
}

出力:

Hello, World!
Hi, World!

"writeln"式は、printStatement()内で実行されないことに注意。むしろ、printStatement()は、main()内で実行されるwriteln()式を含むコードを生成する。生成されたコードは、以下と同等だ。

import std.stdio;

void main() {
    writeln("Hello, World!");
    writeln("Hi, World!");
}
mixinの複数の引数

コンパイル時にすべてが既知の場合、mixinは複数の引数を受け取り、その文字列表現を自動的に連結する:

  mixin ("const a = ", int.sizeof, ";");
D

これは、例えばformat式を使用する場合よりも便利である。

  mixin (format!"const a = %s;"(int.sizeof));  // 上記と同じ
D
文字列ミックスインのデバッグ

生成されたコードはソースコード全体としてすぐには確認できないため、mixin式でコンパイルエラーの原因を特定するのは難しい場合がある。文字列ミックスインのデバッグを支援するために、 dmdコンパイラスイッチ-mixinがある。これは、ミックスインされたすべてのコードを指定したファイルに書き出する。

次のようなプログラムを考えてみよう。このプログラムには、ミックスインされているコードに構文エラーがある。コンパイラエラーからは、structメンバーの定義の末尾にセミコロンが欠落していることが明確ではない:

string makeStruct(string name, string member) {
  import std.format;
  return format!"struct %s {\n  int %s\n}"(name, member);
}

mixin (makeStruct("S", "m"));    // ← コンパイルエラー

void main() {
}

-mixinスイッチを指定してコンパイルすると、コンパイルエラーは指定したファイル ( 以下の例ではmixed_in_code) 内の行を指す。

dmd -mixin=mixed_in_code deneme.d
mixed_in_code(154): エラー: セミコロンが予想されるが、}ではない
Bash

標準ライブラリによって混入された他のすべてのコードと共に、指定されたファイル内のmixed_in_codeの行に以下のコードが追加される:

[...]
// deneme.d(6)の拡張
struct S {
  int m
}        ← 154行

文字列ミックスインをデバッグするもう1つのオプションは、 pragma(msg)である。これはコンパイル時に生成されたコードを出力する。ただし、デバッグのために一時的にmixinキーワードをpragma(msg)に置き換える必要があるため、実用性は低い。

pragma(msg, makeStruct("S", "m"));
D
ミックスインの名前空間

テンプレートミックスインでの名前の曖昧さを回避および解決することができる。

例えば、次のプログラムでは、main()内に2つのi変数が定義されている。1つはmainで明示的に定義され、もう1つはミックスインされている。ミックスインされた名前が、周囲のスコープにある名前と同じ場合、周囲のスコープにある名前が使用される。

import std.stdio;

template Templ() {
    int i;

    void print() {
        writeln(i);  // Templで定義されている'i'を常に表示する
    }
}

void main() {
    int i;
    mixin Templ;

    i = 42;      // mainで明示的に定義されている'i'を設定する
    writeln(i);  // mainで明示的に定義されている'i'を表示する
    print();     // 混合されている'i'を表示する
}

上記のコメントで示唆されているように、テンプレートミックスインは、その内容のための名前空間を定義し、テンプレートコードに現れる名前は、まずその名前空間で検索される。これは、print()の動作で確認できる。

42
0     ← by print()によって表示される

複数のテンプレートミックスインで同じ名前が定義されている場合、コンパイラは名前衝突を解決できない。同じテンプレートインスタンスを2回ミックスインする短いプログラムでこれを確認しよう。

template Templ() {
    int i;
}

void main() {
    mixin Templ;
    mixin Templ;

    i = 42;        // ← コンパイルエラー
}
エラー: deneme.main.Templ!().iが ... deneme.main.Templ!().iと
 競合している ...

これを防ぐために、テンプレートミックスインに名前空間識別子を割り当て、その識別子で含まれる名前を参照することができる。

mixin Templ A;    // A.iを定義する
mixin Templ B;    // B.iを定義する

A.i = 42;         // ← 曖昧さがなくなった
D

文字列ミックスインには、このような名前空間機能はない。ただし、単純なラッパーテンプレートを介して文字列を渡すだけで、文字列をテンプレートミックスインとして使用することは簡単だ。

まず、文字列ミックスインで同様の名前衝突が発生する場合を見てみよう。

void main() {
    mixin ("int i;");
    mixin ("int i;");    // ← コンパイルエラー

    i = 42;
}
エラー: deneme.main.iの宣言はすでに定義されている

この問題を解決する1つの方法は、文字列ミックスインをテンプレートミックスインに効果的に変換する、次の単純なテンプレートを介してstringを渡すことだ。

template Templatize(string str) {
    mixin (str);
}

void main() {
    mixin Templatize!("int i;") A;    // A.iを定義する
    mixin Templatize!("int i;") B;    // B.iを定義する

    A.i = 42;                         // ← 曖昧さがなくなった
}
演算子オーバーロードにおける文字列ミックスイン

演算子オーバーロードの章で、mixin式がいくつかの演算子の定義にどのように役立ったかを見てきた。

実際、ほとんどの演算子メンバー関数がテンプレートとして定義されているのは、演算子をstring値として利用可能にし、コード生成に使用できるようにするためだ。この章とその演習問題の解答で、その例を見た。

デストラクタへの混在

ユーザー定義型に複数のデストラクタを混合することができる。これらのデストラクタは、それらを追加したmixin文の逆の順序で呼び出される。この機能により、異なるリソースを1つの型に混合し、それぞれ独自のクリーンアップコードを導入することができる。

import std.stdio;

mixin template Foo() {
    ~this() {
        writeln("Destructor mixed-in by Foo");
    }
}

mixin template Bar() {
    ~this() {
        writeln("Destructor mixed-in by Bar");
    }
}

struct S {
    ~this() {
        writeln("Actual destructor");
    }
    mixin Foo;
    mixin Bar;
}

void main() {
    auto s = S();
}
Barによってミックスインされたデストラクタ
Fooによってミックスインされたデストラクタ
実際のデストラクタ

この記事の執筆時点では、バグのため、コンストラクタなどの他の特殊関数には同じ動作は適用されない。さらに、文字列ミックスインによってミックスインされたデストラクタは、その型の既存のデストラクタと競合する。

テキストファイルのインポート

テキストファイルの内容をコンパイル時にコードに挿入することができる。内容はstringリテラルとして扱われ、文字列が使用できる場所ならどこでも使用することができる。例えば、コードとして混合することができる。

例えば、ファイルシステムに、file_onefile_twoという2つのテキストファイルがあり、その内容が以下のとおりであるとする。

string 次のプログラム内の2つのimportディレクティブは、コンパイル時にそれらのファイルの内容が文字列リテラルに変換されたものに対応する。

void main() {
    string s = import ("file_one");
    mixin (import ("file_two"));
}

テキストファイルのインポート(別名、文字列のインポート)には、テキストファイルの場所をコンパイラに指示する-Jコンパイラスイッチが必要だ。例えば、2つのファイルが現在のディレクトリ(Linux環境では.で指定)にある場合、次のコマンドでプログラムをコンパイルできる。

dmd -J. deneme.d
Bash

出力:

こんにちは、世界!

ファイルの内容をstringリテラルとして考慮すると、プログラムは次のものと同等になる:

void main() {
    string s = `Hello`;         // ← file_oneの内容を文字列として
    mixin (`s ~= ", World!";
import std.stdio;
writeln(s);`);                  // ← file_twoの内容を文字列として
}

さらに、混在する文字列も考慮すると、プログラムは次のものと同等になる:

void main() {
    string s = `Hello`;
    s ~= ", World!";
    import std.stdio;
    writeln(s);
}

(注釈:ラムダ構文がDに追加される前は、述語を文字列で指定するのがより一般的だった。この例のような文字列述語はPhobosでも引き続き使用されているが、ほとんどの場合、=>ラムダ構文の方が適しているだろう。)

数値の配列を受け取り、特定の条件を満たす要素で構成される別の配列を返す、次の関数テンプレートを考えてみよう。

int[] filter(string predicate)(int[] numbers) {
    int[] result;

    foreach (number; numbers) {
        if (mixin (predicate)) {
            result ~= number;
        }
    }

    return result;
}
D

この関数テンプレートは、フィルタリング条件をテンプレートパラメータとして受け取り、その条件をそのままif文に挿入する。

例えば、7未満の数を選択する条件の場合、ifの条件は次のコードのように記述する必要がある:

if (number < 7) {
D

filter()テンプレートのユーザーは、条件をstringとして指定できる:

int[] numbers = [ 1, 8, 6, -2, 10 ];
int[] chosen = filter!"number < 7"(numbers);
D

重要なことは、テンプレートパラメータで使用される名前は、filter()の実装で使用される変数の名前と一致しなければならないことだ。したがって、テンプレートにはその名前が何かを文書化し、ユーザーはその名前を使用しなければならない。

Phobosでは、a、b、nなどの単一の文字で構成される名前を使用している。