2019年11月3日日曜日

【C++、JavaScript】クロージャーについて

C++のラムダ式とJavaScriptの無名関数・匿名関数が似ているなと感じた。
調べてみると、クロージャーという機能の実装として共通点が多いことが分かった。

ポイント


C++のラムダ式とJavaScriptの無名関数(匿名関数)は、どちらもクロージャーを実現することのできる言語機能であるという点で似ている


まずは言葉について整理する。

  • クロージャー
    関数宣言の中に別の関数宣言を書ける機能、あるいはローカル変数を参照している関数内変数の機能である[1]。
  • クロージャー型
    生成される関数オブジェクトの型の名前である
  • クロージャーオブジェクト
    クロージャー型の関数オブジェクトである。

C++では「ラムダ式」という記述方法をとることで、クロージャーを実装することができる。
C++でクロージャーでは、通常の関数定義では不可能なクロージャー外部の変数を保存しておくことができる。この、クロージャー外部の変数を保存する機能のことを「キャプチャ」と呼ぶ。

一方で、JavaScriptでは「アロー」表記で関数を宣言する。
JavaScriptの関数は宣言された時点では名前を持たないため、無名関数(匿名関数)と呼ばれる。JavaScriptではクロージャーを含むスコープ内でvarで宣言された変数はクロージャー内部からもアクセスでき、これによりキャプチャが実現できる。

C++のクロージャーでは、変数へのアクセス方法をある程度細かく指定することができる


C++のクロージャーの基本的なシンタックスを以下に示す。
  1. [....ラムダ指定...]{...関数の本体...}
[]は「ラムダ導入子」といい、ここにクロージャーがスコープ内部の変数にどのようにアクセスするかを記述する。
アクセスの種類には、大きく分けて「コピー」、「参照」、「this」参照がある。
変数を「コピー」しておけば、クロージャーの定義元が消滅しても値を保持することができる。一方で、「参照」でアクセスすることを指定した変数は、クロージャーの定義元が消失した後にアクセスした場合の動作が未定義である。

JavaScriptのクロージャーでは、C++ほど直接的に変数へのアクセス設定をできない


JavaScriptのクロージャーの基本的なシンタックスを以下に示す[3]。
  1. (引数,...)=>{...関数の本体...}
C++では、ラムダ導入子により変数へのアクセス方法を詳しく指定できたが、JavaScriptでは、そのような詳細なアクセス指定の方法は調べた範囲では見つからなかった。
しかしながら、逆に考えると、クロージャーを用いて変数を掩蔽することができるといえる。

開発・実行環境


ブラウザ Google Chrome
バージョン: 78.0.3904.70(Official Build) (64 ビット)
起動時オプション:
--flag-switches-begin --flag-switches-end

PC
プロセッサ: Intel(R) Core(TM) i5-5200U CPU @2.20GHz
メモリ: 8.00 GB
OS: Windows 8.1 (64bit)
compiler: Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Visual Studio: 14.0

コード


