Goでは、関数は第一級の値です。 これは、他のすべての値に対してできることと同じことを、関数に対してもできるという意味です。関数を変数に代入したり、他の関数に引数として渡したり、他の関数から関数を返したりできます。
次のコードでは、engGreetingとespGreetingという2つの関数を作り、それらを変数greetingに代入しています。
import "fmt"
func engGreeting(name string) string {
return fmt.Sprintf("Hello %s, nice to meet you!", name)
}
func espGreeting(name string) string {
return fmt.Sprintf("¡Hola %s, mucho gusto!", name)
}
greeting := engGreeting // greeting is a variable of type func(string) string
fmt.Println(greeting("Alice")) // Hello Alice, nice to meet you!
greeting = espGreeting
fmt.Println(greeting("Alice")) // ¡Hola Alice, mucho gusto!
関数の値を使うと、関数をデータだけでなく振る舞いによってもパラメーター化できます。
次の例では、greetingFuncという仮引数を通じて、dialog関数に振る舞いを渡しています。
func dialog(name string, greetingFunc func(string) string) {
fmt.Println(greetingFunc(name))
fmt.Println("I'm a dialog bot.")
}
func espGreeting(name string) string {
return fmt.Sprintf("¡Hola %s, mucho gusto!", name)
}
greeting := espGreeting
dialog("Alice", greeting)
// =>
// ¡Hola Alice, mucho gusto!
// I'm a dialog bot.
関数型の変数を初期化しない場合、その値はnilです。
そのため、nilの関数値を呼び出すとパニックが発生します。
var dutchGreeting func(string) string
dutchGreeting("Alice") // panic: call of nil function
関数の値はnilと比較できます。
これは、不要なプログラムのパニックを避けるのに役立ちます。
var dutchGreeting func(string) string
if dutchGreeting != nil {
dutchGreeting("Alice") // safe to call dutchGreeting
}
関数の値を使えるのは、Goの関数型のおかげです。
関数型は、仮引数の型の並びと戻り値の型の並びが同じである、すべての関数の集合を表します。
ユーザー定義型は、関数型を基に宣言できます。
たとえば、前の例のdialog関数は、次のように書き換えられます。
type greetingFunc func(string) string
func dialog(name string, f greetingFunc) {
fmt.Println(f(name))
fmt.Println("I'm a dialog bot.")
}
第一級関数が使えるおかげで利用できる、もう1つの強力な道具が無名関数です。
無名関数は、使う場所で定義され、funcキーワードの後に名前が続きません。
このような関数は、外側の関数の変数にアクセスできます。
たとえば、次のようになります。
func fib() func() int {
var n1, n2 int
return func() int {
if n1 == 0 && n2 == 0 {
n1 = 1
} else {
n1, n2 = n2, n1 + n2
}
return n2
}
}
next := fib()
for i := range N {
fmt.Printf("F%d\t= %4d\n", i, next())
}
fibを呼び出すと、変数n1とn2が宣言され、無名関数が返されます。この無名関数は、呼び出されるたびにこれらの変数の値を変えます。
無名関数をn回呼び出すと、0から始まるフィボナッチ数列のn番目の数が返されます。内側の無名関数は、外側の関数fibのローカル変数(n1とn2)にアクセスできます。
これは、関数の値に呼び出し間の状態を保持させる優れた方法です。
無名関数は、変数n1とn2のクロージャーであると言います。クロージャーはプログラミングで広く使われており、他の言語でもサポートされているのを見かけるかもしれません。
Bobは金融アドバイザーで、人々が支出を管理するのを手伝っています。 Bobのクライアントは、分析してもらうために支出の記録をBobに送ります。 Bobは過去の期間の記録も持っているので、支出の変化を見ることができます。 Bobはカレンダーが好きではなく、日付の代わりにBob epochを使います。 Bob epochは、Bobのクライアントが活動を始めてから経過した日数です。
この演習では、Bobがクライアントの支出を管理・分析するのを助けるプログラムを作ります。
BobはRecordとDaysPeriodを扱います。
Recordは支出の記録を表し、支出があった日、使った金額、支出のカテゴリーを含みます。
// Record represents an expense record.
type Record struct {
Day int
Amount float64
Category string
}
DaysPeriodは日の範囲を表し、Fromの日からToの日までのすべての日を含みます。
範囲には両端の日も含まれます。
// DaysPeriod represents a period of days.
type DaysPeriod struct {
From int
To int
}
p := DaysPeriod{From: 1, To: 31}
// p represents all days from the day 1 to the day 31:
// - days 1, 20, 16 and 31 are examples of days that are included
// in the range of time specified by p
// - days 50 and 40 are examples of days that are not included
// in the range of time specified by p
Bobは毎日たくさんのレコードを扱いますが、Bobが行う分析によっては、そのすべてが興味深いものではありません。 Bobが基本的なレコードの絞り込みを行えるよう、手伝いましょう。
関数で与えられた条件に従ってレコードを絞り込む、汎用的なFilter関数を実装します。
このフィルター関数は、レコードのコレクションと条件を判定する関数を受け取り、コレクションの中でその条件を満たすレコードだけを返します。
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
}
// Day1Records only returns true for records that are from day 1
func Day1Records(r Record) bool {
return r.Day == 1
}
Filter(records, Day1Records)
// =>
// [
// {Day: 1, Amount: 15, Category: "groceries"}
// ]
Bobは、ある期間内にあるレコードを絞り込む必要がよくあります。
そのようなフィルターを作れるようにするByDaysPeriod関数を実装します。
この関数はDaysPeriodを受け取り、レコードを受け取って、そのレコードが引数で与えられたDaysPeriodで指定された期間内にあるかどうかを返す関数を返します。
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
{Day: 26, Amount: 300, Category: "university"},
{Day: 28, Amount: 1300, Category: "rent"},
}
period := DaysPeriod{From: 1, To: 15}
Filter(records, ByDaysPeriod(period))
// =>
// [
// {Day: 1, Amount: 15, Category: "groceries"},
// {Day: 11, Amount: 300, Category: "utility-bills"},
// {Day: 12, Amount: 28, Category: "groceries"},
// ]
期間でレコードを絞り込むだけでなく、Bobはカテゴリーでもレコードを絞り込む必要があります。
そのようなフィルターを作れるようにするByCategory関数を実装します。
この関数はカテゴリーを受け取り、レコードを受け取って、そのレコードのカテゴリーが引数で与えられたカテゴリーと同じかどうかを返す関数を返します。
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
{Day: 28, Amount: 1300, Category: "rent"},
}
Filter(records, ByCategory("groceries"))
// =>
// [
// {Day: 1, Amount: 15, Category: "groceries"},
// {Day: 12, Amount: 28, Category: "groceries"},
// ]
Bobは、ある期間内の支出の合計額も知りたいと思っています。
TotalByPeriod関数を実装して、その期間内の支出の合計を返すようにします。
records := []Record{
{Day: 15, Amount: 16, Category: "entertainment"},
{Day: 32, Amount: 20, Category: "groceries"},
{Day: 40, Amount: 30, Category: "entertainment"}
}
p1 := DaysPeriod{From: 1, To: 30}
p2 := DaysPeriod{From: 31, To: 60}
TotalByPeriod(records, p1)
// => 16
TotalByPeriod(records, p2)
// => 50
Bobがクライアントに出す最も複雑なレポートのために、Bobはカテゴリーと期間の両方で同時にレコードを絞り込む必要があります。 つまり、Bobはある期間内の、あるカテゴリーのレコードの支出合計を知りたいということです。
ある期間内の、あるカテゴリーの支出の合計額を返すCategoryExpenses関数を実装します。
また、この関数は、与えられたカテゴリーが支出の記録に存在しない場合と、指定された期間内にそのカテゴリーの支出がない場合を区別する必要があります。
そのカテゴリーがどのレコードのカテゴリーでもない場合(期間に関係なく)、この関数はエラーを返す必要があります。
p1 := DaysPeriod{From: 1, To: 30}
p2 := DaysPeriod{From: 31, To: 60}
records := []Record{
{Day: 1, Amount: 15, Category: "groceries"},
{Day: 11, Amount: 300, Category: "utility-bills"},
{Day: 12, Amount: 28, Category: "groceries"},
{Day: 26, Amount: 300, Category: "university"},
{Day: 28, Amount: 1300, Category: "rent"},
}
CategoryExpenses(records, p1, "entertainment")
// => 0, error(unknown category entertainment)
CategoryExpenses(records, p1, "rent")
// => 1300, nil
CategoryExpenses(records, p2, "rent")
// => 0, nil