در 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 باعث بروز panic میشود.
var dutchGreeting func(string) string
dutchGreeting("Alice") // panic: call of nil function
مقادیر تابعی را میتوان با nil مقایسه کرد.
این میتواند برای جلوگیری از بروز panicهای غیرضروری در برنامه مفید باشد.
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اُم تابع بینام، عدد nاُم دنبالهی فیبوناچی را با شروع از ۰ برمیگرداند. تابع بینام درونی به متغیرهای محلی (n1 و n2) تابع دربرگیرندهی fib دسترسی دارد.
این روش خوبی است تا مقادیر تابعی وضعیت را بین فراخوانیها حفظ کنند.
میگوییم که تابع بینام یک «بستار» روی متغیرهای n1 و n2 است. بستارها در برنامهنویسی بهطور گسترده استفاده میشوند و ممکن است ببینید زبانهای دیگر هم از آنها پشتیبانی میکنند.
باب مشاور مالی است و به افراد کمک میکند هزینههایشان را مدیریت کنند. مشتریان باب سوابق هزینههای خود را میفرستند تا او آنها را تحلیل کند. باب سوابق دورههای پیشین را هم دارد تا بتواند تغییرات هزینهها را ببیند. باب تقویم را دوست ندارد و بهجای تاریخ از «مبدأ باب» استفاده میکند. مبدأ باب تعداد روزهایی است که از زمان شروع فعالیت مشتری باب گذشته است.
در این تمرین، برنامهای میسازید که به باب کمک میکند هزینههای مشتریانش را مدیریت و تحلیل کند.
باب با 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
باب هر روز با سوابق زیادی سر و کار دارد، اما بسته به تحلیلی که انجام میدهد، همهی آنها برایش جالب نیستند. بیایید به باب کمک کنیم تا یک فیلترگذاری پایه روی سوابق انجام دهد.
تابع عمومی 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"}
// ]
باب اغلب لازم دارد سوابقی را فیلتر کند که در یک بازهی زمانی مشخص قرار دارند.
تابع 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"},
// ]
باب علاوه بر فیلتر کردن سوابق بر اساس بازهی زمانی، لازم دارد سوابق را بر اساس دستهی آنها هم فیلتر کند.
تابع 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"},
// ]
باب همچنین میخواهد مجموع هزینهها را در یک بازهی زمانی بداند.
تابع 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
باب برای پیچیدهترین گزارشهایی که به مشتریانش ارائه میدهد، لازم دارد سوابق را همزمان بر اساس دسته و بازهی زمانی فیلتر کند. یعنی باب میخواهد مجموع هزینههای سوابق یک دسته را در یک بازهی زمانی مشخص بداند.
تابع 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