main.cc

  1. #include <stdio.h>
  2. #include <functional>
  3.  
  4. #define show(EXPRESSION) \
  5. { \
  6. printf("%s\n", #EXPRESSION); \
  7. printf("%d\n", EXPRESSION); \
  8. printf("\n"); \
  9. }
  10.  
  11. std::function<int(int)> CreateAdd(int add) {
  12. return [add](int i) { return i + add; }; // Use copy of add.
  13. }
  14.  
  15. std::function<int(int)> CreateAddRef(int add) {
  16. return [&add](int i) { return i + add; }; // Use reference to add.
  17. }
  18.  
  19. int main(int argc, char* argv[]) {
  20. (void)argc;
  21. (void)argv;
  22.  
  23. auto add1 = CreateAdd(1);
  24. auto add2 = CreateAdd(2);
  25. show(add1(1))
  26. show(add2(1))
  27.  
  28. auto addref1 = CreateAddRef(1);
  29. auto addref2 = CreateAddRef(2);
  30. show(addref1(1)) // Behavior is undefined.
  31. show(addref2(1)) // Behavior is undefined.
  32.  
  33. return 0;
  34. }
  35.  
C++におけるクロージャーの実装例である。
  1. std::function<int(int)> CreateAdd(int add) {
  2. return [add](int i) { return i + add; }; // Use copy of add.
  3. }
この関数はクロージャーオブジェクトを生成する関数である。
生成されたクロージャーオブジェクトは、引数にある数を受け取り、クロージャー生成関数に渡した数を加える機能を持つ。
  1. auto add1 = CreateAdd(1);
  2. auto add2 = CreateAdd(2);
  3. show(add1(1))
  4. show(add2(1))
クロージャー生成関数に渡された引数は、この関数が消滅した後もクロージャーから利用することができる。なぜなら、消滅してしまったクロージャー生成関数の引数の値をコピーしてあるからだ。

  1. std::function<int(int)> CreateAddRef(int add) {
  2. return [&add](int i) { return i + add; }; // Use reference to add.
  3. }
この関数もクロージャーオブジェクトを生成する関数である。
先ほどのクロージャー生成関数と異なる点は、ラムダ指定子で変数へのアクセス方法を参照としている点だ。
  1. auto addref1 = CreateAddRef(1);
  2. auto addref2 = CreateAddRef(2);
  3. show(addref1(1)) // Behavior is undefined.
  4. show(addref2(1)) // Behavior is undefined.
このように、クロージャー生成関数が消滅した後に、クロージャーで参照するクロージャー生成関数の引数にアクセスすることは、動作が未定義であるため、してはいけない。
(一応動いたけど、わけの分からん値が表示された)

ここら辺のことについては、こちらの質問と回答に詳しい[4]。

test.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charser="utf-8" />
  5. <title>count</title>
  6. <link rel="manifest" href="/manifest.json">
  7. <script type="text/javascript">
  8. function createCounter() {
  9. var count = 0;
  10. return function() {
  11. let elem = document.getElementById("id_count");
  12. elem.innerText = count;
  13. ++count;
  14. }
  15. }
  16. var counter = createCounter();
  17. // console.log(count); // Error... 'count' cannot be seen from here.
  18. </script>
  19. </head>
  20. <body>
  21. <input type="button" value="Increment" onclick="counter();"><br />
  22. count: <span id="id_count"></span><br />
  23. </body>
  24. </html>
  25.  
JavaScriptにおけるクロージャーの実装例である。
  1. function createCounter() {
  2. var count = 0;
  3. return function() {
  4. let elem = document.getElementById("id_count");
  5. elem.innerText = count;
  6. ++count;
  7. }
  8. }
この部分の記述はクロージャーオブジェクトを生成する関数リテラルである。
  1. var counter = createCounter();
変数counterにクロージャーオブジェクトを代入する。
  1. <input type="button" value="Increment" onclick="counter();"><br />
ボタンをクリックした際に、クロージャー生成関数内部のスコープにある変数varへアクセスし、ドキュメントにカウンタの値を表示し、カウンタの値を増加させる。
  1. // console.log(count); // Error... 'count' cannot be seen from here.
クロージャー生成関数とクロージャーの外側からカウンタにアクセスすることはできない。

実行結果



count:

test.htmlの一部を埋め込んだ。

まとめ



  • クロージャーオブジェクトと通常の関数宣言の違いは、スコープ内部変数のコピーや参照をキャプチャしたクロージャーオブジェクトを生成することができる点である
  • C++、JavaScriptのいずれもクロージャーの実装をすることができる
  • C++では変数へのアクセス方法の詳細な指定ができる一方で、JavaScriptではC++ほど詳細なアクセス指定の仕組みが無いと思われる(調べた範囲では)。逆に考えると、JavaScriptではクロージャーを用いることにより変数の掩蔽をすることができるといえる

感想


ラムダ式というと関数型プログラミングのイメージが強い(関数型はhaskellでハローワールドしたことのある程度の初心者)。
一般的なラムダ式に関する理解は足りていないと思うが、少なくともC++では、ラムダ式よりもクロージャーとして考えた方が理解しやすいかもしれないと思った。

付録


今回のコードの最新版(C++の方)をここに上げる。

参考文献


  1. JavaScriptでクロージャー
  2. ラムダ式
  3. JavaScript アロー関数を説明するよ
  4. ラムダ式って何が便利なのです??

0 件のコメント:

コメントを投稿

コメント表示は承認制に設定しています