[memo #2] ファーストクラス関数(第一級関数)

2022年12月15日

golamgマイメモ

Go言語を学ぶために「入門Goプログラミング」を読み進めながら、自分が理解を深めるためにポイントや練習結果などを残した自分用メモ。

Goでは、変数に関数を代入したり、引数や戻り値として関数を渡したりすることによって、どこでも関数が使えるようになっており、このような関数は「ファーストクラス」(第一級関数)と呼ばれるのだそう。

変数に関数を代入する

コード例

package main

import (
    "fmt"
    "math/rand"
)

type year int

func randShowa() year {    // 1925年(昭和元年)から1989年(昭和64年)までをランダムに求める関数
        return year(rand.Intn(64) + 1925)
}

func main() {
        seireki := randShowa    // 変数に関数を代入する
        fmt.Printf("西暦%v年\n", seireki())
}

実行例

西暦1958年
ポイント
  • 上記コードの14行目でseireki変数に代入しているのは、関数呼び出しの結果ではなくconvShowa関数そのもの。
  • 普通、関数やメソッドを呼び出す際は()を付けるが、変数に関数を代入する場合には()を付けない。
  • その後は変数seirekiが、代入された関数として振る舞う。なので関数を複数用意しておき、状況に応じて変数に代入する関数を分岐させることにより、多彩な動作をする記述が可能となる。

上記の変数seirekiは「関数型」であり、その関数は引数を取らずにyearの結果を返すだけのため、型推論を必要としない。このように型推論に頼らないときは、変数seirekiを次のように宣言できる。

var seireki func() year

関数を他の関数に渡す

関数を変数に代入出来るということは、そのまま引数として他の関数に渡すことが出来るということを意味する。どんなメリットがあるのだろうか。

コード例

package main

import (
    "fmt"
    "math/rand"
    "time"
)

type year int

// 第2引数として関数を受け取る関数
func getRandYear(times int, seireki func() year) {
    for i := 0; i < times; i++ {
        y := seireki()
        fmt.Printf("%v年\n", y)
        time.Sleep(time.Second)
    }
}

func getShowa() year {
    return year(rand.Intn(64) + 1925)
}

func main() {
    // 第2引数として関数名を関数に渡す
    getRandYear(3, getShowa)
}

実行例

1958年
1940年
1932年

getRandYear関数の第2引数の型は「func() year」だが、同じ型を持つ変数の宣言に似ている。

var getRandYear func() year

関数を引数として他の関数に渡す事により、コードを分割して再利用しやすくなるというメリットがある。

関数型を宣言する

ある関数のために新しい型を宣言することによって、その関数を参照しているコードを短くし、より簡潔明瞭に書くことが出来る。

これまでに、年号を表現するためにyear型を使ったのと同じように、引数として使い回す関数についても同様の書き方が可能である。

type seireki func() year

このコードの意味は、単に「引数を受け取らずにyear型を返す関数」というのではなく、「seireki型の関数」ということである。この型には、他のコードを凝縮する効果があるので、func getRandYear(times int, seireki func() year)というコードを次のように書くことが出来る。

func getRandYear(times int, s seireki)

この例だけでは、今ひとつ改善とは思えない。このコードを見たときに「seireki」とは何かを知っている必要があるからである。だが、seirekiをあちこちで使う場合や、関数にたくさんの引数がある場合には、型を使うことでゴチャゴチャしたコードをスッキリと整理することが出来るはずだ。

クロージャーと無名関数

無名関数とは? クロージャーとは?

無名関数とは、Goでは「関数リテラル」とも呼ばれる、名前を持たない関数のこと。普通の関数と違い、スコープの外側においても自分のスコープ内にある変数の値(参照)を保持することが出来るという特徴を持ち、この機能を「クロージャー」と呼ぶ。

使い方として、例えば無名関数を変数に代入しておけば、その変数は他の関数と同じように使うことが出来る。また、その変数の宣言場所は、グローバルでもローカルでも構わない。以下に例を示す。

package main

import "fmt"

// グローバルでの宣言
var f1 = func() {  // 無名関数を変数に代入
    fmt.Println("(1) パッケージスコープで宣言した無名関数が実行されました。")
}

func main() {
    f1() // 無名関数を呼び出す

    // ローカルでの宣言
    var f2 = func(message string) {  // 無名関数を変数に代入
        fmt.Println(message)
    }
    f2("(2) 関数内で宣言した無名関数が実行されました。")  // 無名関数を呼び出す
}
(1) パッケージスコープで宣言した無名関数が実行されました。
(2) 関数内で宣言した無名関数が実行されました。

また、次の例のように無名関数の宣言と呼び出しを1ステップで行うことも出来る。

package main

import "fmt"

func main() {
    func() { // 無名関数の宣言
        fmt.Println("無名関数が宣言され、そのまま実行されました。")
    }()  // 無名関数の呼び出し
}
無名関数が宣言され、そのまま実行されました。

無名関数は、例えば他の関数から関数を返すときなど、その場で作る必要がある場合に便利に使える。少なくとも、わざわざ関数名を別途宣言しておいて戻り値に指定するよりも遥かにラクである。

実際に使ってみる

次のコードではファーストクラス関数を用いて、西暦から昭和何年かを求める。全く意味のないコードで申し訳ない。

package main

import "fmt"

type year int

// showa関数型
type showa func() year

func calcSeireki() year {
    return 1988
}

func getShowa(s showa, offset year) showa {
    return func() year {  // 無名関数を宣言して返す
        return s() + offset
    }
}

func main() {
    ansYear := getShowa(calcSeireki, -1925)
    fmt.Printf("昭和%v年\n", ansYear())
}
昭和63年
ポイント

上記のファーストクラス関数を使うgetShowa関数は、西暦をパラメータとして受け取り、代替となる関数を返している。この新しいshowa型関数は、呼び出されると元の関数を呼び出し、西暦をオフセットで補正している。

上記の無名関数は、クロージャーを利用することによって、getShowa関数がパラメータとして受け取ったsoffsetを参照しているが、たとえgetShowa関数がリターンした後でもクロージャーによってキャプチャーされた変数は保持される特徴を持っている。だからこそ、ansYearの呼び出しでsおよびoffsetにアクセス出来た訳だ。このように無名関数とは、そのスコープにある変数を包み込んで保持する働きから「クロージャー」(閉包)と呼ばれるのである。

ちなみに、クロージャーは閉包する変数の値をコピーするのではなく、その変数への「参照」を保持するので、元の変数を変更すると無名関数の呼び出し結果に反映される。

package main

import "fmt"

func main() {
    num := 999

    answer := func() int {
        return num
    }

    fmt.Println(answer())  // -> 999

    num++

    fmt.Println(answer())  // -> 1000
}

※特に、forループの中でクロージャーを使うときには注意が必要。

チャレンジ

ファーストクラス関数を用いて、西暦から昭和に換算する表を出力させる。

|       AD |    SHOWA |
=======================
|     1950 |       25 |
|     1955 |       30 |
|     1960 |       35 |
|     1965 |       40 |
|     1970 |       45 |
|     1975 |       50 |
|     1980 |       55 |
|     1985 |       60 |
-----------------------
package main

import (
	"fmt"
)

// 型の定義
type seireki uint64
type showa uint64

// 西暦から昭和に換算するメソッド
// (seireki型の値を受け取って換算後、showa型に型変換して戻す)
func (ad seireki) getShowa() showa {
	return showa(ad - 1925)
}

// フォーマット類の定義
const (
	line         = "======================="
	line2        = "-----------------------"
	rowFormat    = "| %8s | %8s |\n"
	numberFormat = "%d"
)

/* ------------------------------------------------------------
	drawTable:	2列の表を出力する関数
   ------------------------------------------------------------
【引数】
	hdr1, hdr2 (string)	:	表の各列に出力する文字列
	rows (int)			:	出力する表の行数
	getRow (getRowFn)	:
		各行に表示出力する内容を取得する関数
			【引数】
				row (int)			:	いま何行目か
			【戻り値】
				(string, string)	:	その行に出力すべき換算結果
【戻り値】
	なし
------------------------------------------------------------ */
type getRowFn func(row int) (string, string)

func drawTable(hdr1, hdr2 string, rows int, getRow getRowFn) {
	fmt.Println(line)
	fmt.Printf(rowFormat, hdr1, hdr2)
	fmt.Println(line)
	for row := 0; row < rows; row++ {
		cell1, cell2 := getRow(row)
		fmt.Printf(rowFormat, cell1, cell2)
	}
	fmt.Println(line2)
}

// 西暦から昭和に換算する関数
func ad_to_sw(row int) (string, string) {
	ad := seireki(row*5 + 1950)            // seireki型に型変換
	sw := ad.getShowa()                    // メソッド呼び出し
	cell1 := fmt.Sprintf(numberFormat, ad) // 出力する文字列を準備
	cell2 := fmt.Sprintf(numberFormat, sw) // 同上
	return cell1, cell2
}

func main() {
	drawTable("AD", "SHOWA", 8, ad_to_sw)
}

drawTable関数では、引数の最後でファーストクラス関数getRawを受け取っており、この関数が呼び出される事により、各列に表示するデータを取得している。

golang

Posted by doka2000gt