지출

지출

학습 연습 문제

소개

Go에서 함수는 일급 값이에요. 이 말은 함수로도 다른 모든 값과 똑같은 일을 할 수 있다는 뜻이에요. 함수를 변수에 할당할 수도 있고, 다른 함수에 인자로 넘길 수도 있고, 심지어 다른 함수에서 함수를 반환할 수도 있어요.

아래에서는 engGreeting과 espGreeting 두 함수를 만들고, 이 함수들을 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.")
}

익명 함수

일급 함수를 지원하기 때문에 쓸 수 있는 또 하나의 강력한 도구는 익명 함수예요. 익명 함수는 사용되는 지점에서 정의하며, 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이 기록을 기본적으로 필터링할 수 있도록 도와봐요.

함수로 주어진 기준에 따라 기록을 필터링하는 제네릭 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은 주어진 기간에 속한 기록을 자주 필터링해야 해요.

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은 기간으로 기록을 필터링하는 것 외에도 카테고리로 기록을 필터링해야 해요.

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은 주어진 기간에 특정 카테고리에 속한 기록의 지출 총액을 알고 싶어 하는 거예요.

주어진 날짜 기간에 특정 카테고리의 지출 총액을 반환하는 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에 가입하고 Go 트랙을 개념 34개연습 문제 165개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.