6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

クロージャーの4言語比較(Python, JavaScript, Java, C++)

Last updated at Posted at 2019-05-06

概要

クロージャーといえばJavaScriptという感じだけど、Pythonでクロージャーを使ってみる機会があったので、色々な言語で比較してみる。

間違えている部分があったらコメントにてお教えください!
もちろん間違いがないように努力します。

JavaScriptの場合

function outer(){
    let a = 0;
    let inner = () => {
      console.log(a++);
    };
    return inner;
}

let fn = outer();

>>> fn();
0
>>> fn();
1
>>> fn();
2

innerがaへの参照を保持しているので、変数aがGCに回収されないままである。
シンプルで分かりやすい。
ただ、以下のようにaがouterの引数として渡される場合でもaが保持されることに注意する。

function outer(a){
    let inner = () => {
      console.log(a++);
    };
    return inner;
}

let fn = outer(0);

>>> fn();
0
>>> fn();
1
>>> fn();
2

クロージャーを定義時と異なる場所で実行してみる

結論から言えば、もちろん定義時と異なる場所でも定義時の変数を参照出来る。

nodeでimport文を実行するために.mjsファイルにし、node --experimental-modulesコマンドを用いる。

module.mjs
// export let a = 1;
// aはエクスポートしない
let a = 1;

export function outer(){
  let b = 1000;
  let inner1 = ()=>{
    console.log(b++);
  }
  return inner1;
}

//関数内関数ではない
export function inner2(){
  console.log(a++)
}
closure.mjs
import * as m from "./module.mjs";

let fn = m.outer();

fn();
fn();
fn();
m.inner2();
m.inner2();
m.inner2();
console.log(a)

出力:


$ node --experimental-modules closure.mjs
(node:12980) ExperimentalWarning: The ESM module loader is experimental.
1000
1001
1002
1
2
3
file:///***********/closure.mjs:11
console.log(a)
            ^

ReferenceError: a is not defined

このようにaは定義されていないと出るのに、inner2で参照出来ている。
ということは、JavaScriptでは関数内関数でなくとも、関数はクロージャーになる。

firefox(66.0.3)で実際に実行してみる。

$ cp closure.mjs closure.js
$ cp module.mjs module.js

closure.jsのインポート文をimport * as m from "./module.js";と書き換えておく。

closure.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <script type="module" src="closure.js"></script>
    <title>test</title>
  </head>
  <body>
  </body>
</html>

closure.htmlにfirefoxでアクセスし、ログを確認。

1000
1001
1002
1
2
3
ReferenceError: a is not defined[詳細]

全く同じ結果になった。
Pythonだとグローバル変数のスコープはそのファイル限りであるが、import/export機能を使い、その変数をexportしないのであればJavaScriptでも同じっぽい?(exportしないのだから当然か)

Pythonの場合

def outer():
    a = 0
    def inner():
        nonlocal a
        print(a)
        a += 1
    
    return inner

fn = outer()

>>> fn()
0
>>> fn()
1
>>> fn()
2

Pythonもほぼ同じで分かりやすいが、一つ余分なものが入っている。その名はnonlocal。
一つ外側のスコープの変数を変更しようとする時に必須となる。
ちなみにnonlocalではなくglobal aとすると今度はaはグローバル変数aを参照するようになる。

nonlocal 文は、列挙された識別子がグローバルを除く一つ外側のスコープで先に束縛された変数を参照するようにします。

global 文は、列挙した識別子をグローバル変数として解釈するよう指定することを意味します。

(引用元:Python 言語リファレンス)

a = 111
def outer():
    a = 0
    def inner1():
        # 参照するだけならnonlocal aとしなくてもOK
        print(a)
        # nonlocal aとしないとa+=1でエラーが出る
        # a += 1
    def inner2():
        #global aとするとa = 111の定義を参照する
        global a
        print(a)
    
    return (inner1, inner2)

inner1, inner2 = outer()

