smalltalkやSELFからみたjavascript


はじめに重要なことを言っておきます。すべてはオブジェクトであるというのがSmalltalkおよびそれから派生した言語の考え方です。
しかし、Smalltalkはオブジェクトを作る方法にクラスというものを導入したためクラスもオブジェクトだとすると……

クラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスのクラスの……

といった風に──まるでMOTHER2かヤンデレです──オブジェクトに対するメタオブジェクトのそのまたメタオブジェクトといったように無限に再帰する羽目になってしまいます。ちなみに再帰的に定義されるのはSmalltalkの設計論の一つですがこれは困ります。ちなみにSmalltalkはクラス定義が循環しているのでここまで単純な無限再帰にはなりませんがプロトタイプベースが注目する問題点が解決されないのは変わりません。そして、クラスを導入したことのもう一つの弊害に、新たなオブジェクトを作るのに一つの新たなオブジェクトだけを作ることは出来ないのです。必ず新たなクラスオブジェクトが必要で新たに作られるオブジェクトはそのクラスオブジェクトの子オブジェクトとなります。これはis a関係として知られていますがクラスベースではこの関係から脱却することは出来ず、切っても切り離せません。しかし、新たなオブジェクトを作るのにクローンすればクラスは必要必要ないという考え方がプロトタイプベースの言語の考え方です。

新たなオブジェクトがクローンそのものか親から派生した子オブジェクトかは今は気にしないでください。jsでは方法によります。


実際の開発においてクラスベースで問題になるのがクラスベースは将来の変更に弱いという話ですが今回の話ではそこは重要ではありません。すべてはオブジェクトだという考え方、クラスというオブジェクトが存在するという方法論の問題点の解決策にプロトタイプベースの言語が生まれたということです。

さて、前置きの前置きは以上です。これらを踏まえて前置きに入ります。SELFではスコープやスタックフレームといったものもオブジェクトと移譲で実現しています。これはjsでも同じことでCallオブジェクトやArgumentsオブジェクトが担っています。Callは取得できないしArgumentsは書き込めない? よくご存知で──しかし、それにはnetscapeなりのちゃんとした理由があるのです。netscapeは自己反映計算(古い言い方だがリフレクションのこと)を嫌っていてjsのバージョンを上げる度にArgumentsオブジェクトに制限を加えてきました。Argumentsのプロパティに名前でアクセスできなくなったり、書き込めなくなったり、Callオブジェクトが取得できなくなったり、書き込めなくなったり。それどころかnetscapeはArgumentsオブジェクトそのものを無くしspreadに置き換えるつもりでした。(つもりでした──というのはもはやお馴染みのes4の登場です。es4ではその予定でしたがes4そのものが叶いませんでした)

ピンときた人もいるでしょうが元々のjsは関数の実引数とローカル変数についてSELFとのそれと同じことが出来ました。わざわざevalを使う必要はありません。
さてピンとこなかった人、実引数とローカル変数がオブジェクトのスロットであることは理解できたでしょう。でもあなたはこう思ったはず、「なぜスロットなら良いの? なにそれ美味しいの?」と。そうです。美味しいんです。ものごとを単純化しつつ強力なんです。SELFやjavascriptといった言語は動的かつ自己反映的な機能を持ち合わせているのが特徴です。それはものごとを単純化しつつ強力にしたおかげです。詳しくはSELF: The Power of Simplicityを読んでください。

話を戻しますがnetscapeが自己反映計算を嫌ったのはこの強力な一面です。強力すぎてネット上で使うには脆弱であったり最適化を妨げる場合もあるからです。

プロパティとスロットと変数と ~俺は俺より強いやつに会いに行く!~ 


スロットと変数は同じようなものです。インスタンス変数、クラス変数、プール変数、一時変数(ローカル変数)、静的変数などなど。ではプロパティとはなにか?

C#のプロパティ? MSに洗脳されすぎています。脳みそを入れ替えて出直して来なさい。

Smalltalkを知っている人はこの段落を飛ばしても支障ありません。プロパティというのはいちいち変数を作らずにメッセージを受け取れるようにするアプローチです。(今風に言えばデザインパターンか? Smalltalkはデザインパターンの宝庫と言われるくらいだし……)MS製言語にあるプロパティはただのアクセッサで(おそらく)Smalltalk由来でメッセージ送信に依存したデザインパターンなのでアクセッサとプロパティは別物です。

さてこのプロパティですが、SELFやjsではスロットには直接触れずにプロパティを利用します。jsのプロパティが古くから呼び出せるプロパティと言われるのはこれが関係します。他の言語では変数の操作とメッセージ送信は別の操作ですがSELFやjsではプロパティのみを直接触ることによりメッセージ送信によってスロット(変数)の操作も可能になり、変数の操作とメッセージ送信が等しくなります。これにより物事は単純化され、またッセージ送信計算モデルをより強力なものにします。SELFやjsでは当たり前のことなので分かりづらいですがjsの呼び出せるプロパティを考えれば理解することが出来ます。

