كيفية استخدام Currying وComposition في JavaScript
مقدمة إلى Currying وComposition في JavaScript
تُعد مفاهيم Currying وComposition من الأسس المهمة في Functional Programming داخل JavaScript. ورغم أن الفكرة قد تبدو نظرية في البداية، فإنها تمنحك طريقة أكثر مرونة لبناء دوال صغيرة، واضحة، وقابلة لإعادة الاستخدام في أكثر من موضع داخل التطبيق.
تكمن أهمية هذا الأسلوب في أنه يساعد على تحويل الدوال من كتل كبيرة تؤدي أكثر من مهمة إلى وحدات صغيرة يمكن اختبارها وفهمها ودمجها بسهولة. وعندما تجمع بين Currying وComposition، يصبح بإمكانك إنشاء منطق برمجي متقدم انطلاقاً من أجزاء بسيطة للغاية.

ما هو Currying في JavaScript؟
يشير Currying إلى أسلوب برمجي يتم فيه تحويل الدالة التي تستقبل عدة معاملات إلى سلسلة من الدوال، بحيث تستقبل كل دالة معاملاً واحداً فقط في كل مرة. وبدلاً من استدعاء الدالة بالشكل التقليدي، يتم تمرير القيم على مراحل.
لنفترض أننا نريد حساب المسافة بين نقطتين بالإحداثيات {x1, y1} و{x2, y2}. في الحالة العادية سنكتب دالة تستقبل النقطتين معاً، ثم تُرجع المسافة بينهما.
مثال على دالة تقليدية لحساب المسافة
const distance = ( start, end ) => Math.sqrt( Math.pow(end.x-start.x, 2 ) + Math.pow(end.y-start.y, 2 ) );
console.log( distance( { x: 2, y: 2 }, { x: 11, y: 8 } ) );
// logs 10.816653826391969
هذا الأسلوب مباشر وواضح، لكن عند استخدام Currying ستصبح طريقة الاستدعاء مختلفة قليلاً.
كيف تبدو الدالة بعد تطبيق Currying؟
بدلاً من كتابة:
distance(start, end)
سنكتب:
distance(start)(end)
أي أننا نمرر المعامل الأول إلى دالة، فتُرجع دالة جديدة تنتظر المعامل الثاني.
const distance = function ( start ) {
return function ( end ) {
return Math.sqrt( Math.pow(end.x-start.x, 2 ) + Math.pow(end.y-start.y, 2 ) );
}
}
console.log( distance({ x: 2, y: 2 })({ x: 11, y: 8 }) );
// logs 10.816653826391969 again
هنا نلاحظ وجود closure، إذ تحتفظ الدالة الداخلية بقيمة start حتى بعد انتهاء تنفيذ الدالة الخارجية.
صياغة مختصرة باستخدام ES6 Arrow Functions
const distanceWithCurrying = ( start ) => ( end ) =>
Math.sqrt( Math.pow(end.x-start.x, 2 ) + Math.pow(end.y-start.y, 2 ) );
قد يبدو هذا أطول أو أقل فائدة من النسخة التقليدية، لكن القوة الحقيقية تظهر عندما تبدأ في إعادة استخدام الدوال وتركيبها مع دوال أخرى.
لماذا يُعد Currying مفيداً عملياً؟
الفائدة الكبرى من Currying تظهر عندما تريد تثبيت جزء من المدخلات وإنشاء دالة جديدة أكثر تخصصاً. هذا مفيد جداً عند التعامل مع البيانات المكررة أو عند العمل مع map وfilter وreduce.
لنفترض أننا نطوّر لعبة capture the flag ولدينا مجموعة لاعبين، وكل لاعب لديه موقع على الخريطة، ونريد حساب مسافة كل لاعب من العلم.
const players = [
{ name: 'Alice', color: 'aliceblue', position: { x: 3, y: 5 } },
{ name: 'Benji', color: 'goldenrod', position: { x: -4, y: -4 } },
{ name: 'Clarissa', color: 'firebrick', position: { x: -2, y: 8 } }
];
const flag = { x: 0, y: 0 };
الحل التقليدي باستخدام map
const distances = players.map( player => distance(flag, player.position) );
/***
* distances == [
* 5.830951894845301,
* 5.656854249492381,
* 8.246211251235321
* ]
***/
الحل باستخدام Curried Function
const distanceFromFlag = distanceWithCurrying(flag);
const curriedDistances = players
.map( player => player.position )
.map( distanceFromFlag );
/***
* curriedDistances == [
* 5.830951894845301,
* 5.656854249492381,
* 8.246211251235321
* ]
***/
في هذا المثال قمنا بتمرير flag مرة واحدة فقط، ثم حصلنا على دالة جديدة اسمها distanceFromFlag يمكن استخدامها مراراً لحساب المسافة من أي نقطة أخرى. هذا يجعل الكود أكثر ترتيباً وأسهل في إعادة الاستخدام.
ما هو Composition في JavaScript؟
Composition هو أسلوب يعتمد على دمج دوال صغيرة لبناء دوال أكبر وأكثر تعبيراً. والفكرة هنا بسيطة: بدلاً من كتابة منطق طويل داخل دالة واحدة، نقسّم العمل إلى دوال صغيرة تؤدي مهام دقيقة، ثم نربط هذه الدوال معاً.
هذا الأسلوب يُعد جوهرياً في Functional Programming لأنه يسهّل فهم الكود واختباره وصيانته مع مرور الوقت.
مدخل إلى filter callback functions
تسمح الدالة Array.prototype.filter() بتمرير دالة تُرجع true أو false لكل عنصر في المصفوفة. وإذا أعادت true، فسيتم الاحتفاظ بالعنصر.
const ages = [ 11, 14, 26, 9, 41, 24, 108 ];
function isEven ( num ) {
if (num % 2 === 0) {
return true;
} else {
return false;
}
}
const isEvenArrow = ( num ) => num % 2 === 0 ? true : false;
console.log( ages.filter(isEven) );
// [14, 26, 24, 108]
هذه الدالة بسيطة، لكنها مكتوبة لغرض محدد جداً. وإذا أردنا بناء منطق أكثر مرونة، فسنحتاج إلى دوال أكثر قابلية للتعميم.
بناء دوال Curried قابلة لإعادة الاستخدام
بدلاً من إنشاء دوال ثابتة لكل مقارنة، يمكننا بناء دوال عامة مثل isEqualTo وisGreaterThan. هذه الدوال تستقبل أولاً قيمة المقارنة، ثم تُرجع دالة تستقبل القيمة المراد اختبارها.
const isEqualTo = ( comparator ) => ( value ) => value === comparator;
const isGreaterThan = ( comparator ) => ( value ) => value > comparator;
const isSeven = isEqualTo(7);
const isOfLegalMajority = isGreaterThan(18);
بهذا الأسلوب نستطيع إنشاء دوال أكثر وصفاً للمعنى من دون تكرار المنطق البرمجي نفسه في كل مرة.
إضافة دوال صغيرة تساعد على Composition
const isNot = ( value ) => !value;
const isNotEqual = ( comparator ) => ( value ) => isNot( isEqualTo(comparator)(value) );
const isLessThanOrEqualTo = ( comparator ) => ( value ) => isNot( isGreaterThan(comparator)(value) );
قد تبدو هذه الطريقة مبالغاً فيها للوهلة الأولى، لكنها تبني مكتبة صغيرة من الوحدات البرمجية القابلة للتركيب، وهو ما يفتح الباب لكتابة كود أكثر مرونة ووضوحاً.
هل Composition يستحق هذا التعقيد؟
من الطبيعي أن تتساءل: لماذا أكتب كل هذا بدلاً من مقارنة مباشرة؟ لنقارن المثالين التاليين:
const isGreaterThan = ( comparator ) => ( value ) => value > comparator;
const isNot = ( value ) => !value;
const isLessThanOrEqualTo = ( comparator ) => ( value ) => isNot( isGreaterThan(comparator)(value) );
const isTooYoungToRetire = isLessThanOrEqualTo(65);
const ages = [ 31, 24, 86, 57, 67, 19, 93, 75, 63 ];
console.log( ages.filter(isTooYoungToRetire) );
مقابل:
console.log( ages.filter( num => num <= 65 ) );
النتيجة النهائية متقاربة، لكن أسلوب Composition يقدّم عدة مزايا مهمة:
- تسمية أكثر تعبيراً: اسم مثل
isTooYoungToRetireيشرح الغرض مباشرة. - سهولة الاختبار: يمكن اختبار كل دالة صغيرة بشكل منفصل.
- إعادة الاستخدام: تستطيع نقل هذه الدوال إلى مشاريع أخرى بسهولة.
- صيانة أفضل: تعديل جزء صغير لا يفرض إعادة كتابة المنطق كاملاً.
إنشاء دوال مركبة أكثر تقدماً
بعد امتلاك عدد من الدوال الصغيرة مثل isGreaterThan وisLessThan، يمكننا بناء دوال أعلى مستوى مثل isInRange.
const isInRange = ( minComparator ) => ( maxComparator ) => ( value ) =>
isGreaterThan(minComparator)(value) && isLessThan(maxComparator)(value);
const isTwentySomething = isInRange(19)(30);
هذا المثال عملي، لكنه لا يزال قابلاً للتحسين من حيث الوضوح. لذلك يمكننا بناء دالة عامة اسمها and لدمج عدة شروط معاً.
const and = ( conditions ) => ( value ) =>
conditions.every( condition => condition(value) );
const isInRange = ( min ) => ( max ) =>
and([ isGreaterThan(min), isLessThan(max) ]);
بهذا الشكل تصبح الدوال المركبة أكثر وضوحاً وقابلية للتوسعة. فإذا أردت مثلاً الحصول على الأعداد الزوجية بين 20 و40، يمكنك دمج isEven مع isInRange بسهولة شديدة.
مثال على دمج عدة شروط
const isLessThan = ( comparator ) => ( value ) => value < comparator;
const isEven = ( num ) => num % 2 === 0;
const and = ( conditions ) => ( value ) =>
conditions.every( condition => condition(value) );
const isInRange = ( min ) => ( max ) =>
and([ isGreaterThan(min), isLessThan(max) ]);
const isEvenAndBetween20And40 = and([
isEven,
isInRange(20)(40)
]);
أفضل ممارسات استخدام Currying وComposition
- ابدأ بدوال صغيرة وواضحة تؤدي مهمة واحدة فقط.
- استخدم أسماء وصفية للدوال لتجعل الكود أقرب إلى اللغة الطبيعية.
- لا تلجأ إلى Currying إذا كان سيزيد التعقيد دون فائدة عملية.
- استفد من Composition في عمليات التصفية والتحويل المتكررة.
- اختبر كل دالة بشكل منفصل قبل دمجها مع غيرها.
متى يكون هذا الأسلوب مناسباً؟
يكون Currying وComposition مناسبين بشكل خاص عندما تعمل على:
- تطبيقات تحتوي على منطق متكرر في معالجة البيانات.
- بناء أدوات داخلية أو مكتبات helper functions.
- مشاريع تعتمد على أسلوب Functional Programming.
- حالات تتطلب مرونة عالية في إنشاء دوال متخصصة انطلاقاً من دوال عامة.
أما إذا كان المشروع صغيراً جداً أو كانت المقارنات محدودة وبسيطة، فقد يكون الحل المباشر أكثر وضوحاً وأقل كلفة من ناحية القراءة.
الخلاصة التقنية
يوفر كل من Currying وComposition في JavaScript طريقة ذكية لبناء دوال مرنة، نظيفة، وقابلة لإعادة الاستخدام. ورغم أن هذا الأسلوب قد يبدو أكثر تعقيداً من الحلول التقليدية في البداية، فإنه يصبح ذا قيمة كبيرة مع اتساع المشروع وتزايد الحاجة إلى تنظيم المنطق البرمجي. تقنياً، أفضل استخدام لهذا النهج يكون عند بناء وحدات قابلة للاختبار والتركيب، خاصة في المشاريع التي تعالج البيانات بكثافة أو تعتمد على نمط Functional Programming بشكل واضح.