囲碁におけるクロージャーの基本原理
1. クロージャとは?
関数内部で外部ローカル変数を参照する関数をクロージャと呼びます。
例えば、この下のコードでは
>>> def fun(arg):
pass;
>>> fun(1,arg=3);
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
fun(1,arg=3);
TypeError: fun() got multiple values for argument 'arg'
>>>
関数が返す無名関数は
adder
関数のローカル変数
sum
であれば、その関数はクロージャである。
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
また、このクロージャで参照される外部ローカル変数は
adder
関数が戻り、スタックから破棄されます。
この関数を呼び出してみたところ、呼び出すたびに
sum
の値がクロージャ関数に保持され、使用されます。
func main() {
valueFunc:= adder()
fmt.Println(valueFunc(2)) // output: 2
fmt.Println(valueFunc(2)) // output: 4
}
2. 複雑なクロージャーシナリオ
クロージャを書くのは比較的簡単ですが、簡単なクロージャ関数が書けるだけではダメです。クロージャの本当の仕組みを理解していないと、複雑なクロージャのシナリオでは、関数の実行ロジックを見誤ってしまうことがあります。
<ブロッククオートさっそくですが、こんな例はいかがでしょうか。
何が印刷されると思いますか?
6なのか11なのか?
import "fmt"
func func1() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}
func main() {
closure := func1()
fmt.Println(closure)
}
3. クロージャの根本原理?
まだトップの例を使って分析中
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
valueFunc:= adder()
fmt.Println(valueFunc(2)) // output: 2
}
まずエスケープ解析を実行してみると、簡単にわかるのは
sum
として
adder
関数ローカル変数の場合、スタック上ではなくヒープ上に確保されます。
これで、最初の謎が解けました。
なぜ
adder
関数が返された後に
sum
は一緒に破壊されないのでしょうか?
$ go build -gcflags="-m -m -l" demo.go
# command-line-arguments
. /demo.go:8:3: adder.func1 capturing by ref: sum (addr=true assign=true width=8)
. /demo.go:7:9: func literal escapes to heap:
. /demo.go:7:9: flow: ~r0 = &{storage for func literal}:
. /demo.go:7:9: from func literal (spill) at . /demo.go:7:9
. /demo.go:7:9: from return func literal (return) at . /demo.go:7:2
. /demo.go:6:2: sum escapes to heap:
. /demo.go:6:2: flow: {storage for func literal} = &sum:
. /demo.go:6:2: from func literal (captured by a closure) at . /demo.go:7:9
. /demo.go:6:2: from sum (reference) at . /demo.go:8:3
. /demo.go:6:2: moved to heap: sum
. /demo.go:7:9: func literal escapes to heap
. . /demo.go:15:23: valueFunc(2) escapes to heap:
. /demo.go:15:23: flow: {storage for ... argument} = &{storage for valueFunc(2)}:
. /demo.go:15:23: from valueFunc(2) (spill) at . /demo.go:15:23
. /demo.go:15:23: flow: {heap} = {storage for ... argument}:
. /demo.go:15:23: from ... argument (spill) at . /demo.go:15:13
. /demo.go:15:23: from fmt.Println(valueFunc(2)) (call parameter) at . /demo.go:15:13
. /demo.go:15:13: ... argument does not escape
. /demo.go:15:23: valueFunc(2) escapes to heap
しかし、もうひとつ出てくる問題は、破壊しないまでも、クロージャ関数が保存しているifが
sum
がコピーされると、クロージャ関数が呼び出されるたびに
sum
は同じであるべきで、レコードを蓄積できるのではなく、呼び出されるたびに2を返すはずである。
したがって、クロージャ関数の構造には
sum
へのポインタの
この疑いを確かめるために、私たちはアセンブリに行く必要がありました。
以下のコマンドを実行することで、対応するアセンブリコードを出力することができます。
go build -gcflags="-S" demo.go
出力はかなり大きいので、クロージャ関数の構造を定義している最も重要な行を以下に抜粋した。
ここで、Fは関数へのポインタですが、それは重要ではなく、ポイントはsumが確かにポインタを格納していることで、我々の推測を検証しているのです。
type.noalg.struct { F uintptr; "".sum *int }(SB), CX
4. パズルが明らかになる
上記3項の背景知識があれば、2項で示されたこの問題の答えがわかると思います。
まず、iは関数定義の戻り値で宣言されているので、goの言うところの
caller-save
パターンでは、変数 i は
main
関数のスタック空間を使用します。
次に
func1
の
return
はiに5を割り当て、ここで
i = 5
クロージャ関数にはこの変数iへのポインタが格納されているので
つまり、最終的にdeferでiを自己インクリメントすると、iへのポインタが直接更新され、i = 5+1となるので、最終的なプリントアウトは6となります。
import "fmt"
func func1() (i int) {
i = 10
defer func() {
i += 1
}()
return 5
}
func main() {
closure := func1()
fmt.Println(closure)
}
5. 質問の再変化
上の問題が理解できたなら、次の問題を見てみましょう。
func1
変数名iを書かなくなったfunc1の戻り値、もともと具象リテラルを返していたのが、変数iになっています。この2つの小さな変化が大きな違いになりますので、結果を考えてみてください。
import "fmt"
func func1() (int) {
i := 10
defer func() {
i += 1
}()
return i
}
func main() {
closure := func1()
fmt.Println(closure)
}
戻り値に変数名を書くと、その変数には
main
をスタック空間に書き込むのに対して、書き込まない場合は、iはあくまで
func1
のスタック空間で
return
は元の変数 i には適用されず、関数のスタックにある別のメモリブロックに格納されます。
つまり
defer
には適用されません。
func1
の戻り値で
だから、印刷される結果は10にしかならない。
答えは合っていましたか?
6. 最後の問題
お気づきかどうかわかりませんが、最初の例の合計はヒープメモリに格納され、次のいくつかの例はスタックメモリに格納されています。
これはなぜでしょうか?
よく見比べてみると、例1ではクロージャ関数を返しており、加算器が返った後もクロージャ関数が別の場所で使われ続けていることがよくわかります。この場合、クロージャ関数を正しく動作させるために、クロージャ関数があるところではiをリサイクルできないので、Goコンパイラはヒープ上にインテリジェントに割り当てているのです。
一方、この後に続く他の例は、いずれもクロージャの性質に関わるだけで、クロージャ関数を直接返すわけではないので、スタック上に代入するのは完全に理にかなっています。
この記事は、Goにおけるクロージャの基本原理について書かれたものです。Goにおけるクロージャの基本原理については、Scripting Houseの過去の記事を検索するか、以下の関連記事を引き続き閲覧してください。
関連
-
Go言語基本変数宣言・初期化例詳細
-
Go言語の基本型と定数の使用例詳細
-
囲碁言語基本囲碁インターフェイス使用例詳細
-
golangはファイルをダウンロードするためにマルチプロセッシングを実装しています(ブレークポイント転送をサポート)。
-
初心者のための囲碁言語 ブラッシュアップ プリントアウト砂時計
-
Go言語の基本的な配列の使用方法と例
-
グラフの幅優先探索と深さ優先探索を実装するためのgo言語によるプログラミングを学ぶ
-
Goのfoループと条件判定
-
Go言語並行プログラミングのための相互排除ロックMutexと読み取り/書き込みロックRWMutex
-
Go サービスでリンクトレースを行う方法を説明します。
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン