タプル

タプルは、単一のオブジェクトとして使用する複数の値を組み合わせるためのものだ。これらは、std.typeconsモジュールにあるTupleテンプレートによってライブラリ機能として実装されている。

Tuple一部の操作では、std.metaモジュールにあるAliasSeqを使用している。

この章では、タプルのより一般的な操作のみを説明する。タプルとテンプレートに関する詳細情報は、Philippe Sigaudの「D Templates: A Tutorialを参照。

Tupleおよびtuple()

タプルは通常、便利な関数tuple()によって構築される。

import std.stdio;
import std.typecons;

void main() {
    auto t = tuple(42, "hello");
    writeln(t);
}

上記のtupleの呼び出しは、intの値42とstringの値"hello"で構成されるオブジェクトを構築する。プログラムの出力には、タプルオブジェクトとそのメンバーの型が含まれている。

Tuple!(int, string)(42, "hello")

上記のタプル型は、以下の擬似struct定義と同等であり、おそらくまったく同じ方法で実装されている。

// Tuple!(int, string)と同等
struct __Tuple_int_string {
    int __member_0;
    string __member_1;
}
D

タプルのメンバーは通常、そのインデックス値によってアクセスされる。この構文は、タプルがさまざまな型の要素で構成される配列と見なせることを示唆している。

writeln(t[0]);
writeln(t[1]);
D

出力:

42
hello
メンバープロパティ

tupleおよびTupleテンプレートがメンバー名でインスタンス化されている場合、プロパティによってメンバーにアクセスすることができる。以下の2つのメソッドは、同じ効果を持つ。

auto a = tuple!("number", "message")(42, "hello");
auto b = Tuple!(int, "number", string, "message")(42, "hello");
D

上記の定義により、.numberおよび.messageプロパティからもメンバーにアクセスできる。

writeln("by index 0 : ", a[0]);
writeln("by .number : ", a.number);
writeln("by index 1 : ", a[1]);
writeln("by .message: ", a.message);
D

出力:

インデックス042
.number42
インデックス1hello
.messagehello
メンバーを値のリストとして展開

タプルメンバーは、関数呼び出し時の引数リストなどとして使用できる値のリストとして展開することができる。メンバーは、.expandプロパティまたはスライスによって展開することができる。

import std.stdio;
import std.typecons;

void foo(int i, string s, double d, char c) {
    // ...
}

void bar(int i, double d, char c) {
    // ...
}

void main() {
    auto t = tuple(1, "2", 3.3, '4');

    // 次の2つの行は、
    // foo(1, "2", 3.3, '4')と同等:
    foo(t.expand);
    foo(t[]);

    // bar(1, 3.3, '4')の等価表現:
    bar(t[0], t[$-2..$]);
}

上記のタプルは、intstringdoublecharの4つの値で構成されている。これらの型は、foo()のパラメータリストと一致するため、そのメンバーの展開をfoo()の引数として使用することができる。bar()を呼び出すと、タプルの最初のメンバーと最後の2つのメンバーから、一致する引数リストが作成される。

メンバーが同じ配列の要素として互換性がある限り、タプルの展開は配列リテラルの要素値としても使用できる。

import std.stdio;
import std.typecons;

void main() {
    auto t = tuple(1, 2, 3);
    auto a = [ t.expand, t[] ];
    writeln(a);
}

上記の配列リテラルは、同じタプルを2回展開して初期化されている:

[1, 2, 3, 1, 2, 3]
コンパイル時foreach

値を展開できるため、タプルはforeach文でも使用できる。

auto t = tuple(42, "hello", 1.5);

foreach (i, member; t) {
    writefln("%s: %s", i, member);
}
D

出力:

042
1hello
21.5

上記のforeach文は、実行時にループが実行されるような誤解を与えるかもしれない。しかし、そうではない。むしろ、タプルのメンバーに対して操作を行うforeach文は、各メンバーについてループ本体を展開するものだ。上記のforeach文は、次のコードと同等だ。

{
    enum size_t i = 0;
    int member = t[i];
    writefln("%s: %s", i, member);
}
{
    enum size_t i = 1;
    string member = t[i];
    writefln("%s: %s", i, member);
}
{
    enum size_t i = 2;
    double member = t[i];
    writefln("%s: %s", i, member);
}
D

展開が行われる理由は、タプルのメンバーが異なる型の場合、foreach体は型ごとに別々にコンパイルされなければならないためだ。

より強力なループ展開機能であるstatic foreachについては、後の章で説明する。

関数から複数の値を返す

タプルは、関数が1つの値しか返せないという制限の簡単な解決策となる。その例がstd.algorithm.findSplitだ。findSplit()は、別の範囲内の範囲を検索し、見つかった範囲の前、見つかった範囲、見つかった範囲の後の3つの部分で構成される結果を生成する。

import std.algorithm;

// ...

    auto entireRange = "hello";
    auto searched = "ll";

    auto result = findSplit(entireRange, searched);

    writeln("before: ", result[0]);
    writeln("found : ", result[1]);
    writeln("after : ", result[2]);
D

出力:

he
発見ll
o

関数から複数の値を返すもう1つの方法は、structオブジェクトを返すことだ。

struct Result {
    // ...
}

Result foo() {
    // ...
}
D
AliasSeq

AliasSeqは、std.metaモジュールで定義されている。これは、通常はコンパイラによって使用されるが、それ以外ではプログラマがエンティティとして利用できない概念を表すために使用される。値、型、およびシンボル(つまり、aliasテンプレート引数)をコンマで区切ったリストだ。以下は、そのようなリストの3つの例だ。

次の3行のコードは、同じ順序でこれらのリストを示した例だ。

foo(1, "hello", 2.5);         // 関数引数
auto o = Bar!(char, long)();  // テンプレート引数
auto a = [ 1, 2, 3, 4 ];      // 配列リテラル要素
D

Tupleメンバーを展開する際、AliasSeqを利用している。

AliasSeqという名前は"alias sequence"から来ており、型、値、およびシンボルを含むことができる。(AliasSeqおよびstd.metaは、以前はそれぞれTypeTupleおよびstd.typetupleと呼ばれていた。)

この章では、型のみ、または値のみで構成されるAliasSeqの例を紹介する。型と値の両方を使用した例については、次の章で説明する。AliasSeqは、可変長のテンプレートで特に有用だ。これについても、次の章で説明する。

AliasSeq値で構成される

AliasSeqが表す値は、そのテンプレート引数として指定する。

3つのパラメータを取る関数を考えてみよう。

import std.stdio;

void foo(int i, string s, double d) {
    writefln("foo is called with %s, %s, and %s.", i, s, d);
}
D

この関数は通常、3つの引数で呼び出される。

foo(1, "hello", 2.5);
D

AliasSeqは、これらの引数を1つのエンティティとして結合し、関数を呼び出すときに自動的に展開することができる。

import std.meta;

// ...

    alias arguments = AliasSeq!(1, "hello", 2.5);
    foo(arguments);
D

この関数は1つの引数で呼び出されているように見えるが、上記のfoo()の呼び出しは、前の呼び出しと同等だ。その結果、どちらの呼び出しも、同じ出力が生成される。

fooの呼び出し
1hello2.5

また、argumentsは、autoなど、変数として定義されていないことにも注意。これは、特定のAliasSeqインスタンスのaliasだ。AliasSeqの変数を定義することも可能だが、この章の例では、それらをエイリアスとしてのみ使用する。

Tupleで見たように、値が同じ配列の要素として互換性がある場合、AliasSeqを使用して配列リテラルを初期化することもできる。

alias elements = AliasSeq!(1, 2, 3, 4);
auto arr = [ elements ];
assert(arr == [ 1, 2, 3, 4 ]);
D
インデックス付けとスライシング

Tupleと同じように、AliasSeqの要素はインデックスとスライスでアクセスできる:

alias arguments = AliasSeq!(1, "hello", 2.5);
assert(arguments[0] == 1);
assert(arguments[1] == "hello");
assert(arguments[2] == 2.5);
D

上記のAliasSeqの最後の2つのメンバーと一致するパラメータを持つ関数があるとする。この関数は、AliasSeqの最後の2つのメンバーだけのスライスを使用して呼び出すことができる。

void bar(string s, double d) {
    // ...
}

// ...

    bar(arguments[$-2 .. $]);
D
AliasSeqで構成される

AliasSeqのメンバーは、型で構成することができる。つまり、特定の型の特定の値ではなく、int自体のような型だ。型で構成されるAliasSeqは、テンプレート引数を表すことができる。

2つのパラメータを持つstructテンプレートを持つAliasSeqを使ってみよう。このテンプレートの最初のパラメータはメンバー配列の要素型を決定し、2番目のパラメータはメンバー関数の戻り値を決定する。

import std.conv;

struct S(ElementT, ResultT) {
    ElementT[] arr;

    ResultT length() {
        return to!ResultT(arr.length);
    }
}

void main() {
    auto s = S!(double, int)([ 1, 2, 3 ]);
    auto l = s.length();
}

上記のコードでは、テンプレートが(double, int)でインスタンス化されていることがわかる。AliasSeqも同じ引数リストを表すことができる:

import std.meta;

// ...

    alias Types = AliasSeq!(double, int);
    auto s = S!Types([ 1, 2, 3 ]);
D

Typesは単一のテンプレート引数のように見えるが、自動的に展開され、テンプレートのインスタンス化は以前のようにS!(double, int)になる。

AliasSeqは、可変長のテンプレートで特に便利だ。この例については、次の章で説明する。

AliasSeqを使用したforeach

Tupleと同じで、AliasSeqで動作するforeach文は、実行時のループではない。むしろ、各メンバーに対してループ本体が展開される。

これを、上で定義した構造体Sに対して書いたユニットテストで例を見てみよう。次のコードは、要素型intlong、およびfloatについてSをテストする。この例では、ResultTは常にsize_tだ。

unittest {
    alias Types = AliasSeq!(int, long, float);

    foreach (Type; Types) {
        auto s = S!(Type, size_t)([ Type.init, Type.init ]);
        assert(s.length() == 2);
    }
}
D

foreach変数Typeは、intlongfloatの順に対応している。その結果、foreach文は、以下のコードと同等のコードとしてコンパイルされる。

{
    auto s = S!(int, size_t)([ int.init, int.init ]);
    assert(s.length() == 2);
}
{
    auto s = S!(long, size_t)([ long.init, long.init ]);
    assert(s.length() == 2);
}
{
    auto s = S!(float, size_t)([ float.init, float.init ]);
    assert(s.length() == 2);
}
D
.tupleofプロパティ

.tupleofは、型またはオブジェクトのメンバーを表する。ユーザー定義型に適用すると、.tupleofはその型のメンバー定義へのアクセスを提供する。

import std.stdio;

struct S {
    int number;
    string message;
    double value;
}

void main() {
    foreach (i, MemberType; typeof(S.tupleof)) {
        writefln("Member %s:", i);
        writefln("  type: %s", MemberType.stringof);

        string name = S.tupleof[i].stringof;
        writefln("  name: %s", name);
    }
}

S.tupleofは、プログラム内の2箇所に現れる。まず、要素の型は、typeof.tupleofに適用して、各型がMemberType変数として現れるように取得される。次に、メンバーの名前は、S.tupleof[i].stringofによって取得される。

メンバー名前
0intnumber
1stringmessage
2doublevalue

.tupleofは、オブジェクトにも適用できる。その場合、オブジェクトのメンバーの値で構成されるタプルが生成される。

auto object = S(42, "hello", 1.5);

foreach (i, member; object.tupleof) {
    writefln("Member %s:", i);
    writefln("  type : %s", typeof(member).stringof);
    writefln("  value: %s", member);
}
D

foreach変数memberは、オブジェクトの各メンバーを表す:

メンバー
0int42
1stringhello
2double1.5

ここで重要な点は、.tupleofが返すタプルは、オブジェクトのコピーではなく、オブジェクト自体のメンバーで構成されていることだ。つまり、タプルのメンバーは、実際のオブジェクトのメンバーへの参照だ。

要約