Go Programming

تسلط بر الگوهای تابعی عمومی در Go: Map، Filter و Reduce

برای سال‌ها، زبان برنامه‌نویسی Go به دلیل سادگی و جریان کنترل صریح خود شناخته می‌شد. اگرچه این پارادایم Go را برای برنامه‌نویسی سیستمی و همزمانی بسیار موفق کرده است، اما اغلب در تضاد با سبک مختصر و اعلانی یافت شده در زبان‌های تابعی مانند Haskell، Scala یا حتی JavaScript قرار داشته است. توسعه‌دهندگانی که از این اکوسیستم‌ها مهاجرت می‌کنند، اغلب از ظرافت توابع مرتبه بالاتر مانند map، filter و reduce خالی می‌مانند.

با این حال، با معرفی پارامترهای نوع (جنریک‌ها) در Go 1.18، منظره تغییر کرده است. ما دیگر مجبور به نوشتن کدهای تکراری و قالبی برای هر نوع داده نیستیم. در این پست، بررسی خواهیم کرد که چگونه نسخه‌های قدرتمند و عمومی از این الگوهای برنامه‌نویسی تابعی را در Go پیاده‌سازی کنیم، که با حفظ ایمنی نوع که Go به آن معروف است، قابلیت استفاده مجدد کد را افزایش می‌دهد.

قدرت جنریک‌ها در Go

قبل از غرق شدن در پیاده‌سازی‌های خاص، درک مسئله‌ای که جنریک‌ها در این زمینه حل می‌کنند، حیاتی است. پیش از Go 1.18، پیاده‌سازی یک تابع map عمومی نیاز به استفاده از اینترفیس‌هایی مانند interface{} یا نوشتن چندین تابع با بارگذاری بیش از حد (overloaded) برای انواع مختلف (مثلاً MapInt، MapString) داشت. این رویکرد طولانی بود و از ایمنی نوع در زمان کامپایل برای منطق تبدیل برخوردار نبود.

با استفاده از جنریک‌ها، می‌توانیم یک تابع واحد تعریف کنیم که یک برش (slice) از هر نوع T و یک تابع تبدیل را می‌پذیرد. این امر به کامپایلر اجازه می‌دهد تا تأیید کند که عملیات ما برای انواع خاص مورد نظر معتبر هستند و خطاها را قبل از اجرای کد شناسایی می‌کند.

پیاده‌سازی Map

تابع map یک تابع داده شده را روی هر عنصر یک مجموعه اعمال می‌کند و یک مجموعه جدید با نتایج را برمی‌گرداند. در پیاده‌سازی عمومی ما، به یک برش منبع از نوع T و یک تابع تبدیل که T را می‌پذیرد و احتمالاً نوع متفاوتی U را برمی‌گرداند، نیاز داریم.

func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

این پیاده‌سازی ساده اما قدرتمند است. سناریویی را در نظر بگیرید که یک برش از اعداد صحیح نشان‌دهنده شناسه‌های کاربر دارید و نیاز دارید آن‌ها را به یک برش از رشته‌های قالب‌بندی شده تبدیل کنید. با استفاده از map عمومی ما:

ids := []int{1, 2, 3}
formatted := Map(ids, func(id int) string {
    return fmt.Sprintf("User-%d", id)
})
// Result: []string{"User-1", "User-2", "User-3"}

پیاده‌سازی Filter

در حالی که map عناصر را تبدیل می‌کند، filter آن‌ها را انتخاب می‌کند. این تابع فقط آن عناصری را حفظ می‌کند که تابع شرط‌سنج (predicate) برای آن‌ها true را برمی‌گرداند. از آنجا که نوع خروجی با نوع ورودی مطابقت دارد، امضا کمی ساده‌تر است.

func Filter[T any](slice []T, fn func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

یک مورد استفاده عملی شامل پاکسازی داده‌ها است. فرض کنید لیستی از دماها دارید و می‌خواهید فقط آن‌هایی را که در محدوده عملیاتی ایمن هستند، حفظ کنید:

temps := []float64{10, 20, 30, 40, 50}
safeTemps := Filter(temps, func(t float64) bool {
    return t < 45
})
// Result: []float64{10, 20, 30, 40}

چندمنظوره بودن Reduce

Reduce که به عنوان fold یا accumulate نیز شناخته می‌شود، اغلب پیچیده‌ترین مورد از سه مورد است. این تابع تمام عناصر یک مجموعه را به یک مقدار واحد ترکیب می‌کند. برخلاف map و filter، reduce به یک مقدار اولیه تجمیع‌کننده (accumulator) نیاز دارد. این به ما اجازه می‌دهد اعداد را جمع کنیم، رشته‌ها را به هم بچسبانیم یا اشیاء پیچیده بسازیم.

func Reduce[T, U any](slice []T, fn func(U, T) U, initial U) U {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

برای محاسبه قیمت کل اقلام در یک سبد خرید، ممکن است یک ساختار برای کالا تعریف کنیم:

type Item struct { Price float64 }
items := []Item{{10.0}, {20.0}, {5.5}}
total := Reduce(items, func(acc float64, item Item) float64 {
    return acc + item.Price
}, 0.0)
// Result: 35.5

نتیجه‌گیری

با بهره‌گیری از قابلیت‌های جنریک Go، می‌توانیم ظرافت و بیان‌گری برنامه‌نویسی تابعی را به کدهای Go خود بیاوریم. اگرچه Go یک زبان تابعی نیست، اما پذیرش این الگوها می‌تواند منجر به کدی تمیزتر، ماژولارتر و به شدت قابل آزمایش شود. کلید کار استفاده خردمندانه از این انتزاعات است—ترجیح دادن خوانایی و قابلیت نگهداری بدون قربانی کردن عملکرد و وضویی که Go را منحصر به فرد می‌کند.

هنگامی که map، filter و reduce را در پروژه‌های خود ادغام می‌کنید، به یاد داشته باشید که سادگی همچنان اولویت اصلی است. از این ابزارها برای حذف تکرار و شفاف‌سازی قصد خود استفاده کنید و اطمینان حاصل کنید که برنامه‌های Go شما هم قدرتمند و هم قابل درک باقی بمانند.

Share: