JavaScript 2.0
根拠
実行モデル
previousupnext

09/10/2002 (Tue)

このページはやや古いものである。

はじめに

(値、関数、型、クラス、メソッド、プラグマなどの) 宣言はいつその効果を生ずるのだろうか? 式が評価されるのはいつだろうか? これらの問いに対する答えは、一般的なプログラミング言語の間で異なる。以下の C++ や Java のような構文を持つ関数定義を考えよう:

gadget f(widget x) {
  if ((gizmo)(x) != null)
    return (gizmo)(x);
  return x.owner;
}

Java や C++ のような静的言語では、全ての型式はコンパイル時に評価される。例えばこの例の widgetgadget はコンパイル時に評価される。gizmo が型であれば、これもコンパイル時に評価される ((gizmo)(x) は型キャストになる)。(gizmo)(x) が引数が1つの関数呼び出しなのか (この場合 gizmo は実行時に評価される) 型キャストなのか (この場合 gizmo はコンパイル時に評価される) を決定するためには、変数を表す識別子と型を表す識別子を静的に区別できなければならないことに注意していただきたい。大抵の場合、以下の C++ のようにコンパイルが非常に困難だと考えられている例外があるにも関わらず、静的言語における宣言は囲っているスコープを通して可視である:

typedef int *x;

class foo {
  typedef x *y;
  typedef char *x;
}

多くの動的言語では型式を実行時に構築、評価、操作できる。(Common Lisp などの) 幾つかの動的言語は式の早期評価のためにコンパイル時と実行時を区別し、構造体 (eval-when) を提供している。(Scheme などの) 最も単純な動的言語では入力を単一のパスで処理し、コンパイル時と実行時を区別しない。上に挙げた関数をこのような単純な言語で評価した場合、widgetgadget は関数呼び出し時に評価される。

挑戦

JavaScript はスクリプト言語である。多くのプログラマは様々な環境で動作する JavaScript スクリプトをウェブページに組み込みたいと考えている。これらの環境の中にはスクリプトで使用したいライブラリを提供するものがあるかもしれないが、一方で他の環境ではスクリプトがそれらのライブラリを模倣しなければならないかもしれない。スクリプト言語で簡単に実現できると思われる例について見てみよう:

Bob はウェブページに使うスクリプトを書いており、特定の環境 (Macintosh) では利用可能だが他の環境では得られない選択的なパッケージ MacPack をスクリプト中で使用したいと考えている。MacPack には HyperWindoid というクラスが含まれており、Bob はこのクラスの派生クラスとして彼自身のクラス BobWindoid を作成しようと思っているのである。他のプラットホームでは Bob は BobWindoid とは異なる実装を持つ BobWindoid' を模倣クラスとして定義しなければならない — このクラスは異なるメソッドとフィールドの集合を持つ。また、Bob のパッケージには WindoidGuide というクラスもある。BobWindoid クラスと BobWindoid' クラスのコードとメソッドシグニチャは WindoidGuide 型のオブジェクトを参照し、WindoidGuide のコードは BobWindoid 型 (或いは BobWindoid' 型) のオブジェクトを参照する。

JavaScript が動的実行モデル (後述する) を使っていたら宣言の効果が生ずるのはその実行時だけであり、Bob は彼のパッケージを以下に示すように実装できる。

class WindoidGuide; // 前方宣言

if (onMac()) {
  import "MacPack";

  global class BobWindoid extends HyperWindoid {
    private var x;
    var g:WindoidGuide;

    private function speck() {...};
    public function zoom(a:WindoidGuide, uncle:HyperWindoid = null):WindoidGuide {...};
  }
} else {
  // BobWindoid' クラスの模倣
  global class BobWindoid {
    private var i:Integer, j:Integer;
    var g:WindoidGuide;

    private function advertise(h:WindoidGuide):WindoidGuide {...};
    private function subscribe(h:WindoidGuide):WindoidGuide {...};
    public function zoom(a:WindoidGuide):WindoidGuide {...};
  }
}

class WindoidGuide {
  var currentWindoid:BobWindoid;

  function introduce(arg:BobWindoid):BobWindoid {...};
}

一方、言語が静的なものであれば (型はコンパイル時の式ということになる)、Bob は問題にぶつかったことだろう。彼が選択的な2つの BobWindoid クラスを宣言するにはどうすればよかったのだろうか?

Bob の最初の考えは、彼のパッケージを3つの SCRIPT HTML タグ (それぞれ BobWindoidBobWindoid' 、WindoidGuide を含む) に分割し、プラットホームに応じて最初の2つを切り替えるというものだった。あいにくこれではうまく動かない。これらのクラスは相互に参照し合うため、BobWindoid クラス (或いは Windoid' クラス) の定義を WindoidGuide クラスの定義から引き離すと型エラーが発生するのである。更に Bob は多くのページでスクリプトを共有したいと思っているので、単一のファイル BobUtilities.js に全てのスクリプトをまとめたいのである。

これは JavaScript 2.0 で型式がコンパイル時に評価される仮定の下で新しく持ち上がった問題であることに注意していただきたい。JavaScript 1.5 にはコンパイル時に式を評価するという概念が無いため、この問題は起こらない。また、JavaScript 1.5 では、グローバル変数 g を1つだけ宣言して条件分岐で無名関数を代入することにより、クラス (実際は関数に過ぎないが) を条件的に定義することは比較的簡単なことである。

Bob の問題を解決する動的実行モデルと静的実行モデルの中間に位置する案もある。その内の1つはこの章の終わりで説明する。

動的実行モデル

純粋な動的実行モデルではプログラム全体が単一のパスで処理される。宣言がその効果を生ずるのは実行されたときだけで、永久に実行されない宣言は無視される。Visual Basic の初期のバージョンと同じく Scheme はこのモデルに従っている。

動的実行モデルは言語を大幅に単純化し、インタープリタはファイルから読み込まれたプログラムと対話型コンソールから入力されたプログラムを同等に扱うことができる。また、動的実行モデルのインタープリタや JIT コンパイラはスクリプトのダウンロードが完全に終了していなくてもスクリプトの実行を開始することができる。

動的実行モデルの最大の利点は、動的に得られた情報に基づいて JavaScript 2.0 スクリプトの一部を有効/無効化できることである。例えば、CSS の単位計算ライブラリを提供する環境ではスクリプトやライブラリは他にクラスや関数の定義を追加できるだろうし、そういった環境でなくてもスクリプトやライブラリは正しく稼動する。

動的実行モデルにおいては、関数や変数が使用される前に定義されるようにするために、それらに名前を付けることが識別子に対して要求される。「使用」は識別子がスコープ規則により変数或いは関数に解決される箇所で、識別子が読み取り、書き込み、或いは呼び出されるときに起こる。関数外側の ifwhile のような制御文の内側からの参照は、実行がその参照に到達したときのみ解決される。関数本体からの参照は関数が呼び出された後でのみ解決される。このため、実装は eval を含まない関数やメソッド内の全ての参照をその関数が初めて呼び出されるときに解決してもよい。

これらの規則よれば、以下のプログラムは正しいものであり 7 をプリントするであろう:

function f(a:Integer):Integer {
  return a+b;
}

var b:Integer = 4;
print(f(3));

変数 b がホストにより定義済みだとして、featurePresent が真であればこのプログラムもまた正しく動く:

function f(a:Integer):Integer {
  return a+b;
}

if (!featurePresent) {
  var b:Integer = 4;
}

print(f(3));

一方、以下のプログラムは f が定義される前に参照されているのでエラーになる:

print(f(3));

function f(a:Integer):Integer {
  return a*2;
}

相互再帰関数の定義は、それらが呼び出される前にそれらの全てを定義する限りは問題にはならない。

混成実行モデル

JavaScript 1.5 は純粋な動的実行モデルには従っておらず、互換性のために JavaScript 2.0 もこのモデルから逸脱しており、代わりに混成実行モデルを採用している。JavaScript 2.0 は以下の静的実行モデルの様相を JavaScript 1.5 から受け継いでいる:

これらに加えて、クラス宣言の評価には相互参照クラスを認めるための遅延評価のために特別な規定がある。

上に挙げた2つ目の条件から、以下のプログラムは JavaScript 2.0 では動作する:

const b:String = "Bee";

function square(a:Integer):Integer {
  b = a;   // グローバルな b ではなく、ローカルな b を参照する
  return b*a;
  var b:Integer;
}

このようなことは認められてはいるが、この例のような変数を宣言する前に使用するのはまずいスタイルであり、警告が出されるかもしれない。

上に挙げた3つ目の条件から、純粋な動的実行モデルの項の最後の例は正しく動く:

print(f(3));

function f(a:Integer):Integer {
  return a*2;
}

繰り返すが関数を宣言する前に最上位でその関数を呼び出すのはまずいスタイルであり、警告が出されるかもしれない。またクラスとともに用いた場合はうまく動かない。

動的実行モデルのコンパイル

おそらく、動的実行モデルに基づきスクリプトをコンパイルする最も簡単な方法は、関数定義を未処理のまま蓄積しておき最初に呼び出されたときにだけそれらをコンパイルするというものである。この方法は永久に呼び出されない関数のコンパイルにかかるオーバーヘッドを避けることができるため、多くの JIT コンパイラが使用している。静的実行モデルにおいてコンパイラはソースコードを二度走査するか、或いは二度目のために一度目で全て未処理のまま保存しておく必要があるため、上記の方法は静的実行モデルの場合よりもより少ないオーバーヘッドですむ。

オフラインでの動的実行モデルスクリプトのコンパイルも、eval が既存の宣言を隠す新しい宣言を導入しないように制限されている限りは難しいものではない (もし eval がそのようなことが可能であれば、静的実行モデルも含めてあらゆる実行モデルで問題が発生することだろう)。動的実行モデルの下では、一度コンパイラがスコープの終端に達するとそのスコープは完了したとみなされる。そしてこの時点でそのスコープ中の全ての識別子が解決可能な範囲は静的モデルの場合と同じである。

条件コンパイル案

Bob の問題は C プリプロセッサに似た条件コンパイルにより解決することもができる。条件コンパイルの採用するとすれば、条件コンパイルのメタ言語としてどのような式が良いか考えなければならない。C のプリプロセッサは貧弱なものである。JavaScript アプリケーションにおいて、コンパイルをどう制御するか決めるときに DOM や環境などを詳しく調べるためには、しばしば JavaScript の力をフルに使う必要がある。その上、JavaScript をメタ言語として使用すると、プログラマが習得しなければならない言語は少なくなる。

以下はこれをいかにして可能にするか考えてみたものである:

変数の初期化子はコンパイル時には評価されないため、型名 int の別名 a を定義するには var a = int ではなく #var a = int を使わなければならないことに注意していただきたい。

この下書きは解決すべき多くの問題、例えば型付けされた変数が宣言されてから初期化されるまでの間どのように処理されるか (この問題は動的実行モデルでは起こらない) とか、実行時パスのレキシカルスコープがコンパイル時パスのスコープとどのように関係するかなどといったことを焦点にしているのではない。

動的実行モデルと条件コンパイルの比較

いずれのアプローチも Bob の問題を解決するが、他の側面においてはこれらは異なったものである。以下に現れる「条件コンパイル」とは今まで説明してきた条件コンパイル案を指すものである。

コンパイラブロック

ある時期考えられていたが不採用となった実行モデル案にコンパイラブロックというものもある。コンパイラブロックは次のような構文を持つ:

   compile { Statement ... Statement }

compile 属性はそのブロックを早期評価してもよい (強制ではない) というヒントを示すものである。このブロックの内側の文はそれらの文、より早期に評価されるブロックの結果、そして早期に使用可能になるよう設計された環境プロパティにのみ依存していなければならない。早期評価が行われない場合は、コンパイラブロックは囲っているプログラムの全てのスコープ規則とセマンティクスを考慮する。コンパイラブロックにより導入されたあらゆる定義は (一度) 保存され、通常の評価時に再導入される。これに対し、副作用は通常の評価時に再導入されるかもしれないしされないかもしれないため、コンパイラブロックは副作用に依存すべきではない。

compile は属性であるので、ブロック中で個々の定義を囲まなくてもそれぞれに適用できる。

例として、以下の定義の後、

compile var x = 2;

function f1() {
  compile {
    var y = 5;
    var x = 1;
    while (y) x *= y--;
  }
  return ++x;
}

function f2() {
  compile {
    var y = x;
  }
  return x+y;
}

グローバルな x の値は 2 のままであり、f1() の呼び出しは常に 121 を、f2() の呼び出しは 4 を返す。仮に x=5 という文がグローバルレベルで評価されても f1()121 を返すままである。これはローカルな x が使用されていることによる。これに対して f2() の呼び出しは実装の解釈によっては 710 のいずれをも返す — 実装が compile ブロックを早期評価し値を保存すれば 7 を返し、そうしなければ 10 を返すのである。この例から分かるように、コンパイラブロック内部での変数定義はまずいテクニックである。普通は定数を使った方が良い。

完全な動的実行モデルに則った JavaScript 2.0 実装は compile 属性を無視して、全てのコンパイラブロックを通常のタイミングで評価することにしてもよい。完全な静的実行モデルに則った実装は全てのユーザ定義の型と属性をコンパイラブロック中で定義するよう要求してもよい。

const four = 2+2 のように単純な定数式を伴った const 定義を暗黙のコンパイラ定義 (compile const four = 2+2) として扱うべきだろうか?


Waldemar Horwat
最終更新: 2002年9月10日 (火)
previousupnext
訳者: exeal <exeal@student.interq.or.jp>
このドキュメントのオリジナルは mozilla.org において英語で公布されています。
この和訳は、利用者の利便のために Mozilla Japan 翻訳部門 によって提供されています。
内容に関してご不明な点がありましたら webmaster までお問い合わせください。