KOTET'S
PERSONAL
BLOG

#dlang AliasSeqで引数をこねくりまわす

Created:
#dlang #tech

これは 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
}

run.dlang.io/is/KtUUcG

引数として渡す

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
}

run.dlang.io/is/j8HXPC

-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;
}
// 後略

数値のかわりに型を入れると、型タプルが作れます。 この場合45は実行時に与えられる値でも大丈夫。

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
}

run.dlang.io/is/pwhmeQ

実行時には普通の変数のようになります。

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
}

run.dlang.io/is/f43IWj

普通の関数の引数列を完全にカプセル化して、 タプルとして汎用的に扱うことができるようになるわけです。

引数として宣言する

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
}

run.dlang.io/is/WyL3Wf

これもコンパイル時に普通の引数列に展開されます。

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

aliasSeqOfInputRange から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
}

run.dlang.io/is/OC9DGT

まあこの例だと全く嬉しくないですが……。

使用例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);
    }
}

run.dlang.io/is/1cLv29

上に挙げたコード中の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言語の標準ライブラリにはこういう便利なものがいっぱいあるので、 なにかやりたくなったときにはまずドキュメントを探してみるといいでしょう。