他の言語では1)メソッドの呼び出しはメッセージ送信によって行います。2)変数の操作はそれとは別の操作です。しかし、jsではどちらもプロパティがメッセージを受け取ることによってそのスロットが値であろうとメソッド(関数)であろうと構わず同じ操作として扱えます。

具体例を示すと──
//変数アクセス
foo.a = "文字列をaにセット";
//メッセージ送信
foo.setA("文字列をaにセット");

は本質的に同じですが別々の操作です。

つまりjs的にはfoo.setA("文字列をaにセット");というコードは変数をひとつ無駄にし、さらに問題を複雑化しています。foo.a = "文字列をaにセット";だけで十分です。カプセル化という考え方は抽象データ型の考え方であってオブジェクト指向に必須の考え方ではありません。クラスベースしか知らない人たちはストラウストラップに騙されすぎです。よく考えて見れば簡単にわかることで、プロパティをカプセル化するということはメッセージを受けとれないということ。メッセージ送信をする言語でメッセージを受け取れなければ何も出来ません。直接スロットを操作しない言語であれば尚更のことです。

というか、ある変数を隠す、より限定されない方法というのは外部リンケージを持たないファイルスコープやモジュールスコープを利用することであるにもかかわらずADTとカプセル化というより限定的な方法に囚われすぎています。カプセル化というのはADTに必要な特殊な方法であって、それ以外の考え方の言語でも必ずしもその考え方が通用するとは限りません。ハンマーは金でもなければ弾丸は銀でもないでしょう。カプセル化を利用しない言語は他にもあります。

話を戻して、プロパティを使えば変数を直接触る必要もないかのように見えますが──SELFは実際そうです──プロトタイプベースでは環境がオブジェクトで、移譲を利用しているのは最初に言った通りです。jsもそうです。es3(Callオブジェクトの代わりにactivationというものがあるが細かく決まっていないので実装者の裁量による)までは大体そうです。es4は完全にオブジェクトでGlobalオブジェクト、Variableオブジェクト、Callオブジェクト、Argumentsオブジェクト(あとlet binding用のオブジェクト)とかありました。

es6! お前はダメだ!

es6ではactivationすらなくなってEnvironment Recordという別の概念に変わったのですが、これは不必要に複雑化してただでさえ分かりづらいと昔から評判のecma-262仕様をさらに複雑化してしまっただけです。普通のオブジェクトとしてはっきり定義すればプロトタイプベースの言語の既存の概念だけで説明できるもので、そちらのほうが新たな概念も用語も必要なく分かりやすいものになります。そもそもいままでGlobalオブジェクト、Argumentsオブジェクトの詳細を決めずにCallオブジェクトの代わりにactivationというものがあったのは実装に最適化する余地を与えるため正当な理由があったのですがes6になって突然改悪されました。

実装に最適化の余地を与える:実際にはプロトタイプベース言語の実装では実装のしやすさや最適化のためプロトタイプベース的方法とは違う方法で実装されているのは知られていますが、これは特別なことではなくSmalltalk環境でも制御文がメッセージ送信ではなく低レベルな仮想マシン語やJIT後のマシン語に翻訳され実行されているのと同じように一般的に行われるので、このような実装者の裁量による最適化が行える様に仕様は考慮する必要があります。


jsにはprototype chainの他にscope chainやスタックフレームを表すCallオブジェクトとArgumentsオブジェクトというのがあってこの3種はjsをプロトタイプベース足らしめている要素です。この要素があるからこそのプロトタイプベース言語であり、そして歴然としたプロトタイプベース言語であるSELFと同じことが出来ます。しかし、scope chainやCallオブジェクトは今までまともに標準化されてこなかったかと思えばes6になってオブジェクトではなくEnvironment Recordとなりscope chainという単語はその姿すらありません。環境やスコープもオブジェクトでありプロパティを持ち(これ重要、変数じゃダメ。オブジェクトのプロパティしか考えなくていいのにわざわざ変数/スロットを持ち込む必要はなく、そのような複雑性の増加はプロトタイプベース登場の経緯とその解決案からくる設計思想に反する)、スコープは移譲によって表現出来るという、クラスをなくしたプロトタイプベースの言語では普遍的かつ複雑性を排除しつつも強力さを維持する解釈が出来なくなってしまいます。

というかこの解釈が存在しなかったらselfのないSELFと同じことなんだが……


