駆け足で進めるjavascriptのポリモーフィズム たぶん最後にして補足



javascriptではC++やそれの影響を受けている言語の考え方は一切通用しません。
ひとつ例を挙げるにしてもjavaにあるようなインターフェースという仕組みも不要です
あれはnominal typeが型の構造を気にしないため必要なものです。

初めに例を出したように

//使いまわせる新たな型を作るため無名の新たな型に名前をつける
//使いまわさないなら
//function binary_expr(op : (function(NUMERIC, NUMERIC):NUMERIC)!):NUMERIC op.call()
//と無名のままでいい
//javascript2.0ではnominal typeの型システムも融合されている
type BOP = function(NUMERIC, NUMERIC):NUMERIC
//実装の詳細を呼び出し側が気にする必要はない。よってfunc.call()と呼び出せる。
//関数オブジェクトは演算子()で呼び出せるのでfunc()でもよい
function binary_expr(op : BOP!):NUMERIC op.call()

とし、あとは文書化すればいいだけです。呼び出し側は

var instance = new Foo;
function FuncOne(NUMERIC, NUMERIC):NUMERIC {/*do something*/ }
//Fooオブジェクトのメンバ関数を渡す
binary_expr( instance.FooinMemberFunc );
//関数がどこに属しているかなどjavascriptのような言語には関係ない
//関数はオブジェクトなのでレシーバに属している必要もとくにない
//よってこの場合レシーバを特別扱いすることはない
//名前は関係なく型さえ一致すればいい
binary_expr( FuncOne );
//レシーバを特別扱いする必要はないのでこう呼んでもいい
//Foo.prototype.FooinMemberFuncの実装はinstanceに依存するので正しく動作する
binary_expr( Foo.prototype.FooinMemberFunc.bind(instance) );

必要なものを強制するアプローチが違うのです。必要なのはシグネチャ(引数の型の集合)の一致で、実装は気にしないという所はjavaのインターフェースと同じですね。
javascriptの言語仕様、仕組みを考えるときにはこのようにC++系とは別物なのでやり方も違う事を念頭に置きましょう。
しかし、お察しの通り現行のjavascriptに型を束縛する仕組みはありません。暗黙の型変換によるタイプルーズな型システムしか持ち合わせていないためですね。
ですが、タイプルーズにも多相的な利点があります。例えば以下のコード。

const f1="f1"

function callF1(r){ if(f1 in r) r[f1](); }
function Obj(){}
Obj.prototype={ f1 : function(){ print("f1 in Obj") } };

var o1=new Obj
var o2={};
o2[f1] = function(){ print("f1 in o2") }

callF1(o1)//-> "f1 in Obj"
callF1(o2)//-> "f1 in o2"

o1 instanceof Obj//-> true
o2 instanceof Obj//-> false
o2 instanceof Object//-> true

Obj instanceof Object//->true

タイプルーズなのでcallF1の引数rに型の指定はありません。ですが、それとは関係なしにrが持つ関数f1の実行には成功します。
最後の3行に注目するとrの実引数の型が何であるかは関係ないということを証明していますね。
そこで、const f1が何なのか考えましょう。

o2[f1] = function(){ print("f1 in o2") }
if(f1 in r) r[f1]();

この2つを見る限りconst f1は関数名だということが推測できます。答えは正確にはプロパティ名の文字列です。
javascriptのオブジェクトはプロパティを持ちその内容はなんでも入ります。
よって関数を入れることもできます。いわゆる「呼び出せるプロパティ」です。
なのでインスタンスo2はf1というプロパティに関数を持っています。

さらに、o1はObjのインスタンスなのでObj.prototype.f1を持つことが分かります。

Obj.prototype={ f1 : function(){ print("f1 in Obj") } };はObj.prototype["f1"] = function(){ print("f1 in Obj") };
つまりObj.prototype[f1] = function(){ print("f1 in Obj") };と同義です。

