ウェブ上の記事でクロージャを調べても不十分であったりわかりにくいと感じたので,これを読めば Wikipedia が読めるくらいの内容を目指して書いてみました.
クロージャとは何か
クロージャとは,少し厳密ではありませんが,
「関数の中の関数で,外側の関数の引数やローカル変数を使うもの」
です.
例えば,下のコードの無名関数 (e) => y * z * e
がクロージャです(Scala で書いてしまいましたが,読めると思います).
def example(x: Int, y: Int): List[Int] = {
var z = f(x)
List(0, 1, 2).map((e) => y * z * e)
// 三つの要素を持つリストのすべての要素に対して `y` と `z` をかけている
// この無名関数 `(e) => y * z * e` は外側の関数 `example` の引数 `y` やローカル変数 `z` を使っているのでクロージャ
}
この例ではわからないと思いますが,そのような関数は y
や z
のような変数がどういう値であるかを覚えるという,通常の関数とは異なる処理をする必要があります.
そこで,そのような関数のことを特別扱いし,クロージャと呼んでいるのです.
どういうときに使うか
では,クロージャはどういうときに出てくるか,使うかというと,典型的な例を二つ挙げます.
例1: 関数の引数に無名関数
一つは,上の例のように,高階関数の引数に無名関数を渡すときです.
その無名関数の本体に外側の関数の引数やローカル変数を使っているとき,その無名関数はクロージャとして処理されています.
これは,無意識に使ったことがあるかもしれません.
例2: 状態を持った関数を作る
もう一つは,例えばカウンタ(呼び出された回数を返す関数)のような,呼び出すたびに違う振る舞いをする関数を作りたいときです.
そういうとき,普通はグローバル変数を使うかそれ用のクラスを作ると思いますが,クロージャを使うともっと綺麗に実装することができます.
カウンタの例だと,下のコードのように書きます.
var count = (() => {
var x = 0
() => {x += 1; println(x)}
})() // 無名関数の実行結果を変数 count に代入している
count() // 1 が出力される
count() // 2 が出力される
これは何してるかというと,
内側の無名関数 () => {x += 1; println(x)}
がクロージャです.
そして,外側の無名関数はクロージャを返す関数なので,最終的に変数 count
にはその実行結果であるクロージャが入っています.
ここで,クロージャの特徴として,関数の呼び出しが終わった後も x
の値は保存されます.
なので,最後の行では期待通り,2 が出力されます.
もう少し実用的な例としてはこちらの二回注文を押されると警告を出すボタンが面白いと思いました.
グローバル変数やクラスではダメなのか
このような関数はグローバル変数やクラスでも実装できます.
では,クロージャを使うと何が良いかというと,
まず,グローバル変数を使う方法と比べるとグローバル変数の数を減らせます.
グローバル変数がたくさんあると名前が衝突するなどバグになる可能性があり,減らせると嬉しいです.
また,クラス作る方法と比べると,クロージャを使う方が簡単かと思います.
レキシカルスコープとクロージャ
クロージャをどういうときに使うかを説明しましたが,元々クロージャはレキシカルスコープというものを実現するためのものです.
自由変数
ある関数に対して,引数やローカル変数以外の変数,外で定義された変数のことを自由変数といいます.
スコープ
自由変数の扱い方には,レキシカルスコープとダイナミックスコープという二つの方法があります.
- レキシカルスコープ: 自由変数が出てきたときは,定義されたときの変数を参照する.下の例では 1 (1 行目で定義された方の
x
.5 行目で1
が再代入されている)を出力する. - ダイナミックスコープ: 自由変数が出てきたときは,実行時のその名前の変数を参照する.下の例では 2 (8 行目(
main
関数内)で定義された方のx
)を出力する.
var x = 0
def print_x() = print(x) // この `x` が自由変数
x = 1
def main() = {
var x = 2 // この `x` は上の `x` とは別物
print_x()
}
この二つのどちらを採用するかはプログラミング言語によって異なりますが,普通はレキシカルスコープが採用されます.
これは,Wikipedia によるとミスを招きやすいためだそうです.
確かに,一番最初の例で y
や z
は明らかに外側のローカル変数を参照しており,違う変数の値を使われてしまったら困ると思います.
ダイナミックスコープでは,名前でどの変数を使えばいいか判別できますが,レキシカルスコープを採用するとどの変数を使えばいいか関数ごとに覚えておかなければいけません.
そこで,クロージャが必要になります.
環境
では,どのように覚えているかというと,コンパイラなどの言語処理系内部では,クロージャは関数本体と環境というもののペアとして処理されます.
環境とはそれぞれの自由変数がどの変数を参照しているか(自由変数の名前とポインタの対応)です.
ただ,グローバル変数やクラス変数,インスタンス変数などは特別扱いされるので,それらを使うだけではクロージャの仕組みはいりません.
必要になるのは関数の中に関数を使えるなど,入れ子のような仕組みを備えているときです.
参考
Closure (computer programming) - Wikipedia
クロージャ再考 - Qiita
クロージャって結局何なのか - Qiita
[JavaScript] 猿でもわかるクロージャ超入門 まとめ · DQNEO起業日記