ジェネリクス


ジェネリクスとは

ジェネリクスは型を越えて汎用的に利用される操作に際して,共通操作を行う際に型情報を失わないための機能です。典型的にはコレクションが要素の型を指定したり, 2 つの値を比較する関数の型を指定したりするのに利用されます。

例えば「基本のデータ型」に出てきたリスト,配列,オプションはいずれも [int] のように,コンテナに格納される要素の型を指定することで,格納される型の情報を残しています。これにより,コンテナに本来格納すべき型でない値が混在してしまう事を防いだり,あるいは要素を取り出したときにその要素の型情報に依存する操作を行う事ができます。例えば次の例を見てみましょう。

def list : list[int] = $[1..5];
def (first :: second :: _) = list;
_ = first + second;
// _ = "foo" :: list;  // コンパイルエラー

このコードは整数リストの先頭と二番目の要素を取り出し, 3 行目でその和を計算しています。もしリストの要素が整数であるという情報が失われていたら, 3 行目で和を求めるという操作ができません。なぜなら + という操作はすべての型に認められた操作ではないからです。さらに 4 行目で文字列と整数リストを結合がコンパイルエラーになるのは,要素の型が一致しないことが検出されるからです。

ジェネリクスを使う

.NET Framework の System.Collections.Generic.List クラスを例に挙げます。 .NET Framework のリストは Nemerle のリストとは異なり,要素数が可変で追加・削除といった操作ができます。

using System.Collections.Generic;

def list = List.[int]();
list.Add(1);
list.Add(2);
// list.Add("foo");  // コンパイルエラー

型名の後ろに .[int] の形で型パラメーターを与えてやる事で,リストの要素が int であることを示しています。したがって文字列を追加しようとすると,型が一致しないのでコンパイルエラーになります。

ちなみに Nemerle の型推論を利用すれば,型パラメーターは次のように省略可能です。

using System.Collections.Generic;

def list = List();
list.Add(1);
list.Add(2);
// list.Add("foo");  // コンパイルエラー

あるいは型パラメーターにワイルドカード _ を渡すこともできます。

using System.Collections.Generic;

def list = List.[_]();
list.Add(1);
list.Add(2);
// list.Add("foo");  // コンパイルエラー

いずれのケースにおいても, list に対する最初の操作 Add の型は T -> voidTList クラスの型パラメーターです。ここで Tint なので, list の型が List.[int] であると推論されます。

もう 1 つ例を挙げます。様々な型の値が含まれたコレクションから,特定の型の要素のみ抽出するということを行います。

using System.Collections.Generic;
using Nemerle.Extensions;

def list = List.[object]() <- [1, 2, "foo", 3, "bar"];

ここで <- [...] はオブジェクト修飾と呼ばれるマクロで, [...] で指定された要素をコレクションに追加するものです。したがって list には 3 つの整数値と 2 つの文字列が含まれていることになります。ここから文字列の要素のみを取り出します。

シーケンスから特定の型の値のみを抽出するには LINQ の OfType が使えます。 OfType の型パラメーターに string を与えてやれば, string 型の要素のみ抽出する事ができます。

using System.Linq;
def stringList =  list.OfType.[string]();

このように, Nemerle では .[T] の形式で型パラメーターを指定します。

ちなみに OfType の型パラメーターを明示せず,型推論にまかせる書き方もあります。

using System.Linq;
def stringList : IEnumerable[string] =  list.OfType();

ジェネリックな関数や型を定義する

ジェネリックな関数の定義は,関数名の後ろに [T1, T2, ...] の形式で型パラメーターを宣言します。

def first[T1, T2](x : T1, _ : T2) { x; }
_ = first(1, "foo");  // 1
_ = first("bar", 2.0);  // "bar"

この例で first 関数をジェネリック関数として定義しない場合, first 関数は 2 行目で int * string -> int と型推論されるため, 3 行目が型の不一致でコンパイルエラーになります。

型の場合も同様に,クラスの後ろに [T1, T2, ...] の形式で型パラメーターを宣言します。

public interface IDistance[T]
{
   GetDistanceFrom(other : T) : double;
}

public abstract class Point[T] : IDistance[Point[T]]
{
   private x : T;
   private y : T;

   public this(x : T, y : T)
   {
      this.x = x;
      this.y = y;
   }
   
   public X : T { get { this.x; } }
   public Y : T { get { this.y; } }
   
   public abstract GetDistanceFrom(other : Point[T]) : double;
}

public class DoublePoint : Point[double]
{
   public this(x : double, y : double)
   {
      base(x, y);
   }
   
   public override GetDistanceFrom(other : Point[double]) : double
   {
      def dx = this.X - other.X;
      def dy = this.Y - other.Y;
      System.Math.Sqrt(dx * dx + dy * dy);
   }
}

この例では,クラス内で T1T2 がジェネリック型として使用できます。

型パラメーターの制約

where キーワードで型パラメーターに制約をつけることができます。パラメーター T に対する制約は where T : constraint のような形になります。複数の制約がある場合は複数の where 句を並べます。

def id[T](x : T)  // : T
   where T : struct  // T は値型
{
   x;
}
_ = id(1);
_ = id(-2.0);
// _ = id("foo");  // string は値型でないのでコンパイルエラー
public class WeakDictionary[TKey, TValue] : IDictionary[TKey, TValue]
   where TKey : System.IComparable[TKey]  // TKey は IComparable[TKey] を実装する
   where TValue : class  // TValue は参照型
{
   // ...
}

Nemerle で使える制約は以下の通りです。

制約 意味
TParameter : Type1, Type2, ..., TypeN TParameterType1Type2,…, TypeN のすべてを実装・継承する
TParameter : class TParameter は参照型
TParameter : struct TParameter は値型
TParameter : enum TParameter は列挙型
TParameter : new() TParameter はデフォルトコンストラクターを持つ