以前Javaを書いていた時、フロントエンドの同僚たちがクロージャについて話しているのを聞いて、とても新鮮に感じました。その後、少しJavaScriptを書いてみましたが、基礎的な知識しかありませんでしたが、クロージャの概念を理解することができました。現在、私の仕事でよく使う言語はGoで、多くのエレガントなコードの中にクロージャの姿があります。クロージャを深く理解しないと、この世界に入ることはできません。この記事では、クロージャについて解説します。
1、クロージャとは何か?
クロージャについて詳しく説明する前に、いくつかの基礎知識を補足しておきましょう:
- 関数型プログラミング
- 関数のスコープ
- スコープの継承関係
1.1 前提知識の補足
1.1.1 関数型プログラミング
関数型プログラミングは、問題を解決するためのアプローチの一つです。小さな関数を組み合わせて大きな関数を作り上げる考え方です。関数の引数も関数であり、戻り値も関数になります。一般的なプログラミングパラダイムには以下のようなものがあります:
- 命令型プログラミング:
- 主な思想:コンピュータが実行するステップに焦点を当て、コンピュータに何をすべきか一歩一歩指示します。
- 問題解決のステップを標準化し、ある種のアルゴリズムとして抽象化し、具体的なアルゴリズムを実装します。通常、プロセス指向プログラミングパラダイムをサポートする言語は、BASIC、Cなどプロセス指向プログラミング言語と呼ばれます。
- 宣言型プログラミング:
- 主な思想:コンピュータに何をすべきかを伝えますが、具体的にどうするかは指定しません。例えば、SQL、WebプログラミングのHTML、CSSなどがこれにあたります。
- 関数型プログラミング:
- どうするかではなく、何をするかだけに焦点を当てます。宣言型プログラミングの影響を受けつつ、「関数が第一級である」という原則をより強調します。関数は引数、変数、戻り値など、どこにでも現れることができます。
関数型プログラミングはオブジェクト指向プログラミングの対極にあると考えられます。通常、特定のプログラミング方式を強調する言語は限られており、ほとんどの言語はマルチパラダイム言語で、JavaScriptやGoのように複数の異なるプログラミング方式をサポートしています。
関数型プログラミングは思考方法の一つで、コンピュータの計算を関数の計算と見なすアプローチです。実際には、関数型プログラミングについて話し、その後クロージャについて説明すべきです。なぜなら、クロージャ自体が関数型プログラミングの特徴の一つだからです。
関数型プログラミングにおいて、関数は第一級オブジェクトであり、他の関数の入力パラメータ値として使用したり、関数から戻り値として返されたり、変更されたり、変数に割り当てられたりすることができます。(ウィキペディア)
純粋な関数型プログラミング言語は、通常、プログラムの状態や可変オブジェクトを直接使用することは許可されていません。関数型プログラミングの目的は、共有状態や可変状態の使用を避け、副作用の発生を可能な限り減らすことです。
関数型プログラミングには一般的に以下の特徴があります:
- 関数は第一級市民:関数は第一級の地位を持ち、引数として渡したり、代入したり、渡したり、戻り値として返したりできます。
- 副作用なし:関数は純粋で独立した状態を維持し、外部変数の値を変更したり、外部状態を変更したりしてはいけません。
- 参照透過性:関数の実行は外部変数や状態に依存せず、同じ入力パラメータであれば、いつでも同じ戻り値を返すべきです。
1.1.2 関数のスコープ
スコープ(scope)はプログラム設計の概念です。一般的に、プログラムコードで使われる名前は常に有効/利用可能なわけではなく、その名前の利用可能性を制限するコードの範囲がその名前のスコープです。
分かりやすく言えば、関数のスコープとは関数が作用できる範囲です。関数は箱のように重なり合っており、スコープは閉じた箱、つまり関数のローカル変数のように、箱の内部でのみ使用され、独立したスコープとなります。
関数内のローカル変数は、関数の外に出るとスコープから外れ、その変数を見つけることができません(内側の関数では外側の関数のローカル変数を使用できます。なぜなら、外側の関数のスコープには内側の関数が含まれているからです)。例えば、以下の例ではinnerTempは関数のスコープから外れるとその変数を見つけることができませんが、outerTempは内側の関数内で依然として使用できます。
どの言語でも、基本的なメモリリサイクル機構が存在し、使用されないメモリ領域を回収します。回収のメカニズムは、上記で説明した関数のスコープに関連していることが多いです。ローカル変数はそのスコープから外れると、回収の対象になる可能性があります。しかし、参照されている場合は回収されません。
1.1.3 スコープの継承関係
スコープ継承とは、前に説明した小さな箱が外側の大きな箱のスコープを継承できるということです。小さな箱では外側の大きな箱のものを直接取り出すことができますが、大きな箱では小さな箱のものを取り出すことはできません(逃逸(エスケープ)が発生しない限り)。一般的に、変数のスコープには以下の2種類があります:
- グローバルスコープ:どこでも作用します
- ローカルスコープ:一般的にコードブロック、関数、パッケージ内です。関数内で宣言/定義された変数をローカル変数と呼び、スコープは関数内に限定されます。
1.2 クロージャの定義
「多くの場合、私たちはまず理解してから定義するのではなく、まず定義してから理解します」。まず定義します。読めなくても大丈夫です:
クロージャ(closure)は関数とその束縛された周辺環境状態(レキシカル環境、lexical environment)への参照の組み合わせです。言い換えれば、クロージャは開発者が内部関数から外部関数のスコープにアクセスできるようにします。クロージャは関数の作成と同時に作成されます。
一言で表現すると:
$$
クロージャ = 関数 + 参照環境
$$
上記の定義にはGo言語という言葉が見当たりません。賢い読者なら、クロージャは言語に依存しないものであり、JavaScript特有のものでもGo特有のものでもないことに気づくでしょう。クロージャは関数型プログラミング言語に特有のものです。その通りです、間違っていません。関数型プログラミングをサポートするすべての言語がクロージャをサポートしており、GoとJavaScriptがその2つです。現在のJavaバージョンもクロージャをサポートしていますが、一部の人々は完全なクロージャではないと考えています。詳細については、本文で議論します。
1.3 クロージャの書き方
1.3.1 クロージャの初歩的な見方
以下はクロージャのコード例です:
import "fmt"
func main() {
calcFunc := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc())
}
func delayedCalc(numbers []int) func() int {
fmt.Println("まず関数を取得しますが、結果は計算しません")
var calculate = func() int {
fmt.Println("結果を計算中...")
total := 0
for _, num := range numbers {
total = total + num
}
return total
}
return calculate
}
出力結果:
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
結果を計算中...
結果: 150
このコードから、内側のcalculate()メソッドは外部関数delayedCalc()のパラメータとローカル変数を参照できることがわかります。delayedCalc()が関数calculate()を返すとき、関連するパラメータと変数は返された関数に保存され、後で呼び出すことができます。
上記の関数はさらに進んで、関数とその周囲の状態を束縛していることを示すことができます。回数counterを追加してみましょう:
import "fmt"
func main() {
calcFunc := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc())
fmt.Println("結果:", calcFunc())
fmt.Println("結果:", calcFunc())
}
func delayedCalc(numbers []int) func() int {
fmt.Println("まず関数を取得しますが、結果は計算しません")
counter := 0
var calculate = func() int {
counter++
fmt.Println(counter, "回目の結果計算中...")
total := 0
for _, num := range numbers {
total = total + num
}
return total
}
return calculate
}
上記のコードの出力は何でしょうか?回数counterは変化するでしょうか?counterは明らかに外側の関数のローカル変数ですが、内側の関数で参照(束縛)されています。内側の関数が外部に露出されているため、実行結果は以下のようになります:
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
1 回目の結果計算中...
結果: 150
2 回目の結果計算中...
結果: 150
3 回目の結果計算中...
結果: 150
結果はcounterが実際には毎回変化することです。この状況をまとめると:
- 関数体内に別の関数がネストされており、戻り値が関数である。
- 内側の関数が外部に露出され、外側の関数の外側から参照されている。これがクロージャを形成する。
ここで誰かが疑問を持つかもしれません。前の例ではdelayedCalc()が1回作成され、3回実行されましたが、もしそれが3回の実行すべてで異なる作成であった場合はどうなるでしょうか?実験してみましょう:
import "fmt"
func main() {
calcFunc := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc())
calcFunc1 := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc1())
calcFunc2 := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc2())
}
func delayedCalc(numbers []int) func() int {
fmt.Println("まず関数を取得しますが、結果は計算しません")
counter := 0
var calculate = func() int {
counter++
fmt.Println(counter, "回目の結果計算中...")
total := 0
for _, num := range numbers {
total = total + num
}
return total
}
return calculate
}
実行結果は以下のようになります。各実行はすべて1回目です:
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
1 回目の結果計算中...
結果: 150
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
1 回目の結果計算中...
結果: 150
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
1 回目の結果計算中...
結果: 150
上記の実行結果からわかるように:
クロージャが作成される際、参照される外部変数counterは1つ作成されます。つまり、それぞれの呼び出しは関係ありません。
さらに問題を投げかけます。**もし関数が2つの関数を返した場合、これは1つのクロージャでしょうか、それとも2つのクロージャでしょうか?**以下で実践してみましょう:
1回で2つの関数を返し、1つは合計を計算し、もう1つは積を計算します:
import "fmt"
func main() {
sumFunc, productFunc := delayedCalculation([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("合計:", sumFunc())
fmt.Println("積:", productFunc())
}
func delayedCalculation(numbers []int) (func() int, func() int) {
fmt.Println("まず関数を取得しますが、結果は計算しません")
counter := 0
var sum = func() int {
counter++
fmt.Println(counter, "回目の合計計算中...")
result := 0
for _, num := range numbers {
result = result + num
}
return result
}
var product = func() int {
counter++
fmt.Println(counter, "回目の積計算中...")
result := 1
for _, num := range numbers {
result = result * num
}
return result
}
return sum, product
}
実行結果は以下のようになります:
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
1 回目の合計計算中...
合計: 150
2 回目の積計算中...
積: 12000000
上記の結果からわかるように、クロージャは関数が関数を返す際に、戻り値(関数)がいくつあっても1回のクロージャです。返された関数が外部関数の変数を使用している場合、それらは一緒に束縛され、相互に影響を与えます:
クロージャは周囲の状態を束縛しており、関数が状態を持つことができると理解しています。この場合、関数はオブジェクトが持つすべての能力を備えています。関数は状態を持つようになりました。
1.3.2 クロージャにおけるポインタと値
上記の例では、クロージャで使用されているのはすべて数値でした。もしポインタを渡した場合はどうなるでしょうか?
import "fmt"
func main() {
i := 0
testFunc := testPointer(&i)
testFunc()
fmt.Printf("外部の i = %d\n", i)
}
func testPointer(i *int) func() {
*i = *i + 1
fmt.Printf("test内部の i = %d\n", *i)
return func() {
*i = *i + 1
fmt.Printf("func内部の i = %d\n", *i)
}
}
実行結果は以下のようになります:
test内部の i = 1
func内部の i = 2
外部の i = 2
ポインタの場合、クロージャ内でポインタが指すアドレスの値を変更すると、クロージャ外の値にも影響を与えることがわかります。これはGoでは参照渡しがなく、値渡ししかないためです。ポインタを渡す場合も値渡しであり、ここでの値はポインタの値(アドレス値と考えることができます)です。
関数の引数がポインタの場合、引数はそのポインタアドレスのコピーを作成し、引数として渡されます。本質はアドレスであるため、内部で変更を行っても外部に影響を与えることができます。
クロージャ内のデータのアドレスも同じであることを、以下の実験で証明できます:
func main() {
i := 0
testFunc := testPointer(&i)
testFunc()
fmt.Printf("外部の i のアドレス %v\n", &i)
}
func testPointer(i *int) func() {
*i = *i + 1
fmt.Printf("test内部の i のアドレス %v\n", i)
return func() {
*i = *i + 1
fmt.Printf("func内部の i のアドレス %v\n", i)
}
}
出力は以下のようです。したがって、クロージャが外部環境のポインタデータを参照する場合、ポインタアドレスデータのコピーのみが作成され、実際のデータのコピーではないと推測できます(まず、コピーのタイミングは何時なのかという問題を残しておきます):
test内部の i のアドレス 0xc0003fab98
func内部の i のアドレス 0xc0003fab98
外部の i のアドレス 0xc0003fab98
1.3.3 クロージャの遅延束縛
上記の例は、クロージャが作成されたときにデータがすでにコピーされているかのように見えますが、本当にそうなのでしょうか?
以下は前の実験の続きです:
func main() {
i := 0
testFunc := testPointer(&i)
i = i + 100
fmt.Printf("testFunc実行前の外部の i = %d\n", i)
testFunc()
fmt.Printf("testFunc実行後の外部の i = %d\n", i)
}
func testPointer(i *int) func() {
*i = *i + 1
fmt.Printf("test内部の i = %d\n", *i)
return func() {
*i = *i + 1
fmt.Printf("func内部の i = %d\n", *i)
}
}
クロージャを作成した後、データを変更し、その後クロージャを実行します。答えは、クロージャの実行に実際に影響を与えることです。なぜなら、それらはすべてポインタであり、同じデータを指しているからです:
test内部の i = 1
testFunc実行前の外部の i = 101
func内部の i = 102
testFunc実行後の外部の i = 102
別の書き方に変えて、クロージャ関数を宣言した後、外部環境の変数を変更してみましょう:
import "fmt"
func main() {
calcFunc := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc())
}
func delayedCalc(numbers []int) func() int {
fmt.Println("まず関数を取得しますが、結果は計算しません")
counter := 0
var calculate = func() int {
fmt.Println(counter, "回目の結果計算中...")
result := 0
for _, num := range numbers {
result = result + num
}
return result
}
counter = counter + 100
return calculate
}
実際の実行結果では、counterは変更後の値になります:
しばらくお待ちください
100 回目の結果計算中...
結果: 150
これは、クロージャがvar calculate = func() int {...}という文を宣言した後、外部環境のcounterをクロージャに束縛するのではなく、関数がクロージャ関数を返すときに束縛する、つまり遅延束縛であることを証明しています。
まだ理解できない場合、さらに一つの例を挙げます:
func main() {
funcs := createFunctions(100)
for _, v := range funcs {
v()
}
}
func createFunctions(x int) []func() {
var funcs []func()
values := []int{1, 2, 3}
for _, val := range values {
funcs = append(funcs, func() {
fmt.Printf("createFunctionsの val = %d\n", x+val)
})
}
return funcs
}
上記の例では、クロージャが関数の配列を返しています。意図は各valが異なる値になることですが、実際にはvalはすべて同じ値です(つまり、return funcsの時点(または実際にクロージャ関数を実行する時点)にvalの値が束縛される)。この時点でvalの値は最後の3であり、最終的な出力結果はすべて103になります:
createFunctionsの val = 103
createFunctionsの val = 103
createFunctionsの val = 103
上記の2つの例は、クロージャの遅延束縛の問題によるものです。これも特徴の一つと言えます。ここで、多くの読者がクロージャが外部変数を束縛するタイミングに疑問を持つかもしれません。クロージャ関数を返すときに束縛されるのでしょうか?それとも実際にクロージャ関数を実行するときに束縛されるのでしょうか?
以下の例はこの質問に効果的に答えます:
import (
"fmt"
"time"
)
func main() {
calcFunc := delayedCalc([]int{10, 20, 30, 40, 50})
fmt.Println("しばらくお待ちください")
fmt.Println("結果:", calcFunc())
time.Sleep(time.Duration(3) * time.Second)
fmt.Println("結果:", calcFunc())
}
func delayedCalc(numbers []int) func() int {
fmt.Println("まず関数を取得しますが、結果は計算しません")
counter := 0
var calculate = func() int {
counter++
fmt.Println(counter, "回目の結果計算中...")
result := 0
for _, num := range numbers {
result = result + num
}
return result
}
go func() {
time.Sleep(time.Duration(1) * time.Second)
counter = counter + 100
fmt.Println("go funcで変更された変数 counter:", counter)
}()
return calculate
}
出力結果は以下のようになります:
まず関数を取得しますが、結果は計算しません
しばらくお待ちください
1 回目の結果計算中...
結果: 150
go funcで変更された変数 counter: 101
2 回目の結果計算中...
結果: 150
2回目にクロージャ関数を実行したとき、明らかにcounterが内側のgo func()で変更されています。つまり、呼び出しのときに、実際に最新の外部環境を取得しますが、宣言のときには環境を予約して保存します。
本質的に、Goルーチンの匿名関数の遅延束縛は、クロージャの遅延束縛そのものです。上記の例では、go func(){}が取得しているのは初期値0ではなく、最新の値です。
上記の検証ポイントをまとめます:
- クロージャは毎回新しいインスタンスを返し、各インスタンスには独自の環境があります。
- 同じインスタンスを複数回実行すると、同じ環境が使用されます。
- クロージャが逃げ出す(外部に露出する)のがポインタの場合、相互に影響を与えます。なぜなら、束縛されているのはポインタであり、同じポインタの内容を変更すると相互に影響を与えるからです。
- クロージャは宣言時に値を束縛するのではなく、宣言後は外部環境を予約して保存します。実際にクロージャ関数を実行するときに、最新の外部環境の値を取得します(遅延束縛とも呼ばれます)。
- Goルーチンの匿名関数の遅延束縛は、本質的にクロージャの遅延束縛です。
2、クロージャの利点と欠点は?
2.1 利点
純粋な関数は状態を持たず、クロージャは関数が容易に状態を持つことができます。しかし、物事には両面があり、状態を持つと、複数回の呼び出しで異なる結果が出る可能性があります。これは前にテストしたケースと同じです。では問題があります:
Q:もしクロージャをサポートしない場合、関数に状態を持たせたい場合はどうすればよいですか?
A: グローバル変数を使用し、すべての関数で同じ変数を共有する必要があります。
しかし、グローバル変数には以下のような特徴があります(異なるシナリオでは利点が欠点になることがあります):
- メモリに常駐しており、プログラムが停止しない限りメモリに残り続けます。
- グローバルを汚染し、誰でもアクセスでき、共有しながら誰がこの変数を変更するか分かりません。
クロージャはこの問題をある程度解決できます:
- グローバル変数を使用する必要がなく、外部関数のローカル変数はクロージャの際に1つ作成されます。ライフサイクルは関数のライフサイクルと一致し、クロージャ関数が参照されなくなったら回収できます。
- クロージャが露出するローカル変数は、外部から直接アクセスできず、関数を介して操作する必要があるため、乱用を避けることができます。
上記の利点に加え、JavaScriptのように、ネイティブでプライベートメソッドをサポートしていない場合、クロージャでプライベートメソッドをシミュレートできます。なぜなら、クロージャはそれぞれ独自のレキシカル環境を持っているからです。
2.2 欠点
関数が状態を持つと、クロージャ内の変数が誤って変更される可能性がありますが、これはコーディング者が考慮すべき問題であり、想定内のシナリオです。
クロージャを随意に作成し、参照が保持されると、破棄できなくなり、クロージャ内のローカル変数も破棄できません。クロージャを過度に使用すると、より多くのメモリを占有し、パフォーマンスが低下します。一般的に、クロージャのローカル変数データを共有し、クロージャ関数を複数回作成する必要がない場合、よりエレガントな方法です。
3、クロージャはどのように実装されている?
上記の実験から、クロージャは実際には外部環境の逃げ出し(escape)であり、クロージャ関数と一緒に外部に露出されていることがわかります。
以下のプログラムを分析します:
import "fmt"
func testFunction(i int) func() int {
i = i * 2
testFunc := func() int {
i++
return i
}
i = i * 2
return testFunc
}
func main() {
test := testFunction(1)
fmt.Println(test())
}
実行結果は以下のようになります:
5
逃げ出し分析(escape analysis)を確認するには、以下のコマンドラインを使用できます:
go build --gcflags=-m main.go
変数iがヒープに移動したことがわかります。つまり、本来はローカル変数ですが、逃げ出し(escape)が発生したため、スタックからヒープに移動しました。同様に、test()関数はクロージャ関数であるため、ヒープに逃げ出しています。
次に、アセンブリコードを見てみましょう:
go tool compile -N -l -S main.go
生成されたコードは長いため、一部を切り取ります:
"".testFunction STEXT size=218 args=0x8 locals=0x38 funcid=0x0 align=0x0
0x0000 00000 (main.go:5) TEXT "".testFunction(SB), ABIInternal, $56-8
0x0000 00000 (main.go:5) CMPQ SP, 16(R14)
0x0004 00004 (main.go:5) PCDATA $0, $-2
0x0004 00004 (main.go:5) JLS 198
0x000a 00010 (main.go:5) PCDATA $0, $-1
0x000a 00010 (main.go:5) SUBQ $56, SP
0x000e 00014 (main.go:5) MOVQ BP, 48(SP)
0x0013 00019 (main.go:5) LEAQ 48(SP), BP
0x0018 00024 (main.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0018 00024 (main.go:5) FUNCDATA $1, gclocals·d571c0f6cf0af59df28f76498f639cf2(SB)
0x0018 00024 (main.go:5) FUNCDATA $5, "".testFunction.arginfo1(SB)
0x0018 00024 (main.go:5) MOVQ AX, "".i+64(SP)
0x001d 00029 (main.go:5) MOVQ $0, "".~r0+16(SP)
0x0026 00038 (main.go:5) LEAQ type.int(SB), AX
0x002d 00045 (main.go:5) PCDATA $1, $0
0x002d 00045 (main.go:5) CALL runtime.newobject(SB)
0x0032 00050 (main.go:5) MOVQ AX, "".&i+40(SP)
0x0037 00055 (main.go:5) MOVQ "".i+64(SP), CX
0x003c 00060 (main.go:5) MOVQ CX, (AX)
0x003f 00063 (main.go:6) MOVQ "".&i+40(SP), CX
0x0044 00068 (main.go:6) MOVQ "".&i+40(SP), DX
0x0049 00073 (main.go:6) MOVQ (DX), DX
0x004c 00076 (main.go:6) SHLQ $1, DX
0x004f 00079 (main.go:6) MOVQ DX, (CX)
0x0052 00082 (main.go:7) LEAQ type.noalg.struct { F uintptr; "".i *int }(SB), AX
0x0059 00089 (main.go:7) PCDATA $1, $1
0x0059 00089 (main.go:7) CALL runtime.newobject(SB)
0x005e 00094 (main.go:7) MOVQ AX, ""..autotmp_3+32(SP)
0x0063 00099 (main.go:7) LEAQ "".testFunction.func1(SB), CX
0x006a 00106 (main.go:7) MOVQ CX, (AX)
0x006d 00109 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x0072 00114 (main.go:7) TESTB AL, (CX)
0x0074 00116 (main.go:7) MOVQ "".&i+40(SP), DX
0x0079 00121 (main.go:7) LEAQ 8(CX), DI
0x007d 00125 (main.go:7) PCDATA $0, $-2
0x007d 00125 (main.go:7) CMPL runtime.writeBarrier(SB), $0
0x0084 00132 (main.go:7) JEQ 136
0x0086 00134 (main.go:7) JMP 142
0x0088 00136 (main.go:7) MOVQ DX, 8(CX)
0x008c 00140 (main.go:7) JMP 149
0x008e 00142 (main.go:7) CALL runtime.gcWriteBarrierDX(SB)
0x0093 00147 (main.go:7) JMP 149
0x0095 00149 (main.go:7) PCDATA $0, $-1
0x0095 00149 (main.go:7) MOVQ ""..autotmp_3+32(SP), CX
0x009a 00154 (main.go:7) MOVQ CX, "".testFunction+24(SP)
0x009f 00159 (main.go:11) MOVQ "".&i+40(SP), CX
0x00a4 00164 (main.go:11) MOVQ "".&i+40(SP), DX
0x00a9 00169 (main.go:11) MOVQ (DX), DX
0x00ac 00172 (main.go:11) SHLQ $1, DX
0x00af 00175 (main.go:11) MOVQ DX, (CX)
0x00b2 00178 (main.go:12) MOVQ "".testFunction+24(SP), AX
0x00b7 00183 (main.go:12) MOVQ AX, "".~r0+16(SP)
0x00bc 00188 (main.go:12) MOVQ 48(SP), BP
0x00c1 00193 (main.go:12) ADDQ $56, SP
0x00c5 00197 (main.go:12) RET
0x00c6 00198 (main.go:12) NOP
0x00c6 00198 (main.go:5) PCDATA $1, $-1
0x00c6 00198 (main.go:5) PCDATA $0, $-2
0x00c6 00198 (main.go:5) MOVQ AX, 8(SP)
0x00cb 00203 (main.go:5) CALL runtime.morestack_noctxt(SB)
0x00d0 00208 (main.go:5) MOVQ 8(SP), AX
0x00d5 00213 (main.go:5) PCDATA $0, $-1
0x00d5 00213 (main.go:5) JMP 0
クロージャ関数は実際には、構造体newを作成して実装されていることがわかります:
使用されているのはヒープ上のiです:
つまり、関数を返すとき、実際には構造体を返しており、構造体内には関数の参照環境が記録されています。
4、簡単なお話
4.1 Javaはクロージャをサポートしていますか?
インターネットには多くの見解がありますが、実際にはJavaは現在、関数を返り値としてサポートしていませんが、Javaは本質的にクロージャの概念を実装しています。使用されている方法は内部クラスの形式であり、内部クラスは参照環境を内蔵しているため、不完全なクロージャの一種と言えます。
現在にはいくつかの制限があり、例えばfinalで宣言されているか、または明確に定義された値のみを渡すことができます:
Stack Overflowには関連する回答があります:https://stackoverflow.com/questions/5443510/closure-in-java-7
4.2 関数型プログラミングの将来はどうなりますか?
以下はWikiからの内容です:
関数型プログラミングは長い間学術界で流行してきましたが、ほとんど産業応用はありません。この状況の主な原因は、関数型プログラミングがCPUとメモリリソースを大幅に消費すると考えられていることです[18]。これは、関数型プログラミング言語の初期の実装において効率が考慮されなかったためです。また、関数型プログラミングの特性、例えば参照透過性の保証など、独特のデータ構造とアルゴリズムを要求します[19]。
しかし、最近ではいくつかの関数型プログラミング言語が商業または産業システムで使用されています[20]、例えば:
- Erlang:スウェーデンの会社エリクソンが1980年代後半に開発し、当初は耐障害性のある通信システムの実装に使用されていました。その後、ノーテル、Facebook、EDF、WhatsAppなどの企業で人気言語として一連のアプリケーションを作成されています[21][22]。
- Scheme:初期のApple Macintoshコンピュータ上のいくつかのアプリケーションの基礎として使用され、最近では訓練シミュレーションソフトウェアや望遠鏡制御などの分野で応用されています。
- OCaml:1990年代半ばにリリースされ、金融分析、ドライバ検証、産業ロボットプログラミング、組み込みソフトウェアの静的分析などの分野で商業応用されています。
- Haskell:当初は研究言語として開発されましたが、航空宇宙システム、ハードウェア設計、ネットワークプログラミングなどの分野で一連の企業で応用されています。
産業で使用されているその他の関数型プログラミング言語には、マルチパラダイムのScala[23]、F#、そしてWolfram言語、Common Lisp、Standard ML、Clojureなどがあります。
個人的な見解として、純粋な関数型プログラミングはあまり期待していませんが、関数型プログラミングの思想は、今後ほとんどすべての高度なプログラミング言語に備わるべきだと信じています。特にJavaが関数型プログラミングを取り入れることを期待しています。私が知る言語の中では、GoやJavaScriptの関数型プログラミングの特性は、開発者たちに深く愛されています(もちろん、バグを書いた場合は深く嫌悪されることになります)。
最近突然人気が出た理由も、世界が絶えず発展し、メモリも大きくなっているため、この制約はほぼ解放されつつあるからです。
世界は多彩であるべきだと信じています。ある事柄が世界を支配することは絶対にありません。むしろ百花斉放であり、プログラミング言語やプログラミングパラダイムも同じです。今後には集大成者が出現し、最終的に歴史が人類の発展に適合するものを選び出すでしょう。