例えば、配列 [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コンパイラはすごくまっとうなエラーメッセージを出します(いつでも「まっとう」なわけじゃないけど)。
- error TS2322: Type 'number' is not assignable to type 'X'.
- error TS2365: Operator '+=' cannot be applied to types 'X' and 'X'.
そうですよね。定数0が任意の型Xにあるわけじゃない、足し算が任意の型Xにあるわけじゃない、だからtotalが定義できない、まったくおっしゃるとおり。
じゃ、どうしたらいいのでしょう。
- 型Xに定数0に相当する定数が必要である。その定数を宣言する。
- 型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; }
ワガママ言っているのは分かってますよ。でもね、ある程度のところまでは出来るので、その先に行けないと非常に不満に感じてしまうのです。なんとかして欲しいよ。