とまあ、昔から求められてる機能を導入することに注力しているbrendanに言っても仕方ないんですが、brendanが目指した言語のupdateすら行われずjsをプロトタイプベース足らしめている概念や考え方は正しく標準化されず……委員会方式の問題点が如実に現れています。最近はこのアンチパターンを嫌ってオープンなプロセスやガバナンス、ポリシーが増えていますね。

SELFやIoよりjavascriptの方が広まっている今日において、ecma-262仕様がこの言語をプロトタイプベース言語として正しく説明していない現状は即ちプロトタイプベース言語そのものが正しく理解されていないと置き換えることが出来るでしょう。

ecma-262仕様がダメダメなのは昔からですが、巷でjsがプロトタイプベース言語っぽくないと言われていますが、それにはこのような標準化プロセスやら組織の歴史的に続く問題のせいであって言語のそのものの問題ではないのです。

では本来のjavascriptとは一体どんなものだったのか?


プログラムが実行されるとトップレベルスコープが作られ、Globalオブジェクトのプロパティが共有されます。そこからプログラムが進むと関数にエンターし、スタックフレームであるCallオブジェクトが新しく作られ、これは関数を呼びだすごとに親へ委譲されます。Callオブジェクトはスタックフレームなのでローカル変数はここに作られますが、Callオブジェクトはプロトタイプベースにおけるオブジェクトなのでスロットは表に出てこずプロパティがメッセージを受け取ることになります。

Callオブジェクトには直接アクセスできず、Argumentsオブジェクトを介する必要があります。Argumentsは実引数に対してindexでアクセスすることが出来ますが、名前でアクセスすると自身のCallオブジェクトのプロパティへと転送されます。これは読み書き共に可能です。さらにcaller,calleeプロパティを持ち、さらにFunctionオブジェクトのインスタンスはcallerプロパティを持ちます。さらに関数のプロトタイプオブジェクトとFunctionオブジェクトのインスタンスにはargumentsプロパティがあります。

Argumentsオブジェクトのcallerがひとつ上のCallを参照しcalleeが関数のselfに相当します。Functionのcallerは自身を呼び出したFunctionを参照するのでかの有名な──arguments.callee.caller()というのが出来ます。もう一つ有名なarguments.callerを利用した親フレームの(当然プロパティを経由する)スロットの書き換えやスタックトレースといった当時のjavascriptにしか実現できないことが出来ます。実引数やスタックフレームを外部から書き換え可能でした。

javascriptのverが上がるたびにセキュリティや最適化のためにこのようなことが出来なくなり、やがてjs1.4まで到達します。この頃に出来たことはArgumentsオブジェクトにインデクスでアクセスし実引数を読み取れるだけの現代のjsに成り下がります。それでもスタックフレームやスコープはオブジェクトです。

スコープ(というより環境)のオブジェクトは通常のスコープやwithやcatchで種類が変わるので色々あります。
トップレベルのスコープも同じです。LobbyやShellかも知れませんが大体はGlobalかWindowのはずです。


しかし、既に述べたとおりecma262ではスタックフレームやスコープのオブジェクトが標準化されなかったりes6での変貌といった歴史的経緯があるのです。
そもそものecma262の起こりはjscriptがjavascriptと互換性を持っていないせいで始まり、最低限の共通部分を抽出した仕様に留まっていることが今のecma262仕様に繫がるのでこの経緯からしてダメっぷりはしかるべきです。

ですが今でもjavascriptのプロトタイプベースらしさを見ることの出来る方法がひとつつだけあります。それはecma262の実装ではなく伝統的なjavascriptの実装であるrhinoを利用することです。rhinoのデバッガを起動し、デバッガのインスペクタから__proto__や__parent__やWithオブジェクトの存在を知ることが出来ます。この方法ではwith文やcatchで差し替えられたスコープを見ることが出来ませんがrhinoのデバッガをインタプリタモードで起動しリモートデバッガをアタッチすれば全ての挙動を知ることが出来るでしょう。

javascriptの仕様とecma262の仕様の間には深い部分で大きな差異がありes6ではそれが表面的な部分においても顕著なものとなっています。ecma262の仕様しか知らなければjavascriptがプロトタイプベース言語らしく見えないのは当然のことです。


以下のようなコードがどのように動いているかrhinoで試してみるのは本当のjavascriptを知る良い方法の一つです。

// 大域変数を作っているように見えるが
var a = 10;
// thisのaプロパティにメッセージを送ると
// 値を取得できるので
print(this.a)
// aプロパティに対応するスロットがどこかにあるので
// aはthisのプロパティとしてちゃんと存在している( そして、メッセージを受け取れる)
print("a" in this)
print(this.hasOwnProperty("a"))

//実際の所、varはDontDelete属性を(configurable属性をfalseに)指定してプロパティを定義しているだけ
delete this.a
print("a" in this)

// DontDelete属性は付かない
this.b = 10;
delete this.b
print("b" in this)

// thisのインスタンスの種類は
print("実装によって異なるが" + Object.prototype.toString.call(this))

var f = (function(){
 return function(){
  // argumentsはどんなプロパティを持つか?
  // 何故今のjsが関数のローカル変数と実引数を書き換えられないのか?
  debugger;
  return 1;
 }
})();
// f.__parent__の中身は?

function foo(){}
// foo.__parent__の中身は?

var a=10;

with({a: "文字列"}){
 // Withオブジェクトの__proto__は?
 debugger;
 print(a);
}

try{
 throw new Error;
}catch(e){
// ここまでリモートデバッガで追ってきてエンジンはどんな挙動を見せたか?
// catchではなくlet変数ではどうか?
 debugger;
 print(e)
}


traits objectやprototype objectといったSELFが区別しているobjectの違いやその問題点とjsの場合の話はまた今度の機会に。

callableとfunctionとconstructor function: 予習編

以上3点の話。今回は予習のみ。次からは光の速さで駆け足。
callableとfunctionとconstructor functionの違いに注目して以下のソースを読んでね!(途中愚痴が書いてあるが気にせずに)
function Byte(bit){
"use strict";
 const NewTarget = this;

 const num = Number(bit);
 const bitToByte = num / 8;
 if(NewTarget == undefined){
  return bitToByte;
 }
 this.rawBit = num;
 this.bitToByte = bitToByte;
}

Object.defineProperty(Byte, Symbol.species, {
 get(){
  return Byte;
 }
});

Byte.prototype [Symbol.toStringTag] = "Byte";

Byte.prototype [Symbol.toPrimitive] = function(hint){
 switch(hint){
 case "default":
 case "number":
  return this.valueOf();
 case "string":
  return this.toString();
 }
 throw new TypeError;
}

Byte.prototype.valueOf = function(){
 return this.rawBit;
}

Byte.prototype.toString = function(){
 return String(this.valueOf());
}

function KiroByte(bit){
"use strict";
 const NewTarget = this;

 const num = Number(bit);
 const bitToKiro = num / (1024 *8);
 if(NewTarget == undefined){
  return bitToKiro;
 }
 this.rawBit = num;
 this.bitToKiro = bitToKiro;
}

KiroByte.fromByte = function(byte){
 return new KiroByte(byte.bitToByte *8);
}

Object.defineProperty(KiroByte, Symbol.species, {
 get(){
  return KiroByte;
 }
});

KiroByte.prototype [Symbol.toStringTag] = "KiroByte";

KiroByte.prototype [Symbol.toPrimitive] = function(hint){
 switch(hint){
 case "default":
 case "number":
  return this.valueOf();
 case "string":
  return this.toString();
 }
 throw new TypeError;
}

KiroByte.prototype.valueOf = function(){
 return this.rawBit;
}

KiroByte.prototype.toString = function(){
 return String(this.valueOf());
}

KiroByte.prototype.toByte = function() {
 return new Byte(this.rawBit);
};

const b = new Byte(8);
print(b.bitToByte + "byte is " + b.rawBit + " bit")

const k = new KiroByte(1024 * 8);
print(k.bitToKiro + "kiB is " + k.rawBit + " bit")

// 明示的型変換
print(Number(b))
print(Number(k))

print(Byte(8))
print(Byte(new KiroByte(1024 * 8)))

print(KiroByte(1024 * 8))
print(KiroByte(new Byte(1024 * 8)))

// キリンさんが好きです。でも、こっちの方がも~っと好きです!
//print(new KiroByte(1024 * 8) cast Byte)
//print(new Byte(1024 * 8) cast KiroByte)

// 暗黙的型変換
print(8 == new Byte(8))
print(1024 == new KiroByte(1024))

// netscape草案のunit型があればユーザー定義リテラルが使えたのに・・・
//print(8 == 8byte)
//print(1024 == 1024kiB)
//mozillaの十八番の print(10ft * 2inch + 100px * .5em) とか出来たのにメンドクサイ!

// no conversions
print(8 === new Byte(8))
print(1024 === new KiroByte(1024))

print(k instanceof KiroByte)
print(k instanceof Byte)
// なんでis operator無くなったんだ!なんでObject.is関数はSameValue algorithmなんだ!
//print(k is KiroByte)
//print(k is Byte)

print(Object.prototype.toString.call(b))
print(Object.prototype.toString.call(k))

print("b has bitToKiro", "==", "bitToKiro" in b)
print("k has bitToByte", "==", "bitToByte" in k)