よってcallF1のr[f1]();というコードでo1、o2のプロパティf1にある関数の呼び出しに成功します。
つまり関数名は分かっているのでシグネチャが一致すれば呼び出せる、と……こういうことです。
このときもちろんタイプルーズなので引数rの型を指定していません。
する必要はありません。なぜなら、今まで見てきたようにオブジェクトの構造が一致しているからですね。
しかし、型を束縛した場合それに一致する必要があります。
そうした場合、名前に一致するのか構造に一致するのか知らなければいけません。しかし、そんな詳細は知り得ません。
なのでjavascript2.0では事前に定義していましたね。そんなときはjavaのインターフェースのようなものが有効かもしれませんね。

逆に言えばタイプルーズではそのようなことにとらわれなくて良いということです。
ブラウザに実装された言語ならば必ずしもプログラマがコーディングするとは限らないのでこれは十分利点と呼べるのではないでしょうか。むしろ、これがjavascript流のポリモーフィズムです。

さて、これらを踏まえて最後の3行に話を戻します。instanceof演算子が何をやっているか今一度考えてみましょう。
instanceof演算子のシンタックスは[lhs] instaceof [rhs]となります。
実はinstanceof演算子はrhsが関数ならその"prototype"という名前のプロパティにあるインスタンスから、関数以外なら直接そのインスタンスとrhsの全てのプロトタイプオブジェクトを比べています。
「全ての」とはプロトタイプオブジェクトもオブジェクトなのでさらにプロトタイプオブジェクトを持っているためです。これがプロトタイプチェーンです。
つまり、javascriptのオブジェクトは階層構造を持ちます。クラスベースではクラスが階層構造を持ていますね。そのときクラスは型を表現していました。

しかし、javascriptはクラスベースではなくプロトタイプベースなのでクラスはなくプロトタイプオブジェクトがあります。
プロトタイプオブジェクトもオブジェクトであるということはjavascriptにおける型とはオブジェクトである事を指しています。
これはjavascriptがnominal typeではなくstructural typeであることにも一致しています。

別の言い方をすると、クラスベースでは型はクラスによって表現されるクラスの一種、プロトタイプベースではプロトタイプによって表現されるプロトタイプの一種と言えます。
クラスベースでは新たな型を作るときには新たなクラスを定義します。それはプロトタイプベースでも同じ事で新たな型を作るには新たなプロトタイプを定義すればいいのです。ただし、プロトタイプベースでは継承ではなく移譲を使います。
つまり、クラスとプロトタイプではやっていることは本質的には同じです。
なので例えクラス宣言を言語仕様に追加したとしても今までの文法のシンタックスシュガーでしかありません。
harmonyやstrawmanで導入しようとしているクラス宣言がシンタックスシュガーでしかないのは当然のことです。

しかし、かと言ってmozillaのjavascript以外ではプロトタイプチェーンの変更はユーザーレベルでは許されていません。プロトタイプチェーンが変更できなければプロトタイプを移譲できませんsubtypingできないのです。
全てのオブジェクトはObjectオブジェクトしかプロトタイプに持つことができません。要するに(Arrayオブジェクトを[[prototype]]に持つような)派生の派生は作れません。

mozilla以外にはもちろんecma-262も含まれます。一時期、<|演算子(triangle operator)を策定していましたが削除されました。

これはプロトタイプチェーンの変更を実装していない実装からすれば深い部分の仕様変更であり、またsubtypingするにはプロトタイプチェーンをただ差し替えるだけではなく関連する処理をも変更する大規模な変更へとなります。
大規模な変更は保守派にとっては容認できないようなので仕様策定の議論すら纏まる気配がありません。
保守派が離反して作ったTC-39ではclass宣言はただの構文等であり<|演算子が仕様に追加されることは今のところはないでしょう。
また、mozillaのjavascriptでもプロトタイプチェーンを差し替えれるだけなので差し替えただけでは正しくsubtyping出来ません。


