これは D言語 Advent Calendar 2018 17日目の記事です。
D言語にはAliasSeq
という超絶キモいおもしろいものがあります。
これを使うと引数列をまるで1つのタプルのように取り扱うことができます。
あまり何度も使うものではないですが、知っているとちょっと便利だったので、
ここではそんなAliasSeq
について書きます。
AliasSeq
AliasSeq
について、
ドキュメントにはこう書かれています。
Creates a sequence of zero or more aliases. This is most commonly used as template parameters or arguments. In previous versions of Phobos, this was known as TypeTuple.
ゼロ以上のエイリアスのシーケンスを作ります。これは主にテンプレートパラメータや引数に使われます。 以前のバージョンのPhobosでは、これはTypeTupleとして知られていました。
AliasSeq
はこんな感じのシンプルなテンプレートです。
可変長テンプレート引数として現れるタプルを外の世界に引きずり出しているんですね。
template AliasSeq(TList...)
{
alias AliasSeq = TList;
}
phobos/meta.d at fc96b0a99d5869ef10f503490c1f41be36d276e5 · dlang/phobos
これを使うことでタプルの形でエイリアスをたくさん作って、 コンパイル時にスライスを取ったりインデックスでアクセスしたりできます。
alias Numbers = AliasSeq!(1, 2, 3, 4);
static assert (Numbers[1] == 2);
alias SubNumbers = Numbers[1 .. $];
static assert (SubNumbers[0] == 2);
エイリアスなので、変数を入れると普通の代入にはならないので注意。
import std.stdio;
import std.meta;
void main()
{
long x;
alias A = AliasSeq!("hello", "world", x);
A[2] = 42;
writeln(x); // 42
}
引数として渡す
AliasSeq
はふつうの関数に渡すことができます。
import std.stdio;
import std.meta;
long mul(long a, long b)
{
return a * b;
}
void main()
{
alias A = AliasSeq!(4, 5);
writeln(mul(A)); // 20
}
-vcg-ast
オプション
を付けてコンパイルして出力を見てみると、
上のコードは以下のように展開されています。
import object;
import std.stdio;
import std.meta;
long mul(long a, long b)
{
return a * b;
}
void main()
{
alias A = TList;
writeln(mul(4L, 5L));
return 0;
}
// 後略
数値のかわりに型を入れると、型タプルが作れます。
この場合4
と5
は実行時に与えられる値でも大丈夫。
import std.stdio;
import std.meta;
long mul(long a, long b)
{
return a * b;
}
void main()
{
alias A = AliasSeq!(long, long);
A param;
param[0] = 4;
param[1] = 5;
writeln(mul(param)); // 20
}
実行時には普通の変数のようになります。
import object;
import std.stdio;
import std.meta;
long mul(long a, long b)
{
return a * b;
}
void main()
{
alias A = (long, long);
(long, long) param;
__param_field_0 = 4L;
__param_field_1 = 5L;
writeln(mul(__param_field_0, __param_field_1));
return 0;
}
// 後略
もちろん可変長引数の関数でもOK。
import std.stdio;
import std.meta;
void main()
{
alias A = AliasSeq!("hello", ' ', "world");
writeln(A); // hello world
}
普通の関数の引数列を完全にカプセル化して、 タプルとして汎用的に扱うことができるようになるわけです。
引数として宣言する
AliasSeq
は引数の型として使うこともできます。
import std.stdio;
import std.meta;
long mul(AliasSeq!(long, long) args)
{
return args[0] * args[1];
}
void main()
{
writeln(mul(4, 5)); // 20
}
これもコンパイル時に普通の引数列に展開されます。
import object;
import std.stdio;
import std.meta;
long mul(long _param_0, long _param_1)
{
return _param_0 * _param_1;
}
void main()
{
writeln(mul(4L, 5L));
return 0;
}
aliasSeqOf
aliasSeqOf
は
InputRange
からAliasSeq
を生成するテンプレートです。
たとえばこんなことができます。
import std.stdio;
import std.meta;
long mul(long a, long b)
{
return a * b;
}
void main()
{
enum ary = [4, 5];
alias A = aliasSeqOf!ary;
writeln(mul(A)); // 20
}
まあこの例だと全く嬉しくないですが……。
使用例1:展開されるループ
ここから具体的な使い方を紹介します。
import std.stdio;
import std.meta;
void main()
{
alias A = AliasSeq!(1, 2, 3);
foreach (x; A)
{
writeln(x);
}
long[] ary = [1, 2, 3];
foreach(x; ary)
{
writeln(x);
}
}
上に挙げたコード中の2つのforeach
は同じものを出力します。
つまり、1、2、3と順番にwriteln
が実行されます。
しかし内部的な動作は異なってきます。
import object;
import std.stdio;
import std.meta;
void main()
{
alias A = TList;
/*unrolled*/ {
{
enum int x = 1;
writeln(1);
}
{
enum int x = 2;
writeln(2);
}
{
enum int x = 3;
writeln(3);
}
}
long[] ary = [1L, 2L, 3L];
{
long[] __r43 = ary[];
ulong __key44 = 0LU;
for (; __key44 < __r43.length; __key44 += 1LU)
{
long x = __r43[__key44];
writeln(x);
}
}
return 0;
}
// 後略
AliasSeq
を渡したほうのforeach
は3つのwriteln
になっているのがわかります。
AliasSeq
に関しては特別な言語組み込み機能があり、foreach
をコンパイル時に展開してくれるのです。
あまり使いどころが思いつきませんが、型などの普通foreach
では扱えない要素までイテレートできます。
闇の魔術に応用できそうな気もする。
alias A = AliasSeq!(byte, int, long);
foreach (type; A)
{
pragma(msg, type);
}
現在はstatic foreach
が存在するため、基本はそちらを使ったほうが読みやすいし適切でしょう。
static foreach
で型などは扱えなかったはずなので、そういうときにはAliasSeq
が役に立つはずです。
enum ary = [1, 2, 3];
static foreach (x; ary)
{
writeln(x);
}
上のコードはこんな感じに展開されます。
enum int[] ary = [1, 2, 3];
writeln(1);
writeln(2);
writeln(3);
使用例2:stringをcharの引数列に
こっちは実際に使ったものです。 自作Cコンパイラには以下のようなコードがあります。
if (s[i].among!(aliasSeqOf!"+-*/;=(),{}<>[]&"))
{
/* ... */
}
順を追って説明していきましょう。 上のコードをできるだけベーシックなものに書き直すとこんな感じになります。
if (s[i] == '+' || s[i] == '-' || s[i] == '*' || /* 中略 */ || s[i] == '&')
{
/* ... */
}
比較を何度も書かなくてはいけないので非常に煩わしいですね。
そこでamong
という関数を使うと以下のようになります。
内部的にはswitch
文を使った効率的な関数がコンパイル時に生成されます。
ここの詳細は過去の記事に書いてあるので、
気になる人は読んでください。
if (s[i].among!('+', '-', '*', /* 中略 */, '&'))
{
/* ... */
}
冗長な比較や論理演算がなくなってかなり短く書けるようになりました。 しかし今度は大量の文字をひとつづつクオートで囲むのがめんどくさくなってきます。 それにまだちょっと読みにくい。 こんな感じに書きたくなります。
if (s[i].among!("+-*/;=(),{}<>[]&"))
{
/* ... */
}
そんな時AliasSeq
が役に立ちます。
aliasSeqOf
で文字列、つまりchar
の配列をAliasSeq
にすると、やりたかったことがだいたい実現できます。
そうしてできたのが最初のコードです。
if (s[i].among!(aliasSeqOf!"+-*/;=(),{}<>[]&"))
{
/* ... */
}
この間すべてコンパイル時の出来事である
AliasSeq
テンプレートを頭の片隅に入れておくと、ちょっとコードの冗長性を下げたりできます。
しかもここまでの出来事はすべてコンパイル時に処理されており、実行時に余計なコストはかかりません。
D言語の標準ライブラリにはこういう便利なものがいっぱいあるので、 なにかやりたくなったときにはまずドキュメントを探してみるといいでしょう。