>>> inner1()
0
>>> inner2()
111

# 当たり前だがクロージャーから
# 参照しているouter関数内部のa=0は外部からアクセス出来ず、
# この場合global変数であるa=111がprintされる
>>> print(a) 
111

定義時と異なる場所で実行してみる

module.py
a = 1

def outer():
    b = 1000
    def inner1():
        nonlocal b
        print(b)
        b += 1
    return inner1

#inner2は関数内関数ではない
def inner2():
    #global aとしないとエラーが出る
    global a
    print(a)
    a += 1
closure.py
from module import *

inner1 = outer()

inner1()
inner1()
inner1()

inner2()
inner2()
inner2()

出力:

$ python closure.py
1000
1001
1002
1
2
3

JavaScriptと同様にできた!

(inner2でglobal aとしないままエラーを出して、エラーが出るからJavaScriptとは違うと書いていましたが、コメントでご指摘を頂き修正しました。)

Javaの場合

Java(7以前)にはクロージャーがないらしい。関数内で無名クラス(匿名クラス)を使うことによって似たようなことが出来る(ただし制限あり)。
Java8からはラムダ式が導入されたが、それもまたクロージャーではないらしい。
それらの経緯等含めて以下のリンクが詳細かつ非常に読みやすくおすすめ。

パート2はまだ読んでないが一応貼っておく。

以下ではざっくりした説明をしていく。
まずは無名クラスを使う例を試してみる。
Javaでは関数が第一級オブジェクトではないので、代わりに一つの関数のみをメンバーとして持つ無名クラスオブジェクトをouter関数からリターンしてみたらどうなるか、みたいなイメージでいいのだろうか。

interface Inner {
    public void print();
}

public class ClosureTest {

    public Inner outer() {
        //ここはfinal使わないとエラー
        final int a = 0;
        
        return new Inner() {
            public void print() {
                System.out.println(a);
                
                //finalなのでa++できない
            }
        }
    }
    
    public static void main(String[] args) {
		ClosureTest ct = new ClosureTest();
		
		Inner inner = ct.outer();

		inner.print(); 

	}

}

上の例の通り、finalをつけなければならないので、JavaScriptやPythonのようには出来ない。ただし、finalは参照に対してのfinalに過ぎないので、変数aを配列かArrayList等にして、要素の値を実行ごとに変えることは出来る。つまり同じことを実現すること自体は出来る。

次にラムダ式。
ラムダ式の場合は、スコープ外の変数を参照する場合、その変数はfinalにしなくてもいい。しかし、値を変更するとエラーが出る。

public class Closure {
	public static void main(String... args) {
            //無名クラスと違ってfinalでなくともいい!
            int a = 0;
            //しかしa++の部分でエラーが出る
            Runnable r = () -> System.out.println(a++);
	    r.run();
	  } 
}

エラー内容は以下の通り。

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Local variable a defined in an enclosing scope must be final or effectively final

ということは、変数aはfinalか、実質的にfinalでなくてはならないということ。実質的にfinalというのは、上の例のような変更を施さないこと。

つまり、スコープ外の変数を参照する場合の取扱いは、無名クラスとラムダ式で(ほぼ)同じ。

以前、無名クラスやら関数型インターフェースやらラムダ式の表記法やら学んだ当初はなんなんだこれと思っていたが、クロージャーの観点で見ると理解出来てきてくるような気がする。

C++の場合

C++11のラムダ式を使って簡単に書けるようだ。

まず、ラムダ式はこれ。

[](int a, int b) -> int { return a + b; }

以下のように使う。

auto fn = [](int a, int b) -> int { return a + b; }
int c = fn();

「-> int」の部分はこの関数の戻り値の型を示している。次のように省略してもいい。

[](int a, int b) { return a + b; }

[]は後述。

そして、

このラムダ式によって、その場に以下のような関数オブジェクトが定義される:

struct F {
 auto operator()(int a, int b) const -> decltype(a + b)
 {
    return a + b;
 }
};