//オマケのinstanceof演算子動作
function hasInstance(lhs, rhs) {
let proto = lhs.__proto__;
while (proto) {
if (proto === rhs) return true;
proto = proto.__proto__;
}
return false;
}

駆け足で進めるjavascriptのポリモーフィズム その3

駆け足で見てきたjavascriptのポリモーフィズムですが、ではなぜcallやapply(さらにbind)の様な関数があるかというとタイプルーズでなくなったときその理由がよく分かります。

というわけで変数に型を束縛できるjavascript2.0でcallとbindを使った典型的なコードを書いてみます。

//数字を表す型すべてを表す新たな型
//Number以外はtamarin独自の型なので気にしなくていい
type NUMERIC = (byte, int, uint, double, decimal, Number!);
//二項演算子を表す新たな関数型
//typeキーワードはC++のtypedef typename foo barと同じ意味
type BOP = function(NUMERIC, NUMERIC):NUMERIC
//型名に!を付けるとnullを許さない
function binary_expr(op : BOP!):NUMERIC op.call()

function add(lhs, rhs):NUMERIC
binary_expr( (function(x, y):int x + y).bind(this, lhs, rhs) )

function sub(lhs, rhs):NUMERIC
binary_expr( (function(x, y):int x - y).bind(this, lhs, rhs) )

function multi(lhs, rhs):NUMERIC
binary_expr( (function(x, y):int x * y).bind(this, lhs, rhs) )

function div(lhs, rhs):NUMERIC
binary_expr( (function(x, y):int x / y).bind(this, lhs, rhs) )

add(10, 20)// -> 30
sub(10, 30)// -> -20
multi(10, 40)// -> 400
div(10, 50)// -> 0.2

functionBodyの{}とreturnを省略してますが式クロージャというものです。javascript1.8にもあります。
ちなみに2.0にbindはありませんがこのコードでは用意されていると思い込んで下さい。

そしてこのjavascript2.0コードは特に束縛する必要のない型を束縛して書いただけなのでこれと同じ物をjavascript1.8でも書けます。

function binary_expr(op) op.call()

function add(lhs, rhs)
binary_expr( (function(x, y) x + y).bind(this, lhs, rhs) )

function sub(lhs, rhs)
binary_expr( (function(x, y) x - y).bind(this, lhs, rhs) )

function multi(lhs, rhs)
binary_expr( (function(x, y) x * y).bind(this, lhs, rhs) )

function div(lhs, rhs)
binary_expr( (function(x, y) x / y).bind(this, lhs, rhs) )

見ての通り型の指定がありません。

callやapplyのシグネチャは(thisObj, arg1...)または(thisObj, argArray)です。
thisObjはレシーバでそれ以降は関数への引数です。
C++やjavaなどではレシーバーは特別扱いされているので自由に変更できません。
しかし、レシーバも所詮は引数のひとつなので特別扱いする必要はありません。
レシーバは一番初めの引数なので第一引数と呼ばれる(java bytecodeでいうaload_0)のですが、
特別扱いする必要がないのならばレシーバは名前に縛られた型を気にする必要もありません。

それがマルチメソッドというものです。

マルチメソッドは言い換えるとレシーバも他の引数と同様に扱ってオーバロードしますが、しかし、
javascript1.xは型を束縛できないのでマングリングすることができずマルチメソッドと同じ方法ではこの様な扱いは出来ません。
ですがjavascriptは関数もオブジェクトなのでメソッドを持ち、その引数にレシーバを指定すればいいのです。

第一引数の型を気にせずシグネチャ、つまりインターフェースが一致していれば多相的に処理できる型を多相型と言いました。
javascriptはタイプルーズなのでシステムに型が隠蔽され型が無いかの様に振る舞い、
結果このような多相的な特徴も持ち合わせていないかの様に見えがちですが実際には既に示したように確かに存在しています。
このようにjavascriptは多相型を自然に多用しています。
出てきた当時からこの様な仕様なのでBrendanは始めから設計思想はこういう思惑だったのでしょう。

