title: 関数型プログラミング:匿名とクロージャ
date: 2022-07-03 21:38:00
toc: false
index_img: http://api.btstu.cn/sjbz/?lx=m_dongman&cid=8
category:
- Go
tags: - Java
- 作成
- サポート
- 環境
- リクエスト
- 文字列
- 異なる
- 出現
- 印刷
- add
- 離れる
- JavaScript
- メソッド
- 関数
匿名関数の定義と使用#
匿名関数は、関数名が指定されていない関数宣言の方法です(具名関数とは対照的で、名前のある関数は具名関数と呼ばれます)。PHP、JavaScript(Ajax リクエストの実装を考えてみてください)など、多くのプログラミング言語で実装とサポートがあります。Go 言語でも匿名関数をサポートしており、形式的には他の言語と似ています:
func(a, b int) int {
return a * b
}
他の言語と同様に、Go の匿名関数は変数に代入したり直接実行したりすることもできます:
// 1. 匿名関数を変数に代入
mul := func(a, b int) int {
return a * b
}
// 匿名関数 mul を呼び出す
fmt.Println(mul(1, 2))
// 2. 定義時に直接匿名関数を呼び出す
func(a, b int) {
fmt.Println(a * b)
} (1, 2)
匿名関数とクロージャ#
この問いに答えるために、まずクロージャの概念を理解する必要があります。
クロージャとは、自由変数(特定のオブジェクトにバインドされていない変数で、通常は関数の外部で定義されます)を参照する関数のことであり、参照された自由変数はこの関数と共に存在し続けます。それが作成されたコンテキスト環境から離れても(他の関数やオブジェクトに渡された場合など)、解放されることはありません。つまり、「閉じる」という意味は「外部の状態を閉じ込める」ということであり、外部の状態が無効になっても、クロージャ内部にはまだ外部から参照された変数のコピーが残っています。
明らかに、クロージャは匿名関数によってのみ実現できます。クロージャは「状態を持つ匿名関数」と見なすことができます。逆に、匿名関数が外部変数を参照する場合、クロージャ(Closure)が形成されます。
クロージャの価値は、外部変数を保持する関数オブジェクトまたは匿名関数として機能することです。型システムにとって、これはデータだけでなくコードも表す必要があることを意味します。クロージャをサポートする言語は、関数を「第一級オブジェクト」として扱います(firt-class object、一部の場所では「第一級オブジェクト」とも呼ばれます)。Go 言語も例外ではなく、これは Go 関数と通常の Go データ型(整数、文字列、配列、スライス、マップ、構造体など)が同じ地位を持ち、変数に代入したり、他の関数に引数として渡したり、関数を動的に作成したり返すことができることを意味します。
注:「第一級オブジェクト」とは、実行時に作成され、他の関数に引数として渡すことができる実体です。ほとんどの言語では、数値や基本型は第一級オブジェクトです。クロージャをサポートするプログラミング言語(例:Go、PHP、JavaScript、Python など)では、関数も第一級オブジェクトです。一方、C、C++ などの匿名関数をサポートしない言語では、関数は実行時に作成できないため、関数は第一級オブジェクトではありません。
簡単に言えば、クロージャと匿名関数は一緒に現れ、外部変数を参照する場合はクロージャです!以下のコードを使って理解を深めることもできます!
package main
import "fmt"
var iotaVal int
func iotaFunc() int {
val := 1 << (10 * iotaVal)
iotaVal++
return val
}
func main() {
fmt.Printf("%d: KB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: MB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: GB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: TB -> %-14d byte \n", iotaVal, iotaFunc())
fmt.Printf("%d: PB -> %-14d byte \n", iotaVal, iotaFunc())
}
実行結果:
1: KB -> 1 byte
2: MB -> 1024 byte
3: GB -> 1048576 byte
4: TB -> 1073741824 byte
5: PB -> 1099511627776 byte
匿名関数の一般的な使用例#
次に、いくつかの一般的な Go の匿名関数の使用例を見てみましょう。
ローカル変数の安全性を保証する#
匿名関数内で宣言されたローカル変数は外部から変更することができないため、安全性が保証されます(クラスのプライベートプロパティのようなものです):
var j int = 1
f := func() {
var i int = 1
fmt.Printf("i, j: %d, %d\n", i, j)
}
f()
j += 2
f()
上記のコードの出力結果は次のとおりです:
i, j: 1, 1
i, j: 1, 3
上記の例では、匿名関数は外部変数を参照しているため、クロージャでもあります。変数 f は、ローカル変数 i と j への参照を持つクロージャを指しています。i はクロージャ内で定義され、その値は隔離されており、外部から変更することはできません。一方、変数 j はクロージャの外部で定義されているため、外部から変更することができます。クロージャが保持するのは参照のみです。
匿名関数を関数の引数として使用する#
匿名関数は普通の変数に代入するだけでなく、関数の引数として渡すこともできます。これは普通のデータ型と同じように行います:
add := func(a, b int) int {
return a + b
}
// 関数型を引数として受け取る
func(call func(int, int) int) {
fmt.Println(call(1, 2))
}(add)
関数の型を宣言する場合、各パラメータと戻り値の型を厳密に指定する必要があります。したがって、add 関数に対応する関数型は func (int, int) int です。
また、2 番目の匿名関数を main 関数の外に抽出し、名前付き関数 handleAdd にすることもできます。さらに、異なる加算アルゴリズムの実装関数を定義し、それを handleAdd に引数として渡すこともできます:
func main() {
// 通常の加算操作
add1 := func(a, b int) int {
return a + b
}
// 異なる加算アルゴリズムを定義
base := 10
add2 := func(a, b int) int {
return a*base + b
}
handleAdd(1, 2, add1) // 3
handleAdd(1, 2, add2) // 1*10 + 2 = 12
}
// 匿名関数を引数として受け取る
func handleAdd(a, b int, call func(int, int) int) {
fmt.Println(call(a, b))
}
上記のコードの出力結果は次のとおりです:
3
12
この例では、2 番目の匿名関数 add2 は外部変数 base を参照し、クロージャを形成します。外部関数 handleAdd を呼び出す際に、クロージャ add2 を引数として渡すことで、add2 クロージャは main 関数のスコープを離れても base 変数にアクセスできます。
このように、複数の異なる加算実装アルゴリズムを 1 つの関数で実行することができるため、コードの再利用性が向上します。この機能を活用して、より複雑なビジネスロジックを実装することもできます。たとえば、Go 公式の net/http パッケージのルーティングハンドラもこのように実装されています:
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
匿名関数を関数の戻り値として使用する#
最後に、匿名関数を関数の戻り値として返すこともできます:
// 関数を戻り値の型として宣言
func deferAdd(a, b int) func() int {
return func() int {
return a + b
}
}
func main() {
// ここでは匿名関数が返されます
addFunc := deferAdd(1, 2)
// ここで実際の加算が実行されます
fmt.Println(addFunc())
}
上記のコードの出力結果は次のとおりです:
3
上記の例では、deferAdd 関数を呼び出すと匿名関数が返されますが、この匿名関数は外部関数に渡された引数を参照しているため、クロージャが形成されます。このクロージャが存在する限り、参照されたパラメータ変数も存在し続けます。deferAdd 関数のスコープを超えても、これらの変数にアクセスできます。
また、deferAdd 関数の呼び出し時にはクロージャは実行されず、addFunc () を実行するとクロージャ内のビジネスロジック(ここでは加算)が実際に実行されます。したがって、関数の戻り値を関数型として宣言することで、ビジネスロジックの遅延実行を実現し、実行タイミングを完全に制御できます。
以下のコードを読み、プログラムの実行結果を得てください:
// 関数を戻り値の型として宣言
func deferAdd(a, b int) func() int {
return func() int {
a++
return a + b
}
}
func main() {
// ここでは匿名関数が返されます
addFunc := deferAdd(1, 2)
// ここで実際の加算が実行されます
fmt.Println(addFunc())
fmt.Println(addFunc())
}