(引用元:cpprefjp - C++日本語リファレンス)

()をオーバーロードして関数オブジェクトを実現しているのが面白い。
戻り値がautoとdecltype(a+b)の二つになっている理由はここ参照

[]はキャプチャのこと。

ラムダ式には、ラムダ式の外にある自動変数を、ラムダ式内で参照できるようにする「キャプチャ(capture)」という機能がある。キャプチャは、ラムダ導入子(lambda-introducer)と呼ばれる、ラムダ式の先頭にある[ ]ブロックのなかで指定する。

キャプチャには、コピーキャプチャと参照キャプチャがあり、デフォルトでどの方式でキャプチャし、個別の変数をどの方式でキャプチャするかを指定できる。

(引用元:cpprefjp - C++日本語リファレンス)

以下キャプチャの例

#include <iostream>

using namespace std;

int main(){
  int a = 1;
  int b = 2;
  
  //aをコピーキャプチャ
  auto fn1 = [a] () { cout << a << endl; };
  
  //aを参照キャプチャ
  auto fn2 = [&a] () { cout << a << endl; };
  
  //aとbをコピーキャプチャ
  auto fn3 = [=] () { cout << a + b << endl; };
  
  //aとbを参照キャプチャ
  auto fn4 = [&] () { cout << a + b << endl; };
  
  a = 1000;
  b = 2000;
  
  fn1();
  fn2();
  fn3();
  fn4();
}

出力:

1
1000
3
3000

コピーキャプチャの時は以下のようになるんだろうか。詳しい方いたらお教えください。

これは想像上のコードです
struct F {
  //変数名はaとbにはならなそう?
  int a = 1;
  int b = 2;
  auto operator()() const -> decltype(a + b)
  {
     cout << a + b << endl;
  }
};

そして、クロージャー(っぽいもの)はこのように書ける。

#include <iostream>
#include <functional>

std::function<int()> outer()
{
    int a = 0;
    //aをコピーキャプチャ
    auto inner = [a]() mutable -> int {
        return a++;
    };
    return inner;
}

int main()
{
    auto inner = outer()

    std::cout << inner() << std::endl;
    std::cout << inner() << std::endl;
    std::cout << inner() << std::endl;
    return 0;
}

mutableに関しては、

キャプチャした変数はクロージャオブジェクトのメンバ変数と見なされ、クロージャオブジェクトの関数呼び出し演算子は、デフォルトでconst修飾される。そのため、コピーキャプチャした変数をラムダ式のなかで書き換えることはできない。

コピーキャプチャした変数を書き換えたい場合は、ラムダ式のパラメータリストの後ろにmutableと記述する。

(引用元:cpprefjp - C++日本語リファレンス
とのこと。

この例では、外部スコープにあるaをコピーし、それをラムダ式(関数オブジェクト)の中でメンバー変数として保存する。
Javaのようにコピーされた変数aはconst(final)なわけだが、mutableという語句によって変更可能にしてしまう。

もちろん、上の例で

auto inner = [&a]() mutable -> int {}

のように参照キャプチャをするとouter()実行終了時に参照先が解放されてしまうのでコピーキャプチャでなければならない。

おまけ

Pythonで面白いクロージャーの使い方が出来る。
http.serverというライブラリがあり、簡易なwebサーバーをたてられる。
以下のようにして使うのだが、HTTPServer()の第二引数のhdはクラスオブジェクトでなければならない。しかし、hdがクロージャーでもうまくいく。

server = HTTPServer(('', int(port)), hd)
server.serve_forever()

hdがクロージャーの場合:

def handler_wrapper():
    counter = [0]
    def handler(*args):
        counter[0] += 1
        return HandleServer(counter, *args)

    return handler

hd = handler_wrapper()

なぜこのようにしていいのか等含めて時間があったら別記事として書きたい。

まとめ

クロージャーって難しいなあ。

6
2
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?