駆け足で始めるjavascriptのポリモーフィズム。その2

またここから、http://togetter.com/li/215907
caller.arguments[1] = ... なんて破壊的なことができたのね.知らなかったw
注意すべきはFunctionインスタンスのargumentsがスタックフレームを変更可能だったことが問題であってカプセル化が行なわれないことによってオブジェクトのプロパティを変更可能なことが破壊的なのではないということ。

javascriptの型システムはstructural-typesといってオブジェクトのもつスロットの集合によって型を識別している。それとは別によくある言語ではnominal-typesといってデータの集合に名前をつけてその名前で型を識別している。

structural-typesである型TがTとなるにはそれをTとすべくセマンティックな集合が存在して初めてTと認識する。

たとえばIntという型の集合には整数を表す集合を持っていたりRealという型の集合では実数の集合を持っている。この要領でIntとRealの両方に共通する集合を持つ型はたとえばNumberであるかもしれない。これらの型はインスタンス化された時、Intであれば整数の集合の任意の一要素を持っているオブジェクトのインスタンスとなる。

また、NumberはIntとRealの積集合なのでより上位の型(super type)とより下位の型(sub type)の関係を持つ。

これらのセマンティックな集合はjavascriptではプロパティの集合がそれにあたる。

プロパティの集合の一致をもってして型TはTとして認識されるので、ここで型Tを構成するあるプロパティPをカプセル化するという事が出来たとすればそれは型システムを偽ることが出来てしまう。

ここにLegal型はgood_foo、good_barというプロパティを持っていてIllegal型はgood_foo、good_bar、bad_bazというプロパティを持っているとき、IllegalはLegalのsub-typeである。

IllegalはLegalに対して良くない操作を行うbad_bazスロットを持っている。もし、このときbad_bazをカプセル化出来てしまっては実行環境はIllegal.bad_bazに気付かずにIllegalをLegalのsub-typeと油断してIllegal.bad_bazの呼び出しを許してしまう。

カプセル化というのはデータを主体に扱うnominal-typeな言語の考え方なのでstructural-typesで同じ考えをするとこういった不正に対応できない。なのでstructural-typeな言語ではカプセル化がない(ある場合もある)。

javascriptの場合はあくまでもスタックフレームまでをもプログラマに公開していたことが問題になるのです。なので現在の仕様ではスタックフレームを参照できても変更はできないようになっています。この問題に関する話が前回のエントリの範囲。


ではstructural-typeな言語ではカプセル化のような仕組みを実現したい時どうするか

それにはメタオブジェクトという概念があります。

メタオブジェクトとは一般的なオブジェクトを特定のドメイン向けのオブジェクトへと特化させる仕組みです。

具体的には任意の一般的なオブジェクトの振る舞いをハンドリングして好きなようにカスタムします。この様な仕組みはecmaではharmonyでProxyとして提案されていてspidermonkeyにはすでに実装されています。

少し話を戻しますがstructural-typeなjavascriptがプロパティの集合によって型を識別するとはいってもjavascript1.xでは事情があって、その性質上、型はタイプルーズでプロパティも常に動的で変更可能であるためswitch caseでプロパティの集合を利用したマッチングは出来ません。プロパティが指しているモノもなにかしら型を持つため型にゆるくプロパティも動的ならばマッチングする方法がないからです。

この様な型システムがあってもecmaの仕様の範囲ではsubtypingは行えません。

javascriptではプロパティの集合はプロトタイプ・チェーンに連鎖的に持っているためプロトタイプオブジェクトを変更できないecmaの現行仕様ではsubtypingできないのです。

これもまたharmonyで[[prototype]]を変更可能なtriangle operatorというものが提案されています。これならばsubtypingは可能です。

Array-likeではなくarray-subtypeなオブジェクトをプログラマによって作ることができます。

Object.createとsubtypingは別物なので注意しましょう

Object.createでsubtypingはできずObject.createはprototypeプロパティの汚染を解決するだけです。prototypeプロパティとプロトタイプオブジェクトそのモノの違いを思い出してください。

ちなみにarray-likeは

function ArrayLike(){ Array.prototype.push.apply( this, Array.prototype.slice.call(arguments) ); }

で簡単に作れます。Object.createやProxyは必要ありません。

しかし、このtriangle operatorはes6からは外れてしまいました。これによりjavascriptでsubtypingする術は失われました。

そろそろ締めですが、これまで見てきたようにjavascriptのオブジェクト指向はこのようなセマンティックスとパラダイムによって成り立っています。

Arguments オブジェクト

arguments.calleeは危険ではない

http://togetter.com/li/215907

非推奨の理由はarguments.calleeが自分自身への参照であるため関数の巧みな最適化を妨げるからです。arguments.calleeはargumentsからしか見えない参照を保持し続けるので効率が悪いのです。

本当に危険なのはarguments.calleeとarguments.callerとarguments.callee.callerを混同し理解していないことです。

その理由とこれらを取り巻く仕組みをこれから見ていきます。

arguments.calleeは自身の関数オブジェクトのインスタンスを指します。そのcallerはFunctionインスタンスのcallerを指します。よって呼び出し元オブジェクトを参照できます。arguments.callerは直接呼び出し元を参照しますがそれは関数ではなく関数のargumentsです。

そして、はるか昔argumentsのプロパティCallインスタンスのプロパティの内容を反映していたため呼び出された側から呼び出し元オブジェクトのスタックフレームを変更できました。

しかし、この仕様はjavascriptが標準化される以前に廃止され、argumentsはFunctionインスタンスではなくCallインスタンスのargumentsプロパティとして実引数のビューへと変わりました。ビューなので関数外の変数の内容を変更することはありません。さらにarguments.callerも非推奨となりました。

つまり、arguments.callee.callerすなわちFunctionインスタンスのcallerが危険なのは、はるか昔にFunctionインスタンスのargumentsが引数やローカル変数を好き勝手出来たという過去のはなしです。arguments.callerが危険であり非推奨になった理由も同じですが、ここで重要なのはFunctionインスタンスのargumentsは(関数の)外からも中からもアクセスできたということです。

外からも中からもアクセスでき呼び出し元オブジェクトを反映していたため、理解せず使うと理解していないがため修復不可能なバグを生みます。 javascriptは不遇でよく理解された言語とはとても言えません。故に危険なのです。

ただし、今現在はargumentsのプロパティはCallインスタンスのプロパティの内容を反映していないため関数のローカル変数にアクセスは出来ませんが、実引数へはargumentsにインデックスでアクセスすることがきます。これは先に述べたように実引数のビューなので関数外に変更は反映されませんが、ビュー自体は可変なので実引数ビューを直接持っているCallインスタンスのプロパティと実引数ビューの参照であるCallインスタンスのargumentsプロパティは互いに反映されます。

そのため、関数の中で『仮引数名』プロパティやarguments.『仮引数名』プロパティに代入すると互いに反映されることが、Callオブジェクトとargumentsの関係をよく理解していない直感に反する結果に思われるようです。


そこでStrictモードではこの挙動を単純化することにしました。

Callインスタンスの『仮引数名』プロパティとCallインスタンスのargumentsプロパティの各要素とは互いに反映されないようにしました。

そして、arguments.calleeの読み込み書きすらエラーを投げます。さらにFunctionインスタンスのcallerとargumentsがサポートされている実装でもそれらの読み書きの際にエラーを投げます。

これはFunctionインスタンスのcallerによってGlobalを返すコードがあるなど歴史的な理由から参照だけはできていましたがGlobalオブジェクトを取得できること自体がブラウザでは危険であるからです。ちがってarguments.calleeのほうは一番初めに述べたように最適化のためです。

以上のように危険なのはFunctionインスタンスのcallerと、はるか昔のFunctionインスタンスのargumentsであるためarguments.calleeが危険なのではありません。(間違ったcalleeを呼び出した無限ループまでは知りません。:-))


arguments.calleeが最適化を妨げるとは関数のインライン展開のことです。

たとえば無名関数は外からは参照されないにもかかわらずarguments.calleeに参照が存在するため無名関数の外からは参照されない参照を、呼び出しているあいだ保持する必要があるので効率が悪いのです。

名前付き関数式ならば結合オブジェクトなどで最適化出来る可能性がありますが、そもそも結合オブジェクトをサポートしているかは実装依存であり、さらに名前付き関数式をどこの環境にバインドするかは実装依存であるため実装によっては名前付き関数式の参照が残りますので最適化出来るかは結局実装依存です。

ではなぜecma標準のstrictモードでこの様な仕様が入っているかというとTC39グループが最適化をしたがっているからです。

組み込み用途でScripting API(JSR-223)を使ってはいけない理由

JSR-223はjava5以降のjavaのツール周りの強化の流れで導入されました。
このAPIはjavaからツール・チェインを利用する場合などにプラットフォーム非依存で簡単に利用できるシェルスクリプトを提供します。
その具体例がjrunscriptです。スクリプトを書いてjavaからコンパイルやテストを自動化するなどの用途に使います。
そのため、『スクリプトを実行する』という事以外は全てブラックボックス化されています。

もともと、スクリプトエンジンの各種実装にはエンジンの細かな制御やセキュリティに関わる操作を行うための仕組みがあります。
しかし、先に述べた理由よりJSR-223からはこの仕組みはブラックボックス化されていて、スクリプトエンジンに指示を与えて挙動を変えるといったことはAPIからは一切は出来ません。スクリプトのセキュリティ機構に関しても同じです。
実装によっては固有の方法で行えるかもしれませんがその実装が特定の環境で使える保証はありません。
JSR-223でできることは実行したいスクリプトを指定してそれを実行するだけです。それ以外に関与する仕様は何一つ用意されていません。

ですから組み込みで利用する場合には、その『それ以外に関与する仕様』という部分が不足しています。
また、nashornの例のように特定の実装がいつでも使えるとは限りません。実装には触れられないのですからバージョンが変わって利用できなくなったり互換性がなくなるというのは組み込み用途としては致命的です。
組み込み用途ですのでアプリケーションを利用するユーザーがスクリプトに触れるかもしれませんがエンジンには触れられません。
JSR-223の場合、APIのユーザーであるプログラマですらそこには触れられないのです。

ではJSR-223はどのようなケースで利用するかというとセキュリティも気にせずただ実行するだけのときのみです。
開発者が自分で書いたスクリプトを実行する場合、単に選択肢は2つあります。APIから呼び出すかjrunscriptから呼び出すかだけです。
この様な簡単なことを手間なく行えるようにするのがjavaの方針なので逆に言えばこの様な簡単な用途以外には使えないということです。

外部DSLとしてスクリプトを利用する場合にはスクリプトエンジンを自ら組み込む必要があります。

ついでにnashornについて、これはoracleが自社のサーバー製品でサーバーサイドjavascript環境を用意するために開発しているものです。
組み込み用途であるrhinoや本家mozillaのモダンなjavascirpt仕様がそのまま使える、要するに互換性があるとは期待できません。
また、DynamicInvokeによる高速化もrhino自身が十分に早いため新たな実装をわざわざ作ってまで期待できるものではありません。
nashornはoracleが自分のサーバーで使いたいために実装しているので開発者そのものにメリットはありません。
しかし、java8ではJSR-223の実装がnashornに置き換えられるため、今現在rhinoを使うためにJSR-223を利用している開発者はrhinoそのものへの乗り換えを検討する必要が出てくる可能性を考慮する必要があります。

具体的にはecmascript5ではなくjavascript1.8を(潜在的に)利用している・外部スクリプトの読み込みなど実装依存の機能を利用しているといった場合です。
とくに潜在的にjavascript1.8を利用している場合にはecmascriptとの使用の違いを理解した上で考慮することが必要です。