البرمجة الوظيفية في جافاسكريبت: دليل شامل للمطورين

دقائق القراءة: 11

يُعد التحكم في تعقيد البرمجيات أحد أصعب التحديات التي يواجهها المبرمجون. فبدون تخطيط دقيق، يمكن أن يتضخم حجم البرنامج وتعقيده إلى درجة تربك حتى منشئه. وكما قال أحد المؤلفين ببراعة: “فن البرمجة هو مهارة التحكم في التعقيد” – ماريجن هافربيكي.

في هذا المقال، سنغوص في مفهوم برمجي محوري يمكن أن يساعدك في السيطرة على التعقيد وكتابة برامج أفضل وأكثر كفاءة. بنهاية هذا الدليل، ستكون قد فهمت ماهية البرمجة الوظيفية، وأنواع الدوال المختلفة، ومبادئها الأساسية، وستكتسب فهماً أعمق لدوال الترتيب الأعلى (Higher-Order Functions).

نفترض أن لديك معرفة مسبقة بأساسيات الدوال في JavaScript، حيث لن يتم تغطية المفاهيم الأساسية للدوال في هذا المقال. إذا كنت ترغب في مراجعة سريعة للدوال، فقد كتبنا مقالاً مفصلاً حولها.

ما هي البرمجة الوظيفية (Functional Programming)؟

البرمجة الوظيفية (Functional Programming) هي نموذج برمجي أو أسلوب يعتمد بشكل كبير على استخدام الدوال النقية والمعزولة. وكما يوحي الاسم، فإن استخدام الدوال هو المكون الرئيسي للبرمجة الوظيفية. لكن مجرد استخدام الدوال لا يعني بالضرورة أنك تمارس البرمجة الوظيفية.

في البرمجة الوظيفية، نستخدم الدوال النقية (Pure Functions)، وهي دوال لا تسبب أي آثار جانبية (side effects). سنشرح معنى كل هذا بالتفصيل. ولكن قبل التعمق أكثر في المقال، دعنا نفهم بعض المصطلحات وأنواع الدوال الموجودة.

أنواع الدوال في جافاسكريبت

هناك أربعة أنواع رئيسية من الدوال التي يجب أن تكون على دراية بها في سياق البرمجة الوظيفية:

دوال الفئة الأولى (First-Class Functions)

في JavaScript، تُعد جميع الدوال دوال فئة أولى. هذا يعني أنه يمكن التعامل معها تماماً مثل أي متغير آخر. دوال الفئة الأولى هي دوال يمكن:

  • تعيينها كقيم للمتغيرات.
  • إعادتها من دوال أخرى.
  • تمريرها كوسائط (arguments) لدوال أخرى.

لننظر إلى هذا المثال لدالة تم تمريرها إلى متغير:

 const helloWorld = () => {
  console.log("Hello, World"); // Hello, World
 };
 helloWorld();

دوال الاستدعاء العكسي (Callback Functions)

دوال الاستدعاء العكسي هي دوال يتم تمريرها إلى دوال أخرى كوسائط، ويتم استدعاؤها بواسطة الدالة التي تم تمريرها إليها. ببساطة، هي دوال نكتبها كوسائط في دوال أخرى. لا يمكننا استدعاء دوال الاستدعاء العكسي مباشرة؛ بل يتم استدعاؤها عندما يتم استدعاء الدالة الرئيسية التي تم تمريرها إليها كوسيط.

دعنا نلقي نظرة على مثال:

 const testValue = (value, test) => {
  if (test(value)) {
   return `${value} passed the test`;
  } else {
   return `${value} did not pass the test`;
  }
 };

 const checkString = testValue('Twitter', string => typeof string === 'string');
 checkString; // Twitter passed the test

