D言語で分割代入がしたかった

TL;DR

  • テンプレートよくわからない。助けて。

本編

1行で入力を取りたい

競プロでしばしばこんな文面を見ますよね:

入力は以下の形式で与えられる。 X Y Z

X, Y, Zが整数だったとして、これを例えばpythonで受け取りたければ1行でこう書けます:

X, Y, Z = map(int, input().split())

ところがD言語には分割代入の仕組みがないので、通常こう書くしかありません (追記: readfという便利な関数があるのを教えていただきました。これで十分でしたね……。) :

int X, Y, Z;
auto line = readln.chomp.split.to!(int[]);
X = line[0];
Y = line[1];
Z = line[2];

長いのはもちろんのこと、今後一生使わない変数lineがスコープを汚すのも気に食わない感じがします。 そんなわけで、もう少しすっきりさせたいね、と思ったので簡単なmixinを書きました。

import std.algorithm;
import std.conv;
import std.range;
import std.stdio;
import std.string;

// 「read words as T into D」 のつもり
string readAsInto(T, string D)() {
  return
    T.stringof ~ " " ~ D ~ ";"
  ~ "foreach(tup; zip("
    ~ "[" ~ (D.split(",").map!(s => "&" ~ s.strip).join(",")) ~ "],"
    ~ "readln.chomp.split.to!(" ~ T.stringof ~ "[])"
  ~ ")) *tup[0] = tup[1];";
}

これを使うと先の入力はこのように受け取れます:

mixin(readAsInto!(int, "X, Y, Z"));

短くてめでたい。

分割代入したかった

ところで、さっきのmixinは標準入力を受け取る用途に特化しすぎています。 より一般的に分割代入ができるようにmixinを書き直してみたのですがあまりうまくいかず。 多分テンプレート分かってない。 以下は迷走の記録です。

mixin()に放り込む文字列はコンパイル時に確定していて、かつ合法なソースコードでなければなりません。したがって、文字列の生成は関数ではなくテンプレートによって行う必要があります。

ここで必要になるテンプレートは「変数名の列と値の列を受け取って、各変数を宣言し、各値を代入する」ようなソースコードの文字列を生成するものですので、シグネチャは(気持ち的には)多分こんな感じです:

string decomposeInto(InputRange!E r, string D)() {
    // ...
}

当然ですが型変数Eが未定義なので上のコードは非合法です。どうしよう。

1. alias を使う

そういえば library reference でテンプレート引数をaliasにしているのを見かけたことがあります。Language reference 読んでも正直意味がよく分かりませんでしたが、まあ手を動かせば何か分かるかも知れんしと思い、ものは試しで以下のように書きました:

string decomposeInto(alias R, string D)() {
  return 
    (ElementType!(typeof(R))).stringof ~ " " ~ D ~ ";"
  ~ "foreach(tup; zip("
    ~ "[" ~ (D.split(",").map!(s => "&" ~ s.strip).join(",")) ~ "],"
    ~ R.stringof
  ~ ")) *tup[0] = tup[1];";
}

これを次のようにして使います:

void main() {
  auto a = readln.chomp.split.to!(int[]);
  mixin(decomposeInto!(a, "X, Y, Z"));
  X.writeln;
}
> rdmd test.d
1 2 3
1

動くじゃん。

しかし!main()を以下のように書き直すと……

void main() {
  // 変数 a を省略しただけ
  mixin(decomposeInto!(readln.chomp.split.to!(int[]), "X, Y, Z"));
  X.writeln;
}
> rdmd test.d
test.d(9): Error: identifier 'chomp' of 'readln.chomp.split.to!(int[])' is not defined
test.d(10): Error: undefined identifier 'X'

という謎エラーで怒られます。なんで……?

2. 型変数 E を定義する

やっぱりよく分かっていないものを使うとうまくいきません。分かる範囲でどうにかしましょう。

要はEが定義できればよかろうということで次のように書きました:

template decomposeInto(E) {
  string decomposeInto(InputRange!E R, string D)() {
    // ...
  }
}

冗長ですが仕方がないので以下のようにして呼びます:

auto a = readln.chomp.split.to!(int[]);
mixin(decomposeInto!int.decomposeInto!(a, "X, Y, Z"));

すると:

> rdmd test.d
test.d(11): Error: cannot resolve type for decomposeInto!int
test.d(12): Error: undefined identifier 'X'

詳しいことを何も教えてくれないこの謎のエラーは、テンプレート名と関数名を別にすることでなぜか解消します*1。しかし今度は

>rdmd test.d
test.d(11): Error: variable a cannot be read at compile time
test.d(11):        while looking for match for hoge!(a, "X, Y, Z")
test.d(12): Error: undefined identifier 'X'

と怒られます。いや確かに考えてみればそれはそう。でもじゃあなんでaliasなテンプレート引数は実行時に渡しても怒られないんだ……?というかaliasって何?

D言語くんと対話できない

別にどうしても分割代入を書きたくて苦しんでいるわけではなく、吐かれたエラーを理解できないところで苦しんでいます。D言語くんと真に仲良くなれる日は訪れるのでしょうか。

おわりに

こんな本質から外れたことばっかやってるから水色になれないんだぞ。

*1:本当になんで?