Goの無名関数とクロージャ

プログラミング
スポンサーリンク

プログラムはできるだけシンプルに分かりやすく記述することで、バグや認識違いを少なくすることができます。

Goの無名関数とクロージャは、プログラムの見通しを高めてくれる手法の1つです。

本記事では、Goの無名関数とクロージャについて解説します。

記事内に記載しているプログラムは、go1.15.2を使って動作確認をしています。

Goの無名関数とクロージャ

Goの無名関数とクロージャについて、以下の内容を解説します。

  • 無名関数とは
  • 無名関数の基本形
  • 無名関数の応用形
  • クロージャ

無名関数とは

無名関数とは、その名の通り名前を付けずに定義された関数のことです。

他の言語と同じく、Go言語においても無名関数を定義することができます。

基本的な構文は以下の通りです。

func(引数1, 引数2, …) 戻り値の型1, 戻り値の型2, … {
  関数の本体
}

このような関数を変数に格納し、関数リテラルとして扱うことができます。
無名関数を用いることのメリットとして、以下のようなことがあります。

  • 一度しか使わない関数であれば、名前を考える手間が省け、また他と名前の衝突を避けることができる。
  • 関数本体の処理が、使うプログラムの近くにあると、処理の見通しが良くなる。

無名関数の基本形

無名関数と従来の関数の違いを比較するために、まずは従来の関数を使った方法が以下となります。

func pow(n int) int {
    return int(math.Pow(float64(n), 2))
}

func main() {
    r := pow(2)
    fmt.Println(r) // -> 4
}

上記のプログラムでは、関数powを作成し、引数で受け取った数値を2乗して返しています。
呼び出し元となるmain関数では、pow関数を呼出して戻り値を受け取っています。

関数の呼出しは、以下のように一旦変数に格納してから呼び出すこともできます。

func main() {
    f := pow
    fmt.Println(f(2)) // -> 4
}

呼出し時に関数の名前ではなく、格納した変数名を指定して呼出しています。。

そして、無名関数を用いて記述したプログラムが以下となります。

func main() {
    f2 := func(n int) int {
        return int(math.Pow(float64(n), 2))
    }
    fmt.Println(f2(2)) // -> 4
}

関数を返す無名関数として定義し、中身の処理は先程と同じく引数の値を2乗して返す処理としています。
呼出し時は、関数を格納した変数名f2を使って呼出しています。

無名関数の応用形

無名関数の応用形として、戻り値として関数を使う場合と、引数に関数を使う場合を紹介します。

関数の戻り値として関数を指定する

以下のプログラムは、関数の戻り値を定義する部分が func となっていて、戻り値が関数型であることを表しています。

func returnPow() func(int) int {
    return func(n int) int {
        return int(math.Pow(float64(n), 2))
    }
}

これを使う場合は、先程と同様に直接関数名を指定しても良いですし、一旦変数に格納してから呼び出すこともできます。
ただ、最初に呼び出す関数(returnPow)は、関数を返すので、その処理をするためにはさらに()で、関数を呼び出す必要があります。

fmt.Println(returnPow()(2)) // -> 4

f3 := returnPow()
fmt.Println(f3(2)) // -> 4

これらを無名関数を使うと以下のようになります。

f4 := func() func(int) int {
    return func(n int) int {
        return int(math.Pow(float64(n), 2))
    }
}
fmt.Println(f4()(2)) // -> 4

上記のプログラムでは、無名関数をネストさせて作成し、一旦f4という変数に入れて呼出しています。
上記の処理ではわざわざネストさせるメリットはないのですが、後述するクロージャとして使う際に力を発揮します。

関数の引数に関数を指定する

以下のプログラムは、関数の引数の定義がfuncとなっていて、関数型の引数であることを表しています。

func argFunc(n int, f func(int) int) int {
    return f(n)
}

上記のプログラムの処理は、引数の関数を呼出して、その結果を返す処理をしています。
これを使うには以下のようにします。

fmt.Println(argFunc(
    2, func(n int) int { return int(math.Pow(float64(n), 2)) })) // -> 4

長いので改行していますが、1行のプログラムとなっています。
第1引数に2乗する数の2を指定し、第2引数に2乗するための関数を無名関数として作成しています。

これらは、それぞれ以下のように記述することもできます。

f5 := func(n int, f func(int) int) int {
    return f(n)
}
f6 := func(n int) int {
    return int(math.Pow(float64(n), 2))
}
fmt.Println(f5(2, f6)) // -> 4

関数化しておくことで、引数を入れ替えたり、2乗する関数の処理を入れ替えたりすることも簡単にできます。

fmt.Println(f5(3, f6)) // -> 9

クロージャ

Go言語ではクロージャを扱うこともできます。

クロージャとは、ネストされた関数定義において、内側の関数が外にある変数を使って処理をすることができるというものです。
以下のプログラムでは、クロージャを用いています。

func sumPow() func(int) (int, int) {
    sum := 0
    return func(n int) (int, int) {
        sum = sum + int(math.Pow(float64(n), 2))
        return sum, n
    }
}

変数sumに、2乗した値を足しています。
sumは内側の関数の外にありますが、それを使ってsum自身の値を書き換えています。

この関数を使うプログラムが以下となります。

f7 := sumPow()
for i := 1; i <= 5; i++ {
    sum, n := f7(i)
    fmt.Printf("%d : %d\n", n, sum)
}

関数sumPowを一旦変数に格納して、ループの中で呼出しています。
一旦変数に格納されることで、関数内のsumは値が残り続けるので、結果は以下のようになります。

1 : 1
2 : 5
3 : 14
4 : 30
5 : 55

上記のsumのように、関数内で状態(値)を保持し続けて、処理をさせたい場合などにクロージャは有用となります。

まとめ

Goの無名関数とクロージャについてまとめると、以下のようになります。

  • 無名関数を使うと名前の衝突を避けたり、使い方によっては見通しのよいプログラムにすることができる。
  • 関数の引数や戻り値に関数を指定することで、幅広い処理を行うことができる。
  • クロージャを使うと、関数内で状態を保持しつつ処理を行うことができる。

無名関数やクロージャは、やりすぎると逆に分かりづらくなってしまうので注意が必要ですが、シンプルに記述できそうな場合に試してみてはいかがでしょうか。

 

今回はGoの無名関数とクロージャについて解説しました。

以上、参考になれば幸いです。

コメント

タイトルとURLをコピーしました