Subscribed unsubscribe Subscribe Subscribe

枕を欹てて聴く

香炉峰の雪は簾を撥げて看る

constへの異常な愛情, または私は如何にして心配するのを止めてConstDeclarationを愛するようになったか

ECMAScript JavaScript ES.next

JS Advent Calendar, オレ標準コース 1日目, id:Constellation です.
あれから1年ですか...


前半で, ES.nextに導入されたDeclarationについてさっと, そして後半でタイトルの説明をします.

この文書は, 2011/12/01現在のES5 Engineの実装とES.next draftに基づいています.
また, ES.nextの文法がvalidかどうかについては, parserを用意してあるので, 是非ご利用ください. http://constellation.github.com/iv/js/es.next.html

Declaration

ES.nextでは新たにDeclarationという区切りが入り, LetDeclaration, ConstDeclaration, FunctionDeclarationが導入されました.
Declarationは全てblock scopedな変数の宣言です. VariableStatementと違い, 有効範囲がblockの範囲(もしくはFunctionBody)に収まります.
また, Declarationはそのblock scopedという性質上Block直下, もしくはFunctionBody直下でしか利用できません
つまり,

if (cond) const i = 20;

はBlock直下ではないのでSyntax Invalidです.

LetDeclaration

待ちに待った(Mozilla JSer)letです. ES.nextでは新たにblock scopeという概念が入りました. すなわち,

{
  let i = 20;
  print(typeof i);  // number
}
print(typeof i);  // undefined

blockの中でだけ見える, というかblockで1層scopeが新たに作られるようになります.
LetDeclarationはblock scopedなVariableStatementのようなもので, 変数の範囲を小さく抑えることができます. 大事. ちなみにLetExpressionとかは今のところ入っていません.

また, 現行draftでは,

for (let i = 0; i < 20; ++i) {
  print(i);
}

というふうなletも許可されていません. block scopedなので,

for (let i = 0; i < 20; ++i) print(i);

というようなblockが存在しない場合についての仕様を詰めていかないとということなのでしょうか.

ConstDeclaration

ConstDeclarationがやって来ました.
ConstDeclarationは再代入不可な変数です. 具体的には, 再代入しようとすると, スルーされます. ちなみに, これがstrict modeの場合はErrorが出ます.

{
  const i = 20;
  i = 30;
  print(i === 20);
}
print(typeof i);  // undefined

これももちろんblock scopedなのでblockの外からは参照することができません.
また, initializerが必須という特徴があります. すなわち,

{
  const i;  // Syntax Error
}

です. 必ず, const i = ...;という風にinitializerが存在しなければいけません.

FunctionDeclaration

FunctionDeclarationはDeclarationの一員となりました. よって, 以下のような,

if (cond) {
  function decl() {
  }
}

利用が可能になりました. これはES5では厳密にはSyntax Errorになる挙動です. なぜなら, ES5ではFunctionDeclarationはFunctionBody, つまり関数直下にしか許可されていなかった為です.

ただし, 注意しなければいけません. これはblock scopedです. ということは, 従来からFunctionStatement *1 として解釈していたSpiderMonkeyの仕様, V8, JSC他の単純な全域に見えるFunctionDeclarationと解釈する仕様とも互換性がない, 全く新しい解釈方式となります.



というわけで, Declarationというblock scopedな宣言という構文要素が新たに追加されたのでした.

Avoid const

後半開始です.

f:id:Constellation:20111201031526p:image

http://s3.mrale.ph/nodecamp.eu/#63

V8のperformance tuningのslideとしてあがったもので, そのtopicの1つとして, "Avoid const"がありました. つまり, constを使うなという内容です. これはなぜでしょうか?
というのも根源的にはES5においてConstDeclarationは仕様に入っていなかった為です.
ES5でConstDeclarationをとなった時, そのままVariableStatementの構文, semanticsが採用され, かつconstnessが付け加えられました. つまり, VariableStatementの付加機能のような, patchingのような形で実装されたのです.

結果ES5の一般的実装におけるConstDeclarationについて, constな変数はSyntax上から判別不可能な状態を持ってしまうという致命的な欠陥を抱えてしまったのです.
つまり,

  1. 未初期化
  2. 初期化済み

の2つです.

function part1() {
  print(i);  // ここでiは未初期化
  const i = undefined;
  print(i);  // ここでiは初期化済みundefined

  const j = 20;
  print(j);  // ここでjは初期化済み値あり
}

ということです. 未初期化ならundefinedを入れておけばいいという簡単なことではありません. 具体的には代入処理のところです.

function part1() {
  const i = undefined;  // もちろんpass

  i = undefined;  // もちろんfailすべき. constnessが崩れている

  const j;
  if (cond) {
    j = "OK";
  }
  j = "TEST";  // condがtrueならfail!!!! これが問題
}

未初期化か初期化済みかどうかは代入を受け付けるかどうかの挙動が違います.
しかもcondがtrueならfailという例で示したとおり, syntax上からはこの代入がvalidなものかは判断できないので, 実行時にいちいち, 「この値は初期化済みか, それとも未初期化か」を検査する必要があります. 結果, 処理系内部では値読み込み時は,

if (初期化済み?) {
  return val;
} else {
  return undefined;
}

書き込み時は

if (初期化済み?) {
  fail();
} else {
  target = val;
}

という風になっています. 単なる変数なら, return valとtarget = valで済むのに, 読み込み, 書き込みのたびに初期化済みかどうかのcheckが必要という致命的なことになるのです.
これはもうはっきりと「ES5においてconstはvarよりずっと遅い」と言えます. これが冒頭のAvoid constの理由です.

in ES.next

しかし, ES.nextになって話は変わります. ここで先に説明したConstDeclarationについて重要な点をまとめましょう.

  1. Block Scopedである
  2. Initializerが必須である

そして, ここでわざわざDeclarationをStatementと分けblock scopedとした効果が生きてきます.

DeclarationをとれるのがBlock直下, 及びFunctionBody直下しか存在しない

というPointです. IfStatementもDoもWhileも, みんな"Statement"を取るはずです. そこにはDeclarationは含まれていません.

IfStatement:
  if ( Expression ) Statement else Statement
  if ( Expression ) Statement

ということは, 以下のScriptはES.nextにおいてSyntax Invalidです.

if (cond)
  const i = 20;

これらの条件を全て合わせると, ES5の時にはわからなかったconstの状態が, 全てSyntax Levelで解析することができるのです!

function part1() {
  const i = undefined;  // もちろんpass

  // const j;  // initializerが無いのでSyntax Error
  if (cond) {
    const j = "OK";  // ConstDeclationはBlock Scopedなので, このjは外部にもれない
  }
  const j = "TEST";  // これはpass. ifのblockのConstDeclarationとはScopeが異なる.

  // if (cond) const k = "OK";  // これがfail. DeclarationはBlock直下, FunctionBody直下にしか存在できない
}

ということを踏まえて,

function part1() {
  print(i);  // 未初期化const値であることが文脈上保証される. undefinedが必ず入ると解析可能. lookup必要なし

  const i = 10;

  print(i);  // 初期化済み値であることが解析可能.

  if (cond) {
    const j = "OK";  // ConstDeclationはBlock Scopedなので, このjは外部にもれない
  }

  print(typeof j);  // undefined. Block Scopedなので外部にもれない.

  // よってruntimeの条件分岐で未初期化/初期化済みのpathが分離しない
}

というように, 初期化済み, 未初期化, 代入不可といった情報が, syntax levelで解析することができます. runtimeの条件分岐で未初期化/初期化済みが分岐しなくなりました.
結果, Avoid constの理由であった初期化済みかどうかのcheckは全てparse時に行う事ができ, runtimeにおいてはzero overheadで参照することができます.
やりました! もうAvoid constとは言わせません! (in ES.next only)

まとめ

ES.nextにおいてconstの文法が精査された結果, constの不必要なoverheadは消え去り, costなしでconstnessの利点を享受できるようになりました.
ES5時代においてconstを使うことはperformance低下と標準を恐れぬ所業ですが, ES.nextではconstを使いまくりましょう!! constness万歳!

補足

今年の12/25は, ましろ色クリスマスですね! やったー!

*1:http://nanto.asablo.jp/blog/2005/12/10/172622