في هذا المثال، testValue هي دالة تقبل قيمة ودالة استدعاء عكسي تُسمى test. تُرجع الدالة testValue السلسلة النصية “value passed the test” إذا كانت القيمة تُرجع true عند تمريرها إلى دالة الاستدعاء العكسي. في هذه الحالة، دالة الاستدعاء العكسي هي الوسيط الثاني الذي مررناه إلى الدالة testValue، ويتم استدعاؤها عند استدعاء الدالة testValue.

دوال الترتيب الأعلى (Higher-Order Functions)

دوال الترتيب الأعلى هي دوال تقبل دوال أخرى كوسائط، أو تُرجع دالة. في هذا المقال، سنتعمق أكثر في دوال الترتيب الأعلى ولماذا تُعد ميزة قوية جداً. في الوقت الحالي، كل ما تحتاج معرفته هو أن هذه الأنواع من الدوال تتلقى دوال أخرى كوسائط أو تُرجع دوال.

الدوال اللامسمّاة (Anonymous Functions)

الدوال اللامسمّاة هي دوال لا تحمل اسماً ولا يمكن إعادة استخدامها بسهولة. عادةً ما تُكتب هذه الدوال عندما نحتاج إلى تنفيذ شيء مرة واحدة وفي مكان واحد فقط. مثال ممتاز على دالة لامسمّاة هو ما كتبناه سابقاً في المقال:

 const checkString = testValue('Twitter', value => typeof value === 'string');
 checkString; // Refer to previous code snippet

هنا، checkString هو متغير قيمته دالة. نمرر وسيطين إلى هذه الدالة: 'Twitter' هو الوسيط الأول، والثاني هو دالة لامسمّاة. هذه الدالة لا تحمل اسماً واحداً ولديها مهمة واحدة فقط: التحقق مما إذا كانت القيمة المعطاة هي سلسلة نصية (string).

مفهوم البرمجة الوظيفية في جافاسكريبت: صورة مضحكة لشخص يفكر في التعقيد

مبادئ البرمجة الوظيفية الأساسية

ذكرت سابقاً في المقال أن مجرد استخدام الدوال لا يعني بالضرورة أنك تمارس البرمجة الوظيفية. هناك بعض المبادئ التي نحتاج إلى فهمها لكي تفي برامجنا بمعايير البرمجة الوظيفية. دعنا نلقي نظرة عليها.

تجنب التغييرات الجانبية (Mutations) والآثار الجانبية (Side Effects)

المبدأ الأول للبرمجة الوظيفية هو تجنب تغيير الأشياء. يجب ألا تُغير الدالة أي شيء مثل متغير عام (global variable). هذا مهم جداً لأن التغييرات غالباً ما تؤدي إلى أخطاء (bugs). إذا قامت دالة بتغيير متغير عام، على سبيل المثال، فقد يؤدي ذلك إلى سلوك غير متوقع في جميع الأماكن التي يُستخدم فيها هذا المتغير.

المبدأ الثاني هو أن الدالة يجب أن تكون نقية (pure)، مما يعني أنها لا تحتوي على آثار جانبية. في البرمجة الوظيفية، تُسمى التغييرات التي تُجرى بـ التغييرات الجانبية (mutations)، وتُسمى النتائج بـ الآثار الجانبية (side effects). الدالة النقية لا تفعل أياً من الأمرين.

الدالة النقية ستُعطي دائماً نفس المخرجات لنفس المدخلات. إذا كانت الدالة تعتمد على متغير عام، فيجب تمرير هذا المتغير إلى الدالة كوسيط. هذا يسمح لنا بالحصول على نفس المخرجات لنفس المدخلات.

إليك مثال:

 const legalAgeInTheUS = 21;

 const checkLegalStatus = (age, legalAge) => {
  return age >= legalAge ? 'Of legal age.' : 'Not of legal age.';
 };

 const johnStatus = checkLegalStatus(18, legalAgeInTheUS);
 johnStatus; // Not of legal age
 legalAgeInTheUS; // 21

