詳細 ECMA-262-3 第8章 評価戦略
全国20人の ECMA セオリストのみなさま、おつかれさまです。大形尚弘です。
ついに Dmitry 先生の ES3 シリーズも最終章となりました。この後に ES5 シリーズが5章続きますが、それらは基本的に今シリーズの補足として書かれたものですので、ここまでお読みいただいたみなさまは、ほぼ ECMAScript の理論的側面を理解したと言えます。
もしそうでない部分があったとしても、実際に ECMAScript の仕様書をご覧いただければ、これまでとは全く理解度が違っていて、あっという間に足りない知識を補足できると思います。端的に、「仕様が読める」ようになっているはずです。
ES5 であれば、本来 PDF である仕様書を、有志の方が es5.github.com にて「注釈付きの」 HTML 形式で公開し、頻繁に更新されています。注釈の一つはもちろん我らが Dmitry 先生の ES シリーズです。ここまでお読みいただいたみなさまには、仕様書で初めて目にする言葉や概念はほんの少しです。是非、おすすめします。
それでは ES3 シリーズ最終章『評価戦略』をどうぞ。またいずれ ES5 シリーズの翻訳でお会いできればと思います。
詳細 ECMA-262-3 シリーズ
第8章 評価戦略
目次
はじめに
この短い章では、 ECMAScript における、関数に引き渡す引数の戦略について検討します。
一般的に、コンピュータサイエンスにおけるこの部分は、評価戦略と呼ばれます。つまり、プログラミング言語においてある式を評価し、演算するための規則の集合です。関数に引き渡す引数の戦略とは、その特定のケースです。
この章の動機は、あるフォーラムにおける同様のトピックでした。そこでの議論の結果私たちは、 ECMAScript における引数の引き渡し戦略について、ほぼ完全な表現を記述するに至りました。フォーラムや議論の中で用いることのできる、相応の定義も与えることになったのです。
多くのプログラマは、 JavaScript (あるいは常用する言語においても同様)におけるオブジェクトは参照によって関数に与えられ、一方プリミティブ型の値については値によって与えられると強く考えています。さらに、こうした「事実」はさまざまな記事、フォーラムでの議論、さらには JavaScript に関する書籍においても見ることができます。しかしながら、この用語の使い方がどれほど正確で、この説明が本当に正しいものなのか(さらに重要なことは、この理解がどれだけ正しいものなのか)、この章で議論していきたいと思います。
概説
簡単に申し上げると、一般的に評価戦略には二つの種類があります。正格、つまり実引数が関数への適用の前に計算されるものと、非正格、原則的に実引数の計算は必要になったときに都度実行されるもの(いわゆる「遅延」計算です)があります。
しかしここでは、私たちは ECMAScript の観点から、その理解に重要な、関数への引数の引き渡しに関する基本的な戦略について検討します。
始めに、 ECMAScript では、そのほかの多くの言語と同じように(例えば C 、 Java 、 Python 、 Ruby 等)、正格な引数引き渡し戦略が用いられています。
また、引数が評価される順序も重要です。 ECMAScript では左から右です。他の言語や実装では、この逆の評価順序(右から左)も使われる場合があります。
正格な引き渡し戦略はまた、その中でいくつかの戦略に細かく分けられています。そのうち最も重要ないくつかについては、この後で議論していきます。
以下で述べる全ての戦略が ECMAScript で採用されているわけではありませんので、例えば論理的な戦略を具体的に説明するために、 Pascal 様のシンタックスの抽象的な擬似コードを用いることにします。
値渡し
この種の戦略は、多くのプログラマに良くなじみのあるものです。ここでは、実引数の値は呼び出し元によって渡される対象の値の複製です。関数内部での仮引数への変更は、渡された対象の外側での状態には影響しません。一般的に、外部の対象が複製される際には新しいメモリ領域の割り当てが行われ(この議論では具体的な実装はさほど重要ではありません。スタックでも、動的メモリでもかまいません)、まさにこのメモリ上の新しい領域の値が、関数の内部で使われます。
bar = 10 procedure foo(barArg): barArg = 20; end foo(bar) // foo 内部の変更は、 // 外側の bar には影響しない print(bar) // 10
しかしながらこの戦略は、関数への実引数がプリミティブ値で無く、複雑な構造、オブジェクトである場合に、大きなパフォーマンスの問題を抱えてしまいます。これはまさに、 C/C++ で構造体が関数に値渡しされるとき、完全に複製されるときに起こっていることです。
以下の各評価戦略の説明で用いる、一般的なコード例をここに載せておきます。ある抽象的な手続きが、二つの引数を受け取ります。一つはオブジェクトの値、もう一つはブーリアンフラグで、このフラグによってオブジェクトの値を完全に(新しい値を代入して)変更するか、単にオブジェクトのプロパティを変更するのかを決定します。
bar = { x: 10, y: 20 } procedure foo(barArg, isFullChange): if isFullChange: barArg = {z: 1, q: 2} exit end barArg.x = 100 barArg.y = 200 end foo(bar) // 値渡しの戦略では、 // 外側のオブジェクトは変更されない print(bar) // {x: 10, y: 20} // 全体の変更(新しい値の代入) // においても同様 foo(bar, true) // 何の変更もされていない print(bar) // {x: 10, y: 20} 、 {z: 1, q: 2} ではない
参照渡し
次に、参照渡しの評価戦略は(これもまたよく知られたものです)、値の複製ではなく、対象への暗黙の参照を受け取ります。つまり、外側から渡される対象の、直接のメモリアドレスです。関数の内側での仮引数への変更は(新しい値の代入であれ、プロパティの変更であれ)、全て外側の対象にも影響します。なぜなら、まさにこの対象のアドレスが仮引数に結びついているからです。つまり、この場合の実引数とは、外側から渡される対象のエイリアスのようなものなのです。
擬似コードです。
// 手続き foo の定義は前の例を参照 // 同じオブジェクトを用意して bar = { x: 10, y: 20 } // 参照渡しの場合の // 手続き foo の結果は // 次の通り foo(bar) // オブジェクトのプロパティ値は変更されている print(bar) // {x: 100, y: 200} // 新しい値の代入も // オブジェクトに影響する foo(bar, true) // bar は新しいオブジェクトを参照している print(bar) // {z: 1, q: 2}
この戦略では、複雑なオブジェクトを効率的に渡すことができます。例えば、非常に大量のプロパティを持った構造体を渡す場合などに有効です。
共有渡し
最初の二つの戦略が多くのプログラマのよく知るところだったとしても、この戦略(あるいはより正確には、ここで議論する用語)は、それほど広く認知されていません。しかし、すぐに分かるとおり、まさにこれが、 ECMAScript での引数渡し戦略において重要な役割を果たしています。
この戦略の別の呼び方としては、「オブジェクト渡し」や「オブジェクト共有渡し」などがあります。
「共有渡し」の戦略は、 Barbara Liskov によって、 CLU プログラミング言語のために1974年に提案され、名付けられました。
この戦略の中心となるポイントは、関数は対象への参照の複製を受け取るという点です。この参照の複製が仮引数に結びつき、その値となるのです。
この場合、参照という考え方が現れてはいますが、参照渡しとして扱われるべきではありません(多くの人が誤解しているポイントです)。なぜなら実引数の値は直接のエイリアス(外側の対象のアドレスそのもの)ではなく、アドレスの複製だからです。
参照渡しとの大きな違いは、関数内部での仮引数への新しい値の代入は、外側の対象には影響しない(参照渡しの場合は影響します)ということです。しかしながら仮引数はアドレスの複製を保持しており、外側にあるものと同じ対象にアクセスすることが可能ですので(つまり外側から渡された対象が、値渡しのように完全に複製されているわけではない)、関数ローカルの仮引数のプロパティへの変更は、外側のオブジェクトに影響します。
// 手続き foo の定義は前の例を参照 // もう一度、同じオブジェクトを用意し bar = { x: 10, y: 20 } // 共有渡しの場合、 // 次の通り // オブジェクトに影響する foo(bar) // オブジェクトのプロパティ値は変更されている print(bar) // {x: 100, y: 200} // しかしオブジェクトの完全な変更は // 反映されない foo(bar, true) // 依然として一つ前の関数呼び出しの前と同じ状態 print(bar) // {x: 100, y: 200}
この戦略は、言語の大部分において、プリミティブ値ではなくオブジェクトを対象に演算を行うということを前提にしています。
値渡しの特別なケースとしての共有渡し
共有渡しの戦略は多くの言語で採用されています。例えば Java 、 ECMAScript 、 Python 、 Ruby 、 Visual Basic 等です。
Python コミュニティではまさにこの用語、共有渡しが使われています。他の言語に関してはまた別の用語が用いられたりしますが、命名上他の戦略と混同される原因ともなっています。
多くの場合、例えば Java や ECMAScript 、 Visual Basic においては、この戦略は参照の複製という値を意味する、値渡しの特別なケースとも呼ばれます。
この場合、関数の内部において、仮引数の名前に新しい値(アドレス)を束縛しても、外側の対象には影響を及ぼしません。
こうした用語の交錯は、実に(この問題を深く検証してみなければ)誤った取り扱い( JavaScript において関数にどのようにオブジェクトが渡されるのかというフォーラム中の議論)につながります。
しかし理論的には、これはまさに特定の値、つまりアドレスの複製を扱う値渡しの特別なケースであり、用語の規則を破っているわけでは無いのです。
Ruby では、この戦略が参照渡しと名付けられています。しかしここでも、(値渡しのように)大きな構造体の複製が渡されるわけではなく、対象へのオリジナルの参照を取り扱うわけでもないのです。結果として、こうした用語の錯綜が混乱につながってしまいます。
この理論は、参照渡しの特殊なケースを説明するものではなく、値渡しのある特定のケースを説明するものです。
しかしながら、こうした技術( Java 、 ECMAScript 、 Python 、 Ruby 等)で、それぞれに独自の用語が用いられていることは認識しておく必要があるでしょう。ただし実際に、それらは全て、理論的には共有渡しと名付けられるものです。
共有渡しとポインタ
C/C++ に関して言うと、この戦略は論理的にポインタ渡しに似ていますが、一点大きな違いがあります。ポインタ渡しの場合、ポインタをデリファレンスし、対象を完全に変更することができるのです。しかし一般的に、値(アドレス)をポインタに代入することは、新しいメモリブロックにポインタを束縛するということです(ポインタがその代入以前に参照していたメモリブロックはそのまま変更されず残ります)。そしてポインタを経由して参照する対象のプロパティの変更は、外側の対象にも影響するのです。
したがって、ポインタを用いて表現するなら、共有渡しとはアドレスの値渡しであり、まさにこのアドレスとはポインタがそれに当たります。この場合、共有渡しは代入においてポインタ(しかしデリファレンスできない)のようにふるまい、プロパティ変更においては参照(デリファレンス操作を必要としない)のようにふるまうある種の「シンタックスシュガー」であると言えます。時折これは「安全なポインタ」とも呼ばれます。
しかし、 C/C++ はまた、対象のプロパティを、明確なポインタのデリファレンス無しに参照できる、特別な「シンタックスシュガー」を用意しています。
(*obj).x ではなく obj->x
より C++ に近づいて考えてみると、この考え方は「スマートポインタ」の実装の一つに見ることができます。例えば boost::shared_ptr は、この用途のために代入演算子とコピーコンストラクタをオーバーロードし、対象への参照カウントを使い、 GC によって対象を削除します。このデータ型は、まるでこの戦略と同様の名前、shared_ptr を持ちます。
ECMAScript での実装
さて、これまでの通り、私たちは ECMAScript で用いられている、ある対象を引数として関数に引き渡す評価戦略について知ることができました。共有渡しです。引数のプロパティの変更は外側に影響し、新しい値の代入は、外側の対象には影響しません。
しかし前述したように、 ECMAScript デベロッパの中では、この戦略に対するローカルな用語も使われています。それは「値が参照の複製である場合の値渡し」と呼ばれるものでした。
JavaScript の発明者である Brendan Eich もまた、参照の複製(アドレスの複製)が渡されると述べています。あるフォーラムの議論の中には、 ECMAScript の開発者がこれを値渡しと呼んでいる場面もありました。
より正確に言えば、このふるまいは単純な代入を検討することで理解できます。二つの異なるオブジェクトがあり、しかしそれらは同じ値、つまり同じアドレスの複製をそれぞれが持つという場合です。
ECMAScript のコードで見てみましょう。
var foo = {x: 10, y: 20}; var bar = foo; alert(bar === foo); // true bar.x = 100; bar.y = 200; alert([foo.x, foo.y]); // [100, 200]
つまり、二つの識別子(名前束縛)が、メモリ上の同じ対象に束縛されているということです。対象を共有しているのです。
foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF) <= bar value: addr(0xFF)
そして、代入は単に識別子を新しい対象(新しいアドレス)に束縛し、(それ以前に)束縛されていた対象(古いアドレス)には影響しません。これがもし参照の場合では(メモリアドレスの参照先そのもののを書き換えますので)、影響してしまいます。
bar = {z: 1, q: 2}; alert([foo.x, foo.y]); // [100, 200] - 何も変わらない alert([bar.z, bar.q]); // [1, 2] - しかし bar は新しい対象を参照する
こうして foo と bar は異なる値、異なるアドレスを持つことになります。
foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF) bar value: addr(0xFA) => {z: 1, q: 2} (address 0xFA)
繰り返しになりますが、こうした一連の手続きは全て、オブジェクト型の場合の変数値はアドレスであり、オブジェクト構造そのものではないという事実によるものです。ある変数を他の変数に代入することは、その値参照を複製することであり、両方の変数はメモリ上の同じ場所を参照します。次に新しい値(新しいアドレス)の代入が行われると、これは古いアドレスから名前を解放し、新しいアドレスに束縛します。まさにこれが、重要な、参照渡し戦略との違いです。
さらに踏み込んで考えると、 ECMA-262 標準で与えられる抽象レベルのみを検討すれば、全てのアルゴリズムに現れるのは「値」という考え方だけです。そして「値」(そしてその種類、プリミティブ値であろうとオブジェクトであろうと)を渡す部分の実装は舞台裏に隠されています。この観点から、 ECMAScript について抽象的に議論するなら、正確かつ厳密な意味で、「値」のみが存在し、したがって値渡しのみであるとも言えるのです。
しかし誤解を避けるために(なぜ外部の対象のプロパティが関数内部で変更されるのか?)、実装レベルに降りて検討し、共有渡しあるいは「デリファレンスや対象を完全に変更することのできない、しかし対象のプロパティを変更することのできる、ポインタ渡し」といったように議論する必要があったわけです。
各用語について
それでは、 ECMAScript におけるこの問題に関して、正確な用語の定義を与えましょう。
引き渡される値がアドレスの複製であり、値渡しの特別なケースであると明確に表現できるので、「値渡し」であり得ます。この見地からは例外オブジェクトを除く ECMAScript 上の全てが値渡しであると言えますし、 ECMAScript 仕様の抽象レベルにおいては実際にこの定義で満たされます(訳注:それでは例外オブジェクトが「何渡し」にあたるのかは、訳者は理解できておりません。どなたかおわかりの方、お教えいただけると嬉しいです)。
あるいは、この場合の戦略について特別に、「共有渡し」であるとも言えます。これは正確に伝統的な値渡しと参照渡しの区別を明確にします。この場合は、引き渡される値の種類を二つに分類し、プリミティブ値は値渡し、オブジェクトは共有渡しであると表現できます。
「オブジェクトは参照渡しによって関数に参渡される」という表現は、 ECMAScript においては正式には誤りなのです。
結論
この章が、評価戦略の詳細、そして ECMAScript に関係する特徴を、包括的に理解する手助けになれば幸いです。いつものように、コメントでみなさまの質問にお答えします(訳注:これでシリーズ一旦完結となります。次の ES5 シリーズに進む前に、個人ブログに全て転載をいたしますので、質問等はそちらにお寄せいただければ分かる範囲でお答えしたいと思います。転載が遅くなって申し訳ありません)。
参考文献
英語版翻訳: Dmitry A. Soshnikov[英語版].
英語版公開日時: 2010-04-10
オリジナルロシア語版: Dmitry A. Soshnikov [ロシア語版]
協力: Zeroglif
オリジナルロシア語版公開日時: 2009-08-11
本シリーズはすべて英語版からの訳出です。