「クロージャは関数と環境のペア」とは?(JavaScript)

2023/12/02

クロージャを表す画像。2つの正方形が横方向にペアになっており、左の正方形の中にはF、右の正方形の中にはEと書かれている。

はじめに

クロージャは「関数と環境のペア」と言われます。最初は意味がわかりません。

クロージャ(クロージャー、英語: closure)、関数閉包はプログラミング言語における関数オブジェクトの一種。……(中略)…… 関数とそれを評価する環境のペア であるともいえる。

出典:クロージャ - Wikipedia、強調と中略は筆者による

クロージャは、組み合わされた(囲まれた) 関数と、その周囲の状態(レキシカル環境)への参照の組み合わせ です。

出典:クロージャ - JavaScript | MDN、強調は筆者による

これを理解するために調べたことをまとめました。

サンプルプログラム

本記事を通して、以下のプログラムを使用します。

const makeCounter = () => {
  let cnt = 0;
  return () => {
    cnt++;
    return cnt;
  };
};

const counter1 = makeCounter();
const counter2 = makeCounter();

console.log(counter1()); // 1
console.log(counter2()); // 1
console.log(counter1()); // 2

クロージャの説明として定番のやつですね。

SICPでの説明

SICP(Structure and Interpretation of Computer Programs)という本における説明がわかりやすいので紹介します。

この本で使用されている言語はSchemeですが、JavaScriptに置き換えて説明します。1

この章で説明したいこと

クロージャは、

  • 関数を評価すると作成される
  • 関数と環境のペアである
    • 関数:関数への参照。具体的に何なのかは処理系によって異なる
    • 環境:クロージャが作成されたときの環境への参照
  • 呼び出されたとき、
    1. 新しい環境が作成され、その「外側の環境」はクロージャの環境になる
    2. 新しい環境でクロージャの関数が評価される

環境とは、「束縛の集合」と「外側の環境への参照」のペア。

束縛とは、「変数」と「値」のペア。

Step 0:グローバル環境の作成

プログラムを実行する準備として、次のようなグローバル環境が作成されます。

四角が1つある。四角の中にGlobal Envと書かれている。その下に区切り線があり、その下は空欄である。四角から矢印が出ており、バツ印を指している。

環境とは、「束縛の集合」と「外側の環境への参照」のペアです。この環境には「Global Env」という名前がついており、区切り線の上に書かれています。

束縛とは、「変数」と「値」のペアです。束縛の集合は区切り線の下に書かれます。現状は束縛はありません。

矢印は外側の環境への参照です。グローバル環境に外側は無いため、バツ印を指しています。

以降は、このグローバル環境でサンプルプログラムを評価していきます。

Step 1:makeCounterの作成

const makeCounter = () => {
  let cnt = 0;
  return () => {
    cnt++;
    return cnt;
  };
};

上記のプログラムでは、アロー関数をグローバル環境で評価し、その結果をmakeCounterに代入しています。すると以下の図のようになります。

先程の図のGlobal Envの区切り線の下にmakeCounterという項目が追加されている。makeCounterからは矢印が伸びて、正方形が横方向に2つくっついた物を指している。左の正方形の中にはFと書かれており、そこから矢印が伸びて「() => { let cnt = 0; ... }」というテキストを指している。右の正方形の中にはEと書かれていて、そこから矢印が伸びてGlobal Envの四角形を指している。

関数を評価すると、クロージャが作成されます。正方形の2つのペアがクロージャです。

左のFと書かれた正方形は、関数への参照です。参照先が具体的に何なのかは処理系によって異なりますが、例えばAST、バイトコード、マシンコードなどがあり得ます。

右のEと書かれた正方形は、クロージャが作成されたときの環境への参照です。今回は関数をグローバル環境で評価したので、グローバル環境を指しています。

評価結果であるクロージャをグローバル環境のmakeCounterに代入します。グローバル環境にmakeCounterとクロージャの束縛が追加されました。

