経費

経費

学習演習

はじめに

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

1. 汎用的なレコードフィルターを実装する

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"}
// ]

2. 一定の期間内のレコードを絞り込む

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"},
// ]

3. カテゴリーでレコードを絞り込む

期間でレコードを絞り込むだけでなく、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"},
// ]

4. 期間内の支出の合計額を計算する

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

5. 期間内の、あるカテゴリーのレコードの支出合計を計算する

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
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Go Exercism

経費を始める準備はできましたか?

Exercismに登録すれば、34個のコンセプト166個の演習、そして本物の人間によるメンタリングとともに、Goを学んでマスターできます。すべて無料です。