لاحظ كيف أن قيمة legalAgeInTheUS لم تتغير بعد استدعاء الدالة checkLegalStatus، مما يضمن نقاء الدالة.

التجريد (Abstraction)

التجريد يخفي التفاصيل ويسمح لنا بالحديث عن المشكلات على مستوى أعلى دون وصف جميع تفاصيل تنفيذ المشكلة. نستخدم التجريد في جميع جوانب حياتنا تقريباً، خاصة في الكلام. على سبيل المثال، بدلاً من قول “سأقوم بتبادل المال مقابل آلة تعرض صوراً متحركة مصحوبة بالصوت بمجرد توصيلها بالكهرباء”، من المرجح أن تقول “سأشتري تلفزيوناً”. في هذه الحالة، “أشتري” و”تلفزيون” هما تجريدان.

تُسهل هذه الأشكال من التجريد الكلام وتقلل من فرص قول الشيء الخطأ. ولكنك ستتفق معي على أنه قبل استخدام مصطلحات مجردة مثل “أشتري”، تحتاج أولاً إلى فهم معنى المصطلح والمشكلة التي يُجردها. تسمح لنا الدوال بتحقيق شيء مماثل.

يمكننا إنشاء دوال للمهام التي من المرجح أن نكررها مراراً وتكراراً. تسمح لنا الدوال بإنشاء تجريداتنا الخاصة. علاوة على إنشاء تجريداتنا الخاصة، تم بالفعل إنشاء بعض الدوال لنا لتجريد المهام التي من المرجح أن نقوم بها مراراً وتكراراً. لذا، سننظر في بعض دوال الترتيب الأعلى (Higher-Order Functions) الموجودة بالفعل لتجريد المهام المتكررة.

تطبيقات عملية لدوال الترتيب الأعلى: معالجة المصفوفات

تُعد دوال الترتيب الأعلى جزءاً لا يتجزأ من البرمجة الوظيفية، وتوفر JavaScript العديد منها لمعالجة المصفوفات بكفاءة.

تصفية المصفوفات باستخدام filter()

عند العمل مع هياكل البيانات مثل المصفوفات، من المرجح أن نجد أنفسنا في موقف نهتم فيه فقط بعناصر معينة في المصفوفة. للحصول على هذه العناصر، يمكننا بسهولة إنشاء دالة للقيام بالمهمة:

 function filterArray (array, test) {
  const filteredArray = [];
  for (let item of array) {
   if (test(item)) {
    filteredArray.push(item);
   }
  }
  return filteredArray;
 };

 const mixedArray = [1, true, null, "Hello", undefined, "World", false];
 const onlyStrings = filterArray(mixedArray, item => typeof item === 'string');
 onlyStrings; // ['Hello', 'World']

filterArray هي دالة تقبل مصفوفة ودالة استدعاء عكسي (callback function). تقوم بالمرور عبر المصفوفة وتضيف العناصر التي تجتاز الاختبار في دالة الاستدعاء العكسي إلى مصفوفة تُسمى filteredArray. باستخدام هذه الدالة، يمكننا تصفية مصفوفة وإرجاع العناصر التي نهتم بها، كما في حالة mixedArray.

تخيل لو كان لدينا 10 برامج مختلفة، وفي كل برنامج احتجنا إلى تصفية مصفوفة. عاجلاً أم آجلاً، سيصبح الأمر مرهقاً للغاية لإعادة كتابة نفس الدالة مراراً وتكراراً. لحسن الحظ، فكر شخص ما في هذا بالفعل. تحتوي المصفوفات على دالة قياسية تُسمى filter. تُرجع مصفوفة جديدة تحتوي على العناصر التي اجتازت الاختبار الذي نقدمه.

 const mixedArray = [1, true, null, "Hello", undefined, "World", false];
 const stringArray = mixedArray.filter(item => typeof item === 'string');
 stringArray; // ['Hello', 'World']