Step 2:makeCounterの呼び出し(counter1

const counter1 = makeCounter();

makeCounterはクロージャなので、makeCounter()はクロージャを呼び出しています。

クロージャが呼び出されると、

  1. 新しい環境が作成され、その「外側の環境」はクロージャの環境になります
  2. 新しい環境でクロージャの関数が評価されます

Step 2.1:新しい環境が作成され、その「外側の環境」はクロージャの環境になる

新しい環境Env 1を作成します。今回のクロージャの環境はGlobal Envを指しているので、それをEnv 1の外側の環境とします。

先程の図に加えて新たにEnv 1という環境が書かれている。Env 1の下に区切り線があり、その下に空白がある。Env 1の環境からは矢印が伸び、Global Envの環境を指している。

Step 2.2:新しい環境でクロージャの関数が評価される

クロージャが指す関数の本体は以下です。

let cnt = 0;
return () => {
  cnt++;
  return cnt;
};

これをEnv 1で評価していきます。

まず、let cnt = 0;によってcnt0の束縛が作成されます。

先程の図に加えて、Env 1にcnt: 0という束縛が増えている。

次にreturn () => { cnt++; return cnt; };の部分ですが、これは関数(アロー関数)の評価結果をreturnしています。

おさらいですが、関数を評価するとクロージャが作成されます。

クロージャは、「関数への参照」と「クロージャが作成されたときの環境(ここではEnv 1)への参照」のペアです。

これがreturnされて最終的にcounter1に代入されるため、結果は以下のようになります。

先程の図に加えて、Global Envにcounter1の束縛が増えている。counter1からはクロージャ(2つの正方形のペア)への矢印が伸びている。そのクロージャの関数(左の正方形)からは「() => { cnt++; ... };」というテキストへの矢印が、環境(右の正方形)からはEnv 1への矢印が伸びている。

Step 3:makeCounterの呼び出し(counter2

同様にcounter2が定義されます。

const counter2 = makeCounter();

結果は以下のようになります。

先程の図に加えて、Env 2という環境が追加されている。Env 2にはcnt: 0という束縛があり、Global Envに向かって矢印が伸びている。Global Envにはcounter2という束縛が追加されている。counter2からは新しいクロージャに向かって矢印が伸びている。新しいクロージャの関数は「() => { cnt++; ... };」というテキストを、環境はEnv 2を指している。

Step 4:counterの呼び出し

console.log(counter1()); // 1

counter1()はクロージャを呼び出しています。

おさらいですが、クロージャが呼び出されると、クロージャの環境を外側の環境とする新しい環境が作成され、そこでクロージャの関数が評価されます。

したがって、まず以下ようなEnv 3が作成されます。

先程の図に加えて、Env 3という環境が追加されている。この環境に束縛はなく、Env 1に矢印が伸びている。

次に、クロージャの関数をEnv 3で評価します。クロージャが指す関数の本体は以下のとおりです。

cnt++;
return cnt;

まず、Env 3でcnt++;を評価します。Env 3に 変数cntの束縛はないため、外側の環境である Env 1に探しに行きます。そこでcntが見つかるため、Env 1のcntをインクリメントして、以下のようになります。

先程の図とほぼ同じだが、Env 1のcntの値が1になっている。

次に、Env 3でreturn cnt;を評価します。先ほどと同じようにcntを探索すると、Env 1で束縛が見つかるため、最終的に1returnされ、console.logによって表示されます。

同じように、counter2を呼び出した場合は、Env 2のcntがインクリメントされます。

最終的に、すべてのサンプルプログラムの実行が終わった時点では、以下の図のようになっています。

先程の図に加えて、Env 4、Env 5が追加されている。いずれも束縛はない。Env 4からはEnv 2へ矢印が伸びている。Env 5からはEnv 2へ矢印が伸びている。また、Env 1のcntの値は2に、Env 2のcntの値は1になっている。

SICPの説明まとめ

クロージャは、

  • 関数を評価すると作成される
  • 関数と環境のペアである
    • 関数:関数への参照。具体的に何なのかは処理系によって異なる
    • 環境:クロージャが作成されたときの環境への参照
  • 呼び出されたとき、
    1. 新しい環境が作成され、その「外側の環境」はクロージャの環境になる
    2. 新しい環境でクロージャの関数が評価される

環境とは、「束縛の集合」と「外側の環境への参照」のペア。

束縛とは、「変数」と「値」のペア。

JavaScriptでは

SICPの説明はSchemeを前提にしていました。

実際のJavaScriptの挙動を確かめるために、ECMAScriptを雰囲気で読んでみましょう。

クロージャの作成(アロー関数の評価)

アロー関数についての章立ては以下のようになっています。

  • 15.3 Arrow Function Definitions
    • 15.3.1 SS: Early Errors
    • 15.3.2 SS: ConciseBodyContainsUseStrict
    • 15.3.3 RS: EvaluateConciseBody
    • 15.3.4 RS: InstantiateArrowFunctionExpression
    • 15.3.5 RS: Evaluation

SSとRSは、それぞれStatic SemanticsとRuntime Semanticsのことみたいです。

Static Semanticsには、プログラムを動かす前にわかるようなことが定義されています。例えば、非同期のアロー関数でないのにawaitが現れるなら文法エラー、などです。

今回は、プログラム実行時の評価の挙動に興味があるので、15.3.5 Runtime Semantics: Evaluationから見ていくと良さそうですね。

以下、今回関心のある部分のみ抜粋します。

# 15.3.5 Runtime Semantics: Evaluation
ArrowFunction : ArrowParameters => ConciseBody
  1. Return InstantiateArrowFunctionExpression of ArrowFunction.

# 15.3.4 Runtime Semantics: InstantiateArrowFunctionExpression
ArrowFunction : ArrowParameters => ConciseBody
  // 現在の環境をenvとおく
  2. Let env be the LexicalEnvironment of the running execution context.

  // OrdinaryFunctionCreateを呼び出す。ここで関数(ConciseBody)と環境(env)を渡している
  // その結果をclosureとおく
  5. Let closure be OrdinaryFunctionCreate(%Function.prototype%, sourceText, ArrowParameters, ConciseBody, LEXICAL-THIS, env, privateEnv).

  // closureを返す
  7. Return closure.

# 10.2.3 OrdinaryFunctionCreate ( functionPrototype, sourceText, ParameterList, Body, thisMode, env, privateEnv )
  // 新規オブジェクト作成し、Fとおく
  2. Let F be OrdinaryObjectCreate(functionPrototype, internalSlotsList).

  // Fに関数のBodyをセットする
  6. Set F.[[ECMAScriptCode]] to Body.

  // Fに現在の環境をセットする
  13. Set F.[[Environment]] to env.

  // Fを返す(アロー関数の評価結果となる)
  23. Return F.

おー、やってますね〜。

アロー関数の評価時にオブジェクトを作成し、関数(F.[[ECMAScriptCode]])と環境(F.[[Environment]])を内部スロットにセットしていそうです。

念のため、それぞれの内部スロットの説明を詳しく見てみます。

  • [[ECMAScriptCode]]
    • Type:a Parse Node
    • 説明:関数のBodyを定義するソース・テキストのルート解析ノード。
  • [[Environment]]
    • Type:an Environment Record
    • 説明:関数が閉包されたEnvironment Record。関数のコードを評価する際の外部環境として使用されます。

出典:10.2 ECMAScript Function Objects, ECMAScript、ただし、表をリストに筆者が改変した。また翻訳はDeepLのものを筆者が修正したもの

思ってたとおりですね。

また、F.[[Environment]]の中身である、Environment Recordについては……

Environment Recordは、(中略)識別子と特定の変数および関数との関連付けを定義するために使用される仕様タイプです。通常、(中略)FunctionDeclarationやBlockStatement、TryStatementのCatch節など(中略)が評価されるたびに、そのコードによって作成される識別子の束縛を記録する新しいEnvironment Recordが作成されます。

すべてのEnvironment Recordには、[[OuterEnv]]フィールドがあり、これは NULL または外部のEnvironment Recordへの参照です。

出典:9.1 Environment Records, ECMAScript、翻訳はDeepLのものを筆者が改変したもの

SICPの説明で聞いたようなことが書かれてます。よしよし。

ちなみに、OrdinaryFunctionCreateはアロー関数だけではなく、

  • functionによる(非同期)関数の評価
  • (非同期)メソッドの評価
  • getter / setterの評価
  • (非同期)ジェネレータの評価

でも使用されているようでした。

しがたって、今回のサンプルプログラムではアロー関数を使用しましたが、他の書き方でもクロージャが生成されるのは変わらないということです。

「JavaScriptのほとんど(あるいはすべて?)の関数はクロージャ」と言われる理由がわかります。

クロージャの呼び出し(関数呼び出しの評価)

関数呼び出しの評価は、13.3.6.1 Runtime Semantics: Evaluationから見ていくと良さそうです。

以下、今回関心のある部分のみ抜粋します。

# 13.3.6.1 Runtime Semantics: Evaluation
CallExpression : CallExpression Arguments
  5. Return ? EvaluateCall(func, ref, Arguments, tailCall).

# 13.3.6.2 EvaluateCall ( func, ref, arguments, tailPosition )
  7. Return ? Call(func, thisValue, argList).

# 7.3.14 Call ( F, V [ , argumentsList ] )
  3. Return ? F.[[Call]](V, argumentsList).

# 10.2.1 [[Call]] ( thisArgument, argumentsList )
  2. Let calleeContext be PrepareForOrdinaryCall(F, undefined).

# 10.2.1.1 PrepareForOrdinaryCall ( F, newTarget )
  7. Let localEnv be NewFunctionEnvironment(F, newTarget).

# 9.1.2.4 NewFunctionEnvironment ( F, newTarget )
  // 束縛のない新しいEnvironment Recordを作成し、envとおく
  1. Let env be a new Function Environment Record containing no bindings.

  // envの外側の環境をクロージャの環境にする
  6. Set env.[[OuterEnv]] to F.[[Environment]].

というわけで、クロージャの呼び出し時に、その環境を外側の環境とする新しい環境を作成していそうだとわかります。

JavaScriptまとめ

アロー関数の評価結果は関数オブジェクトでした。これは関数([[ECMAScriptCode]])と環境([[Environment]])を持っているためクロージャです。

[[Environment]]の中身であるEnvironment Recordは、束縛の集合と、外側の環境への参照([[OuterEnv]])を持ちます。これは環境に相当します。

V8では

最後に、実際のJSエンジン内部でクロージャがどのように実現されているか、ふんわり眺めてみましょう。

バイトコードを見てみる

はじめに、makeCounterのバイトコードを見てみます。バイトコードおよびインタプリタについての説明は公式サイトに譲ります。ここでは、「V8がソースコードから生成したやることリスト」くらいの認識で行きましょう。

ともかく、--print-bytecodeオプションを使用してみます。

$ node -v
v18.3.0

$ node --print-bytecode --print-bytecode-filter=makeCounter sample.js
...
   20 E> 0x1c7f8fca6c8 @    0 : 83 00 01          CreateFunctionContext [0], [1]
         0x1c7f8fca6cb @    3 : 1a fa             PushContext r0
...
   40 S> 0x1c7f8fca6d0 @    8 : 0c                LdaZero
   40 E> 0x1c7f8fca6d1 @    9 : 25 02             StaCurrentContextSlot [2]
   45 S> 0x1c7f8fca6d3 @   11 : 80 01 00 02       CreateClosure [1], [0], #2
   91 S> 0x1c7f8fca6d7 @   15 : a9                Return
...

ふむり。

まず、CreateFunctionContextが気になります。makeCounterの呼び出し時に毎回Contextが作られるということで、これは環境やEnvironment Recordの作成に相当するものかと予想しました。

その後のPushContextは、コードを実行する環境を新しいものに切り替えてるっぽいですね。

続くLdaZeroStaCurrentContextSlotは、let cnt = 0;に対応してそうです。Zeroを生成して、Context(環境?)にストアしてる?

残りのCreateClosureReturnはそのまんまですね。

デバッグプリントしてみる

次に、--allow-natives-syntax%DebugPrintを使って、CreateClosureの結果がどのようになっているのかをデバッグプリントしてみます。

サンプルコードの最後に%DebugPrint(counter1);を追加して、次のコマンドを実行します。

$ node --allow-natives-syntax sample.js
DebugPrint: 0x2d73c590c5f9: [Function]
 - map: 0x11b664d813b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x20596e7cb2e9 <JSFunction (sfi = 0x358d345f99a1)>
 - elements: 0x1fd612d41329 <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: <no-prototype-slot>
...

なるほどわからん。

わからんなりに、V8のリポジトリを- function prototype:で全文検索してみます。

すると、以下の1件だけヒットします。

// ...
void JSFunction::JSFunctionPrint(std::ostream& os) {
  Isolate* isolate = GetIsolate();
  JSObjectPrintHeader(os, *this, "Function");
  os << "\n - function prototype: "; // ここにヒット
  if (has_prototype_slot()) {
// ...

出典:v8/src/diagnostics/objects-printer.cc at a619087d49b84eb2f98c7c13d5329c34eca17bde · v8/v8、補足のコメントは筆者による

これはJSFunctionをデバッグプリントするためのコードなので、counter1JSFunctionという構造で表現されているはずです。

重要そうな構造の定義を眺めてみる

というわけで、JSFunctionの定義に飛んで周辺を見てみますと、

// JSFunction describes JavaScript functions.
// 訳:JSFunctionはJavaScriptのfunctionsを表す
class JSFunction : public TorqueGeneratedJSFunction<
                       JSFunction, JSFunctionOrBoundFunctionOrWrappedFunction> {
 public:

  // ...

  // [context]: The context for this function.
  // 訳:[context]: このfunctionのcontext
  inline Tagged<Context> context();

  // ...

  // [code]: The generated code object for this function.  Executed
  // when the function is invoked, e.g. foo() or new foo(). See
  // [[Call]] and [[Construct]] description in ECMA-262, section
  // 8.6.2, page 27.
  // 訳:[code]: 生成されたこの関数のコードオブジェクト。
  // foo()やnew foo()など、関数が呼び出されたときに実行される。
  // ECMA-262の8.6.2(27ページ)における[[Call]]と[[Construct]]の説明を参照。
  DECL_ACCESSORS(code, Tagged<Code>)

出典:v8/src/objects/js-function.h at a619087d49b84eb2f98c7c13d5329c34eca17bde · v8/v8、コメントの翻訳はDeepLのものを筆者が改変したもの

冒頭のコメントを見るに、JSFunctionはJavaScriptの関数を表す内部表現で間違っていなさそうですね。

[code][context]というメンバは、なんとなくですが関数と環境であるように見えます。なんとなくですけど。

次にContextの定義周辺も見てみます。

// JSFunctions are pairs (context, function code), sometimes also called
// closures.
// 訳:JSFunctionsは(context, function code)のペアであり、これはクロージャとも呼ばれる。

// [ previous       ]  A pointer to the previous context.
// 訳:[ previous       ]  前のコンテキストへの参照
class Context : public TorqueGeneratedContext<Context, HeapObject> {
 public:

  //...

  inline Tagged<Context> previous() const;

出典:v8/src/objects/contexts.h at a619087d49b84eb2f98c7c13d5329c34eca17bde · v8/v8、コメントの翻訳はDeepLのものを筆者が改変したもの

さきほど、Contextは環境に相当するものだと予想していましたが、冒頭のコメントを見る限り正解っぽいです。ヨッシャ

previousというメンバには他の環境への参照が入ると書かれています。明示的に書かれていませんが、外側の環境への参照ですかね?

より詳しくデバッグプリントしてみる

先程のJSFunctionPrintcontextの内容を詳しく表示してくれないようでした。

おもむろにobjects-printer.ccを散歩していたところ、ContextPrintという、いかにもcontextを良い感じに表示してくれそうな処理を見つけました。

というわけで、JSFunctionPrintを改造して、contextとそのpreviousに関する詳しい情報を表示するようにしてみます。

V8のビルド環境を構築します
cd $HOME
mkdir ws # ワークスペースを作成
cd ws

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git --depth 1
PATH="$PATH:$HOME/ws/depot_tools/"

fetch v8
cd v8

sudo apt instal ccache

ccacheを導入するために以下のように変更します。

diff --git a/.gn b/.gn
index 3a73ff4e2a1..572d1e3caf6 100644
--- a/.gn
+++ b/.gn
@@ -27,6 +27,7 @@ no_check_targets = [
 default_args = {
   # Disable rust dependencies.
   enable_rust = false
+  cc_wrapper = "ccache"
 }

 # These are the list of GN files that run exec_script. This whitelist exists

以下のようにコードを変更します。

diff --git a/src/diagnostics/objects-printer.cc b/src/diagnostics/objects-printer.cc
index 51c4d991d17..3d9c03d64a7 100644
--- a/src/diagnostics/objects-printer.cc
+++ b/src/diagnostics/objects-printer.cc
@@ -1905,6 +1905,12 @@ void JSFunction::JSFunctionPrint(std::ostream& os) {
   } else {
     os << "not available\n";
   }
+
+  os << "\n--- context ---\n";
+  context()->ContextPrint(os);
+
+  os << "\n--- previous context ---\n";
+  context()->previous()->ContextPrint(os);
 }

 void SharedFunctionInfo::PrintSourceCode(std::ostream& os) {

サンプルコードの最後に%DebugPrint(counter2);も追加します。

V8をビルドし、改造したデバッグプリントを実行した結果が以下です。ただし関心のない部分を省略しています。

$ ./tools/dev/gm.py x64.release && ./out/x64.release/d8 --allow-natives-syntax ../sample.js

# counter1 (0x3b160004aa55) のデバッグプリント
DebugPrint: 0x3b160004aa55: [Function]

# counter1のcontext
--- context ---
0x10580004aa4d: [Context]
 - elements:
           0: 0x3b160019a04d <ScopeInfo FUNCTION_SCOPE>
           1: 0x3b160019a031 <ScriptContext[5]>
           2: 2 # cntの値

# counter1のcontextのprevious
--- previous context ---
0x3b160019a031: [Context] in OldSpace
 - elements:
           0: 0x3b1600199eb1 <ScopeInfo SCRIPT_SCOPE>
           1: 0x3b1600183c79 <NativeContext[284]>
           2: 0x3b160004aa19 <JSFunction makeCounter (sfi = 0x3b1600199ee9)>
           3: 0x3b160004aa55 <JSFunction (sfi = 0x3b160019a07d)> # counter1
           4: 0x3b160004aa85 <JSFunction (sfi = 0x3b160019a07d)> # counter2

# counter2 (0x3b160004aa85) のデバッグプリント
DebugPrint: 0x3b160004aa85: [Function]

# counter2のcontext
--- context ---
0x3b160004aa71: [Context]
 - elements:
           0: 0x3b160019a04d <ScopeInfo FUNCTION_SCOPE>
           1: 0x3b160019a031 <ScriptContext[5]>
           2: 1 # cntの値

# counter2のcontextのprevious
--- previous context ---
0x3b160019a031: [Context] in OldSpace
 - elements:
           0: 0x3b1600199eb1 <ScopeInfo SCRIPT_SCOPE>
           1: 0x3b1600183c79 <NativeContext[284]>
           2: 0x3b160004aa19 <JSFunction makeCounter (sfi = 0x3b1600199ee9)>
           3: 0x3b160004aa55 <JSFunction (sfi = 0x3b160019a07d)> # counter1
           4: 0x3b160004aa85 <JSFunction (sfi = 0x3b160019a07d)> # counter2

まず、counter1contextを見てみましょう。elementsに着目すると2が表示されています。同様にcounter2の方は1が表示されていることから、これらは束縛されたcntの値であるようです。

次に、counter1counter2それぞれのcontextpreviousを見てみます。すると、どちらも同じ内容が表示されているとわかります。それぞれのelementsには、makeCounterの他に2つのJSFunctionの束縛が表示されており、これらはcounter1counter2にアドレスが一致します。したがって、これらのcontextはグローバル環境に相当するもののようですね。

全体的に、例の図と似た構造があるのを確認できました。

最終的に、すべてのサンプルプログラムの実行が終わった時点の環境の図。詳細はSICPの説明の章を参照。

V8まとめ

アロー関数の評価結果はJSFunctionでした。これは関数([code])と環境([context])を持つためクロージャです。

contextは束縛の集合(elements)と、外側の環境への参照(previous)を持っているため、環境に相当します。

まとめ

SICP、ECMAScript、V8を調べることで、それぞれにクロージャ、環境、束縛に相当する構造があることを確認しました。

そもそもの発端は、会社の新人向けJavaScript研修にサポート講師として参加した際、新人の方からの「クロージャとは何ですか?」という質問に、僕がうまく回答できなかったことでした。

こういう素朴な疑問が、理解を深めるきっかけになったのは良かったです。研修というのは、新人だけでなく講師の知見を広げる場でもあるんだなぁと感じました。

今回のことについてもっと知りたいなら、SICPの4章がおすすめです。環境モデルを含めたメタ循環評価器(ある言語で実装された、その言語自身の評価器)を作ることで理解が深まりますし、非決定性計算や論理プログラミングといった馴染みのないパラダイムに対して、環境モデルがどのように適用されるかというのは、むずかしいですが興味深いと感じました。

JavaScriptのクロージャ周りで困った際には、ぜひ環境の図を描いてみてください。

ありがとうございました。


  1. また、本記事の後半の説明に合わせて、一部の概念を統合したり、言い換えたりしています。

続けて読む…

SICPの備忘録

2023/07/29

Zコンビネータを思いつきたい

2024/01/22

TypeScriptにおける配列の共変性

2022/12/15

Blenderでクマをモデリングする

2016/09/17

SICPの感想文

2023/07/29

Dockerで簡単TeX環境構築

2020/01/07

書いた人

sititou70のアイコン画像
sititou70

都内の社会人エンジニア3年生。Web技術、3DCG、映像制作が好き。