タプル
タプルとは、要素の列です。要素としては、 型や、式、alias を持つことができます。 各タプルの要素数はコンパイル時に固定され、 実行時に変更することはできません。
タプルは、構造体と配列の、両方の特徴を備えています。 構造体のように、 タプルの各要素は異なった型を持つことができます。 一方で、配列のように要素に添え字でアクセスすることができます。
さて、では、タプルを使うにはどうすればよいのでしょうか? 特にタプルリテラルなどはありません。可変個引数テンプレートがタプルを作るので、 これを利用してタプル生成テンプレートを定義することが可能です:
template Tuple(E...) { alias E Tuple; }
これは次のように使うことができます:
Tuple!(int, long, float) // 3つの型のタプル Tuple!(3, 7, 'c') // 3つの式のタプル Tuple!(int, 8) // 型と式の混合タプル
タプルをシンボルで参照するには、aliasを使います:
alias Tuple!(float, float, 3) TP; // TP はfloat二つと3のタプル
タプルはテンプレートの引数とすることができて、その場合、 引数のリストへと展開 (flatten) されます。 この動作を使うと、既存のタプルに要素を追加したり、 別のタプルを結合するのは簡単です:
alias Tuple!(TP, 8) TR; // TR は float,float,3,8 alias Tuple!(TP, TP) TS; // TS は float,float,3,float,float,3
タプルには配列と共通の特徴が多くあります。 まず、タプルの要素数は、 .length プロパティで取得できます:
TP.length // 3 と評価される
タプルは添え字アクセスできます:
TP[1] f = TP[2]; // f はfloat型と宣言され、3に初期化される
さらにスライスも取れます:
alias TP[0..length-1] TQ; // TQ は Tuple!(float, float) と同じ
そう、[ ] の中では length が定義されているのも同じです。 ただし、制限がひとつあります。 添え字やスライスの値はコンパイル時に評価される定数でなければなりません。
void foo(int i) { TQ[i] x; // エラー。i は定数でない }
これを使うと、タプルの先頭 'head' と残り 'tail' を取り出すのも簡単です。先頭は単純に TP[0] で、残りは TP[1 .. length] です。 head と tail ができたので、 あとは条件コンパイルを少し組み合わせると、 テンプレートによる古典的な再帰アルゴリズムの実装ができるようになります。 例として、次のテンプレートは、 先頭の型引数 T を残りの型リスト TL から取り除いたタプルを返します:
template Erase(T, TL...) { static if (TL.length == 0) // 長さ 0 の場合。自分自身を返す alias TL Erase; else static if (is(T == TL[0])) // タプルの先頭要素にマッチ。残りを返す alias TL[1 .. length] Erase; else // 先頭はマッチせず。headと、再帰操作を適用した残りを結合 alias Tuple!(TL[0], Erase!(T, TL[1 .. length])) Erase; }
型タプル
タプルの要素が全て型であるとき、 型タプル(TypeTuple)と呼ばれます。 (型リストと呼ばれることもあります。) 関数の引数リストは型のリストなので、 そこから型タプルを取り出すことができます。 たとえば、IsExpression を使う方法があります:
int foo(int x, long y); ... static if (is(foo P == function)) alias P TP; // TP は Tuple!(int, long) と同じ
これの汎用版が std.traits.ParameterTypeTuple テンプレートです:
import std.traits; ... alias ParameterTypeTuple!(foo) TP; // TP はタプル (int, long)
逆に TypeTuple で関数を宣言することもできます:
float bar(TP); // float bar(int, long) と同じ
暗黙の関数テンプレートのインスタンス化が行われる場合には、 引数型を表す型タプルも推論されます:
int foo(int x, long y); void Bar(R, P...)(R function(P)) { writefln("return type is ", typeid(R)); writefln("parameter types are ", typeid(P)); } ... Bar(&foo);
出力はこうなります:
return type is int parameter types are (int,long)
この推論を使うことで、 任意の型と任意の個数の引数を持つ関数を書くことができます:
void Abc(P...)(P p) { writefln("parameter types are ", typeid(P)); } Abc(3, 7L, 6.8);
出力:
parameter types are (int,long,double)
この辺りに関してもっと詳しくは、 可変個引数テンプレート をどうぞ。
式タプル
タプルの要素が全て式であるとき、 式タプル(ExpressionTuple)と呼ばれます。 タプルテンプレートで作成することができます:
alias Tuple!(3, 7L, 6.8) ET; ... writefln(ET); // 表示は 376.8 writefln(ET[1]); // 表示は 7 writefln(ET[1..length]); // 表示は 76.8
配列リテラルを作るのに応用することも可能です:
alias Tuple!(3, 7, 6) AT; ... int[] a = [AT]; // [3,7,6] と同じ
構造体やクラスのデータフィールドは、 .tupleof プロパティを使うと式タプルに変換することができます:
struct S { int x; long y; } void foo(int a, long b) { writefln(a, b); } ... S s; s.x = 7; s.y = 8; foo(s.x, s.y); // 表示は 78 foo(s.tupleof); // 表示は 78 s.tupleof[1] = 9; s.tupleof[0] = 10; foo(s.tupleof); // 表示は 109 s.tupleof[2] = 11; // エラー。S には3つめのフィールドはない
typeof を使うと、 構造体から、データフィールドの型のタプルを作ることができます:
writefln(typeid(typeof(S.tupleof))); // prints (int,long)
これは std.traits.FieldTypeTuple テンプレートとしてカプセル化されています。
ループ
関数型プログラミングのような head-tail スタイルの方法で タプルを操作することはもちろん可能ですが、ループが使えるともっと便利です。 foreach文 では、型タプル と 式タプル 上をループすることができます。
alias Tuple!(int, long, float) TL; foreach (i, T; TL) writefln("TL[%d] = ", i, typeid(T)); alias Tuple!(3, 7L, 6.8) ET; foreach (i, E; ET) writefln("ET[%d] = ", i, E);
表示は:
TL[0] = int TL[1] = long TL[2] = float ET[0] = 3 ET[1] = 7 ET[2] = 6.8
タプルの宣言
型タプル で宣言された変数は 式タプル になります:
alias Tuple!(int, long) TL; void foo(TL tl) { writefln(tl, tl[1]); } foo(1, 6L); // 表示は 166
全部組み合わせると...
これらの機能を組み合わせると、 関数の引数を全てカプセル化して、 その引数で元の関数を呼び出すようなdelegateを返す CurryAll テンプレートが実装できます。
import std.stdio; R delegate() CurryAll(Dummy=void, R, U...)(R function(U) dg, U args) { struct Foo { typeof(dg) dg_m; U args_m; R bar() { return dg_m(args_m); } } Foo* f = new Foo; f.dg_m = dg; foreach (i, arg; args) f.args_m[i] = arg; return &f.bar; } R delegate() CurryAll(R, U...)(R delegate(U) dg, U args) { struct Foo { typeof(dg) dg_m; U args_m; R bar() { return dg_m(args_m); } } Foo* f = new Foo; f.dg_m = dg; foreach (i, arg; args) f.args_m[i] = arg; return &f.bar; } void main() { static int plus(int x, int y, int z) { return x + y + z; } auto plus_two = CurryAll(&plus, 2, 3, 4); writefln("%d", plus_two()); assert(plus_two() == 9); int minus(int x, int y, int z) { return x + y + z; } auto minus_two = CurryAll(&minus, 7, 8, 9); writefln("%d", minus_two()); assert(minus_two() == 24); }
Dummy 引数が必要なのは、 同じ引数リストで二つのテンプレートをオーバーロードすることはできないためです。 一つダミー引数を増やして、区別をつけています。
今後の方向
- 関数からタプルを返せるように
- タプルに対する演算子 =, += など
- タプルのプロパティ .init など (プロパティを各要素に適用する処理)