テンプレートと型の制約

 関数の基本的なスタイルはシンプルだ。関数には名前があり、引数があり、そして戻り値がある。それだけだ。

 しかし、人は時に強欲で、これよりもさらに高級なことがしたいと思うことがある。その最たるものがテンプレートである。テンプレート自体は今や極めて普及している概念なので、ここではさらっとD言語風の書き方を示すに留めておく。以下に配列arrのi番目の要素を変数aの中身に置き換える関数を示す。

import std.stdio;

T replace(T)(T[] arr, int i, T a){
  auto tmp = arr[i];
  arr[i] = a;
  return tmp;
}

unittest{
  double[] arr = [0., 1.1, 2.2, 3.3];
  writeln(arr[]);
  // arrの1番目の値を10.に置き換える
  replace(arr, 1, 10.);
  writeln(arr[]);
}

 これを実行すると以下のような結果を得る。

[0, 1.1, 2.2, 3.3]
[0, 10, 2.2, 3.3]

 さて、一見これでうまく行くように見える。しかし、実はこれだと関数の引数に汎用性が足りない。例えばrelaceを以下のようにして使うとコンパイルエラーになる。

// 3つ目の引数にint型の値を指定
replace(arr, 1, 10);

 Dはとにかく型にうるさい。ここではどちらもT型としているarrと10の型が違うと怒られてしまうのである。これを回避するのは簡単で、以下のように2種類のテンプレート引数を用意すればよい。

// より汎用的なシグネチャ
T replace(T, E)(T[] arr, int i, E a)

 ここでは第一引数の配列と第三引数の変数の型が違う汎用型として定義されているため、先程のような問題は起こらない。

 しかし今度は別の問題が発生する。これでは少々汎用的過ぎるのだ。この関数のシグネチャだけを見ると、例えば以下のような引数も取れそうな気がしてくる。

// 第三引数がstring型???
replace(arr, 1, "Hello");

 もちろんこんなモノがまともに動作するわけがないのは分かりきっている。しかし、問題はコンパイルエラーが発生する場所だ。現状では、aをarr[i]に代入するところでエラーとなるが、理想的にはこんなナンセンスな値は引数として受け取る前に門前払いしたいところである。

 ここからが本日のメインのお話だ。D言語にはテンプレート引数の型の間に、ある種の制約を設けることができる。具体的には、例えば2つの異なる汎用型を持つ変数が、互いに比較可能かどうかを確かめることができる。以下により洗練されたreplaceのシグネチャを示す。

// T, Eが互いに比較可能かどうかを調べる
T replace(T, E)(T[] arr, int i, E a)
  if(is(typeof(arr[0] != a) == bool))

 二行目のif文まで含めて1つのシグネチャであることに注意してほしい。ここではtypeof構文によってarr[0] != aという式の型を取得し、それがboolであるかどうかをis構文によって確かめている。もしarr[0]とaが比較可能でない場合、型がないということになり、これはboolではない。ifの中身がtrueであるときに限りコンパイルが成功し、そうでなければこの段階でコンパイルエラーになる。つまり、制約が満たされていないときは関数本体の処理には入れないのだ。

 is構文の使い方が少々複雑なため、他にどのような形の制約を加えられるのかがよくわからないという難点はあるが、関数をさらなる高みへ押し上げるこの機能を使う機会は多いだろう。

参考:プログラミング言語D

プログラミング言語D

プログラミング言語D