🚇

Temporal Dead Zone と採用されなかった他の候補について

2022/05/13に公開

Temporal Dead Zone とは

ECMAScript 2015で採用された let/const のスコーピングの仕様をTemporal Dead Zoneと呼びます。

const x = "outer";
{
  const f = () => x;
  // console.log(f()); // => Error
  const x = "inner";
  console.log(f()); // => "inner"
}

4つの方式

Temporal Dead Zone という名前は、ブロックスコープ変数の挙動を決める際の4つの候補の名前に由来しているようです。
これはECMAScript 4が放棄されて間もない2008年10月のes-discussのログ[1]に言及があります。

A1. Lexical dead zone. References textually prior to a definition in the same block are an error.
A2. Lexical window. References textually prior to a definition in the same block go to outer scope.
B1. Temporal dead zone. References temporally prior to a definition in the same block are an error.
B2. Temporal window. References temporally prior to a definition in the same block go to outer scope.

ここで、

  • Lexical / temporal は最初に掲げた例のように、クロージャによって字句的な順序と実行順序の入れ替わりが生じたときの可視性の挙動について議論しています。
    • Lexical はソースコード上で let / const 以降に書かれている式から可視であるというルールです。
    • Temporal はブロックの実行中に当該 let / const が実行されて以降は可視であるというルールです。
  • Dead zone / window は当該 let / const が不可視だった場合の挙動について議論しています。
    • Dead zone はブロックスコープ変数が文の前後関係を理由に不可視と判定された場合にエラーとするルールです。
    • Window はブロックスコープ変数が文の前後関係を理由に不可視と判定された場合に外側の束縛にフォールバックするルールです。

そして結果としてB1のTemporal dead zoneがES2015の仕様として採択されたということになります。

Temporal dead zoneの是非

この仕様が覆ることはありませんが、あらためてこのtemporal dead zoneの是非について考えてみたいと思います。

Temporal window

まず当該のログでも最初に議論されているように、temporal windowは論外です。同じ識別子ノードがそのときの実行経路によって異なる束縛を参照するからです。こういう基本的な静的解析ができないのはプログラムにとっても、プログラマーにとっても、周辺ツールにとっても処理系にとってもあまりいいことはありません。

Lexical window

次に気になるのは広義ML族(OCaml, Haskell, Rust等)の let との関係です。ML族のletは通常ここでいうLexical windowのような挙動になります。では、JavaScriptの let/const は異端なのでしょうか?

筆者はこれを議論するにあたってはブロックという概念をよく意識するべきかなと思います。広義ML族のうちRust以外の let ... in はブロックのような構造内に並列しているのではなくネストした構造を持っているからです。

(* 構文的にはこのようにインデントされたものと考えたほうが自然 *)
let x = 42 in
  let x = x + 1 in
    let x = x + 1 in
      x

この構造であれば、内側の式が外側の式の要素を参照できるというほうがおかしな話だとなるでしょう。

この観点からはRustが特殊です。Rustにおけるブロックは手続き型言語で一般的なブロックに近く、 let も手続き型言語の変数宣言のようにブロック内に並列して存在しています。

{
  // これらの文は構文上、ブロック内で対等
  let x = 42;
  let x = x + 1;
  let x = x + 1;
}

であるにもかかわらず、Rustでは let のスコープを let の次の文以降としています。つまり、構文的にはC系の流れを汲みつつ、意味論的にはML系を意識しているハイブリッドだと考えられます。しばしばRustのシャドーイングが批判の対象になるのも、同様の中途半端さから来るものだと言ってもいいかもしれません。

話は少しそれますが、ここまで let / const 「以降」と書いたとき、 let / const 自身を含むのかどうかを誤魔化してきました。これは letlet rec の対比とも絡んでくる話です。OCamlは letlet rec を厳密に区別しますが、この派閥では以下のように考えます。

  • let は自然な定義であり、循環参照を避けるために let 自身の中で変数が有効にならないのもまた自然なルールである。
  • いっぽう関数定義では再帰を行うために自己参照をするのは許されるべきであり、このことを let rec 構文で明示した上で使うことは問題ない。

副次的にシャドーイングで次々に同名の束縛を更新するというパターンが実現できることになります。一方このような面倒さを嫌う派閥では letlet rec の区別を持たない場合もあります。 (Haskellなど)

単に面倒さというだけではなく、評価戦略の違いにより再帰の意味論が変わってくることも影響している気がしますがうろ覚えなので深くは触れないことにします。

hoistingとの対比

JavaScriptには元々hoistingと呼ばれる一連の仕様があります。まずvarの宣言部は関数スコープや同等のスコープまで持ち上げられます。

function f() {
  // iはfのスコープに入る
  for (var i = 0; i < 10; i++);
  {
    // これもf直下で宣言された扱いになる (複数ある場合はマージされる、代入処理自体はhoistされない)
    var i = 42;
  }
}

またfunction宣言はブロックの先頭で宣言・定義されたものとして扱われます。

f(); // この下のfが実行される
function f() {}

let/const のtemporal dead zoneも、こうしたhoistingの挙動とよく似た振る舞いであると見なせます。宣言部はブロックの先頭にあり、初期化だけが元の位置で行われるという見方です。 (ただし const では初期化を分離することはできないので、あくまで解釈上の説明にとどまる話です)

そして、 let/const では var と異なり、「未初期化」と「undefined で初期化されている」が区別されているため、初期化前にアクセスするとエラーになる、という寸法です。 (これはEnvironment Record上で実際にそのように形式化されている)

まとめ

  • Temporal dead zone は4つの仕様候補 (lexical dead zone, lexical window, temporal dead zone, temporal window) のひとつとして名付けられ、最終的にES2015の let/const の仕様として採用された。
  • 筆者の意見
    • ブロックを持つC系統の手続き型言語という観点からは、ブロックをスコープの基礎に置くのはある程度自然な判断であると考えられる。C系統とML系統のハイブリッドであるRustが特殊であるとも考えられる。
    • JavaScriptが元々持っていたhoisting挙動とも類似しているため、そこまで不自然でもない。
脚注
  1. ECMAScript Development Archiveから辿れるharmony:const [ES Wiki] のWeb Archive にこのリンクがある。 ↩︎

Discussion