これもまた型に名前をつけて型名で識別するnominal typeと型名は重要視せず型の構造で識別するstructural typeの考え方の違いでもあります。
しかし、javascript2.0は型を束縛するために型の名前をわざわざ特別視することができます。

ですがやはり、元々structuralなので

type NUMERIC = (byte, int, uint, double, decimal, Number!);

のように既存の型の意味集合、つまり型の構造をもとに新たな型名を付けることができます。

これらのことからすなわち、javascriptのようなstructural typeな言語の型のインターフェースとは型のメンバの名前ではなく、
型の意味集合から成る構造を指しています。それと、型推論を組合われることによりとくに意識せず簡単で自然な多相型を扱えるのです。
しかし、javascriptに型推論はありません。あるのは暗黙の型変換によるタイプルーズな型システムです。
javascriptが出てきた頃はタイプルーズはあまり理解されておらずバリアントと同じものとよく誤解されていましたが今では別物だということが知られていることでしょう。

ではなぜタイプルーズなのか?

javascriptはプロトタイプベースのオブジェクト指向な動的言語です。
プロトタイプはsmalltalkの弱点であるクラス定義の動的な追加・変更への耐性としてSelfという言語の特徴ですが、
smalltalkの流れをくむ言語は学術・研究畑から生まれた言語なので様々な先進的な仕組みを持ちます。
その中に型推論があります。
変数はバリアントではなくブラウザからエンドユーザーが利用するjavascriptは(動的言語でもあり)型を気にしない仕組みが必要でした。
しかし、javascriptが作られた90年代初頭にはPCに型推論を実現するマシンスペックなどはありません。
オブジェクト指向でありながらプリミティブ型を持っているのも同じ理由です。

最近、javascriptの実装が盛んなのは一番身近で一番使われている学術・研究畑出身の言語として研究余地とそれをする理由があるからです。
むしろ、誕生当時はマシンスペックが絶対的に不足していたので研究する余裕などなく、それどころかあの時代に複雑でモダンな言語の動く実装が存在していたこと自体がオーパーツと言ってもよいでしょう。
苦しくも先に挙げた妥協点は必要だったようですが。

nominal typeとstructural typeの型システムの違いが取り上げられるようになったのも最近のことです。
型システムは常にひとつでstructural typeのような型システムがあるとは微塵に思いもしなかった人たちも沢山いるはずです。

javascriptが出てきて20年近く経ちますが型システムはおろか未だにクラスがないなどと言われますが、まずはクラスもオブジェクトの一つだという事とオブジェクトの種類を表すのが型だということ、それと動的言語にクラスを持たせるとsmalltalkの問題に逆戻りしてしまうこと、classオブジェクトが存在するのならばmetaclassオブジェクトが必要だということ、これがややこしい問題だという事、クラスの継承はプロトタイプの移譲に、インスタンス化する際のテンプレート処理はクラスがなくても出来るという事を理解することが先です。

(宣言的、静的に束縛された)明示的な型がなければポリモーフィズムも意識できない(特にする必要がない)ため自分たちがよく使っているクラスという仕組みを安易に求めているに過ぎません。

structural typeでプロトタイプベースの動的オブジェクト指向言語であるjavascriptはSimula系のC++のようなnominal typeでクラスベースの静的オブジェクト指向言語とは全く別の誕生経緯、型システム、言語機能、理論から成るものです。

関数型のパラダイムを持ち合わせていることばかり注目されがちですがこの様な特徴にも目を向けることがjavascriptの理解を助けることに繋がるでしょう。
このブログ内のnominalという単語が全部normalになっていたので不思議に思い調べたらなんと、単語登録時点で間違っていた! というわけで修正しておいた。