このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

TypeScriptジェネリックス:可能性が見えると不満がつのる

例えば、配列 [0, 2, 4, 6, 8] に対して、0 + 2 + 4 + 6 + 8 を計算するようなJavaScript関数:

function total(a) {
  var tot = 0;
  for (var i = 0; i < a.length; i++) {
    tot += a[i];
  }
  return tot;
}


> total([0, 2, 4, 6, 8])
20

次のようなTypeScriptクラスを考えます。

// 自然数型をうまく定義できないので、ナンチャッテ定義
// naturalはintegerで0以上
type natural = number;

class Evens {
  length:natural;
  constructor(length:natural) {
    this.length = length;
  }
  item(index:natural) : number {
    return (index >= this.length)? undefined : index * 2;
  }
}

var evens5 = new Evens(5);

Evensのitemを、もし配列と同じブラケット([])で書けたら、関数totalはそのまま使えて total(evnes5) が計算できるはずなんですが、実際はダメです。[]や()をオーバーロードできないですから。

しょうがないので、totalを書き換えます。a[i] を a.item(i) に直します。

function total(a) {
  var tot = 0;
  for (var i = 0; i < a.length; i++) {
    tot += a.item(i);
  }
  return tot;
}

すると、


> total(evens5)
20

できた。しかし、


> total([0, 2, 4, 6, 8])
TypeError: a.item is not a function

ムー、しょうがないから、組み込み配列型のほうにitemメソッドを持たせます。

interface Array<T> {
  item(index:natural) : any;
}
Array.prototype.item = function(index:natural) {return this[index];};


> total(evens5)
20
> total([0, 2, 4, 6, 8])
20

ともかくもこれで、組み込み配列でもユーザー定義クラスでも使えるtotalになったので、totalに型を与えます。

// lengthとitemを持つ型
interface Sequence<X> {
  length:natural;
  item(index:natural) : X;
}

function total(a:Sequence<number>) : number {
  var tot:number = 0;
  for (var i = 0; i < a.length; i++) {
    tot += a.item(i);
  }
  return tot;
}

次に、totalのなかで具体的に指定されてるnumberを総称化したいですね。

function total<X>(a:Sequence<X>) : X {
  var tot:X = 0;
  for (var i = 0; i < a.length; i++) {
    tot += a.item(i);
  }
  return tot;
}

そうすると、TypeScriptコンパイラすごくまっとうなエラーメッセージを出します(いつでも「まっとう」なわけじゃないけど)。

  1. error TS2322: Type 'number' is not assignable to type 'X'.
  2. error TS2365: Operator '+=' cannot be applied to types 'X' and 'X'.

そうですよね。定数0が任意の型Xにあるわけじゃない、足し算が任意の型Xにあるわけじゃない、だからtotalが定義できない、まったくおっしゃるとおり

じゃ、どうしたらいいのでしょう。

  1. 型Xに定数0に相当する定数が必要である。その定数を宣言する。
  2. 型Xに足し算に相当する二項演算が必要である。その二項演算を宣言する。

その宣言を擬似コードで書けば:

// 足し算ができる型
interface Addable {
  // 定数・ゼロの宣言

  // 二項演算・足し算の宣言
}

二項演算の宣言も定義もできないので、itemのときと同様にメソッドで代用するしかないでしょう。

// 足し算ができる型
interface Addable {
  // 定数・ゼロの宣言
  ZERO : Addable;

  // 二項演算・足し算の宣言
  add(x:Addable) : Addable;
}

ん? なんかおかしいよね。ZEROは、インスタンスごとに持つプロパティ値じゃなくて、スタティックな存在。そのことをどう宣言したらいいのでしょうか? ワカラン! そもそも宣言出来ないのじゃないのかなぁ、出来たらゴメン。

仮に、「定数ZEROはスタティックに持つべし」という宣言が出来たとすると、インターフェースAddableを実装したクラスXに対して、定数 X.ZERO と、Xのインスタンスx, yに対する x.add(y) は意味を持つわけです。そうなると、totalは次のように書けるはず。

function total<X implements Addable>(a:Sequence<X>) : X {
  var tot:X = X.ZERO;
  for (var i = 0; i < a.length; i++) {
    tot = tot.add(a.item(i));
  }
  return tot;
}

どういう解釈になるかというと:

  • <X implements Addable>
    型Xは無条件ではなくて、インターフェースAddableを実装した型と仮定する。
  • var tot:X = X.ZERO;
    変数totの型はX、初期値はX.ZERO。X.ZEROの存在は、XがインターフェースAddableを実装していることから保証される。
  • tot.add(a.item(i));
    totの型はXであった。従ってインターフェースAddableを実装しており、addメソッドは使える。
  • return tot;
    戻り値の型はXとなる。これは関数totalで宣言した戻り値型と一致する。

<X implements Addable> は、キーワードを変えて <X extends Addable> でコンパイラは通るようですが、total関数の全体を意図したように書く方法が分かりません。

仮になんらかの手段があったとしても、addとかitemとかのメソッド名で書くのが相当にフラストレーション。一番最初に書いたtotalのように、普通の演算子で書きたいです。

function total<X implements Addable>(a:Sequence<X>) : X {
  var tot:X = 0; // 右辺の0は、X.ZERO と解釈
  for (var i = 0; i < a.length; i++) {
    tot += a[i];
  }
  return tot;
}

ワガママ言っているのは分かってますよ。でもね、ある程度のところまでは出来るので、その先に行けないと非常に不満に感じてしまうのです。なんとかして欲しいよ。