لطالما عُرِّفَت لغة البرمجة Go ببساطتها وتدفق التحكم الصريح. بينما جعل هذا النهج الـ Go ناجحة للغاية في برمجة الأنظمة والتزامن، إلا أنها غالباً ما تتناقض مع الأسلوب الإعلاني الموجز الموجود في اللغات الوظيفية مثل Haskell أو Scala أو حتى JavaScript. يفوت المطورون القادمون من هذه البيئات غالباً أناقة الدوال ذات الرتبة العليا مثل map و filter و reduce.
ومع ذلك، مع إدخال معاملات النوع (البرمجة العامة) في الإصدار 1.18 من Go، تغيرت المشهد. لم نعد مقصورين على كتابة كود مكرر لكل نوع بيانات. في هذه المقالة، سنستكشف كيفية تنفيذ نسخ قوية وعامة من أنماط البرمجة الوظيفية هذه في Go، مما يعيد استخدام الكود مع الحفاظ على أمان النوع الذي تشتهر به Go.
قوة البرمجة العامة في Go
قبل الغوص في التطبيقات المحددة، من الضروري فهم المشكلة التي تحلها البرمجة العامة في هذا السياق. قبل الإصدار 1.18 من Go، كان تنفيذ دالة map عامة يتطلب واجهات مثل interface{} أو كتابة دوال متعددة محملة بشكل زائد لأنواع مختلفة (على سبيل المثال، MapInt، MapString). كان هذا النهج طويلاً ويفتقر إلى أمان النوع في وقت الترجمة لمنطق التحويل.
مع البرمجة العامة، يمكننا تعريف دالة واحدة تقبل شريحة من أي نوع 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
}
هذا التنفيذ مباشر ولكنه قوي. فكر في سيناريو يكون لديك فيه شريحة من الأعداد الصحيحة تمثل معرفات المستخدمين، وتحتاج إلى تحويلها إلى شريحة من السلاسل النصية المنسقة. باستخدام الخريطة العامة الخاصة بنا:
ids := []int{1, 2, 3}
formatted := Map(ids, func(id int) string {
return fmt.Sprintf("User-%d", id)
})
// النتيجة: []string{"User-1", "User-2", "User-3"}
تنفيذ Filter
بينما تقوم map بتحويل العناصر، تقوم filter باختيارها. فهي تحتفظ فقط بالعناصر التي تُرجع دالة الشرط 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
})
// النتيجة: []float64{10, 20, 30, 40}
تنوع Reduce
المعروف أيضاً باسم fold أو accumulate، يُعد reduce غالباً الأكثر تعقيداً من بين الثلاثة. فهو يجمع كل عناصر مجموعة في قيمة واحدة. على عكس map و filter، يتطلب reduce قيمة متراكمة ابتدائية. يسمح لنا هذا بجمع الأرقام، أو دمج السلاسل النصية، أو بناء كائنات معقدة.
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)
// النتيجة: 35.5
الخاتمة
من خلال الاستفادة من قدرات Go العامة، يمكننا إحضار أناقة ووضوح البرمجة الوظيفية إلى قواعد أكواد Go الخاصة بنا. بينما ليست Go لغة وظيفية، فإن اعتماد هذه الأنماط يمكن أن يؤدي إلى كود أنظف وأكثر تجزئة وقابلية عالية للاختبار. المفتاح هو استخدام هذه التجريدات بحكمة — مع إعطاء الأولوية للقراءة والصيانة دون التضحية بالأداء والوضوح الذي يجعل Go فريدة.
عندما تدمج map و filter و reduce في مشاريعك، تذكر أن البساطة تظل في المقام الأول. استخدم هذه الأدوات للقضاء على التكرار وتوضيح النية، مما يضمن بقاء تطبيقات Go الخاصة بك قوية وسهلة الفهم.