باستخدام دالة filter القياسية، تمكنا من تحقيق نفس النتائج التي حققناها عندما عرفنا دالتنا الخاصة في المثال السابق. لذا، تُعد دالة filter تجريداً للدالة الأولى التي كتبناها.

تحويل عناصر المصفوفات باستخدام map()

تخيل سيناريو آخر حيث لدينا مصفوفة من العناصر، ولكننا نرغب في إجراء عملية معينة على جميع العناصر. يمكننا كتابة دالة للقيام بذلك نيابة عنا:

 function transformArray (array, test) {
  const transformedArray = [];
  for (let item of array) {
   transformedArray.push(test(item));
  }
  return transformedArray;
 };

 const ages = [12, 15, 21, 19, 32];
 const doubleAges = transformArray(ages, age => age * 2);
 doubleAges; // [24, 30, 42, 38, 64];

بهذه البساطة، أنشأنا دالة تمر عبر أي مصفوفة معطاة وتحول جميع العناصر في المصفوفة بناءً على دالة الاستدعاء العكسي التي نقدمها. ولكن مرة أخرى، سيصبح هذا مملاً إذا اضطررنا إلى إعادة كتابة الدالة في 20 برنامجاً مختلفاً. مرة أخرى، فكر شخص ما في هذا نيابة عنا، ولحسن الحظ، تحتوي المصفوفات على دالة قياسية تُسمى map تقوم بنفس الشيء بالضبط. تُطبق دالة الاستدعاء العكسي على جميع العناصر في المصفوفة المعطاة ثم تُرجع مصفوفة جديدة.

 const ages = [12, 15, 21, 19, 32];
 const doubleAges = ages.map(age => age * 2);
 doubleAges; // [24, 30, 42, 38, 64];

اختزال المصفوفات باستخدام reduce()

إليك سيناريو آخر: لديك مصفوفة من الأرقام، ولكنك ترغب في حساب مجموع جميع هذه الأرقام وإرجاعه. بالطبع، يمكنك كتابة دالة للقيام بذلك نيابة عنك.

 function reduceArray (array, test, start) {
  let sum = start;
  for (let item of array) {
   sum = test(sum, item);
  }
  return sum;
 }

 let numbers = [5, 10, 20];
 let doubleNumbers = reduceArray(numbers, (a, b) => a + b, 0);
 doubleNumbers; // 35

على غرار الأمثلة السابقة التي نظرنا إليها للتو، تحتوي المصفوفات على دالة قياسية تُسمى reduce لها نفس المنطق مثل الدالة التي كتبناها أعلاه. تُستخدم دالة reduce لاختزال مصفوفة إلى قيمة واحدة بناءً على دالة الاستدعاء العكسي التي نقدمها. كما أنها تقبل وسيطاً ثانياً اختيارياً يحدد من أين نريد أن تبدأ العملية في دالة الاستدعاء العكسي.

دالة الاستدعاء العكسي التي نقدمها في دالة reduce تحتوي على وسيطين. الوسيط الأول هو العنصر الأول في المصفوفة افتراضياً، أو القيمة الأولية التي نقدمها كوسيط ثانٍ لدالة reduce. الوسيط الثاني هو العنصر الحالي في المصفوفة.

 let numbers = [5, 10, 20];
 let doubleNumbers = numbers.reduce((a, b) => a + b, 10);
 doubleNumbers; // 45
 // المثال أعلاه يستخدم دالة reduce لجمع جميع العناصر في المصفوفة بدءاً من القيمة 10.

دوال مصفوفات مفيدة أخرى

بالإضافة إلى الدوال الأساسية المذكورة أعلاه، توفر JavaScript العديد من دوال المصفوفات المفيدة الأخرى:

Array.some()

تحتوي جميع المصفوفات على دالة some التي تقبل دالة استدعاء عكسي. تُرجع true إذا اجتاز أي عنصر في المصفوفة الاختبار المعطى في دالة الاستدعاء العكسي. بخلاف ذلك، تُرجع false:

 const numbers = [12, 34, 75, 23, 16, 63];
 console.log(numbers.some(item => item < 100)); // true

Array.every()

دالة every هي عكس دالة some. تقبل أيضاً دالة استدعاء عكسي وتُرجع true إذا اجتازت جميع العناصر في المصفوفة الاختبار المعطى في دالة الاستدعاء العكسي. بخلاف ذلك، تُرجع false:

 const numbers = [12, 34, 75, 23, 16, 63];
 console.log(numbers.every(item => item < 100)); // true

Array.concat()

دالة concat، وهي اختصار لـ concatenate (دمج)، هي دالة مصفوفة قياسية تقوم بدمج أو ربط مصفوفتين وتُرجع مصفوفة جديدة:

 const array1 = ['one', 'two', 'three'];
 const array2 = ['four', 'five', 'six'];
 const array3 = array1.concat(array2);
 array3; // [ 'one', 'two', 'three', 'four', 'five', 'six' ]

Array.slice()

دالة slice هي دالة مصفوفة تقوم بنسخ عناصر مصفوفة من فهرس معين وتُرجع مصفوفة جديدة بالعناصر المنسوخة. تقبل دالة slice وسيطين:

  • الوسيط الأول يستقبل الفهرس الذي تبدأ منه عملية النسخ (شامل).
  • الوسيط الثاني يستقبل الفهرس الذي تتوقف عنده عملية النسخ (حصري).

تُرجع مصفوفة جديدة بالعناصر المنسوخة من الفهرس البدائي (شامل) إلى الفهرس النهائي (حصري). ملاحظة هامة: تستخدم دالة slice الفهرسة الصفرية (zero-indexing) في JavaScript، لذا فإن فهرس العنصر الأول في المصفوفة هو 0.

 const numbers = [1, 2, 3, 4, 5, 7, 8];
 console.log(numbers.slice(1, 4)); // [ 2, 3, 4 ]

في المثال أعلاه، تبدأ عملية النسخ من الفهرس 1 (العنصر ‘2’) وتتوقف قبل الفهرس 4 (العنصر ‘5’)، مما ينتج عنه المصفوفة [2, 3, 4].

الخاتمة

نأمل أن تكون قد استمتعت بقراءة هذا المقال وتعلمت شيئاً جديداً في نفس الوقت. هناك العديد من دوال المصفوفات والسلاسل النصية الأخرى التي لم نذكرها في المقال. إذا كنت ترغب في ذلك، خصص بعض الوقت لإجراء بعض البحث حول تلك الدوال لتعميق فهمك.

تُعد البرمجة الوظيفية أداة قوية في ترسانة أي مطور JavaScript، حيث تُمكنك من كتابة كود أكثر قابلية للقراءة، والصيانة، والاختبار. من خلال تبني مبادئها واستخدام دوال الترتيب الأعلى، يمكنك بناء تطبيقات قوية ومرنة.

الخلاصة التقنية

تُقدم البرمجة الوظيفية في JavaScript نهجاً متطوراً لمعالجة التعقيد البرمجي، وذلك بالتركيز على الدوال النقية وتجنب الآثار الجانبية. إن فهم دوال الفئة الأولى، والاستدعاء العكسي، ودوال الترتيب الأعلى ليس مجرد تحسين لأسلوب البرمجة، بل هو ضرورة في بناء تطبيقات حديثة وقابلة للتوسع. الدوال مثل map()، filter()، و reduce() هي تجريدات قوية تُسهل التعامل مع البيانات وتُعزز من قابلية الكود لإعادة الاستخدام، مما يُسهم في إنتاجية أعلى وجودة أفضل للبرمجيات. إن تبني هذه المبادئ يُمكن المطورين من كتابة كود أكثر وضوحاً، وأقل عرضة للأخطاء، وأسهل في الصيانة والاختبار، وهو ما يُعد حجر الزاوية في تطوير الويب المعاصر.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *