أرى شخصياً أن مُصطلح البرمجة الوظيفية مُضلل بعض الشيء، فهو ترجمة مباشرة لـ Functional Programming. إلا أن الـ function في Functional هي الدالة أو التابع الرياضي، وليست الترجمة الحرفية: ‘الوظيفة’.
فكما تعتبر الكلاسات والكائنات أساس البرمجة الغرضية، تعتبر التوابع أساس البرمجة الوظيفية. فكل شيء في البرنامج يُمثّل بتابع ويُتعامل معه على هذا الأساس. ومايقوم به البرنامج الوظيفي هو حساب قيمة تلك التوابع.
تعود فكرة أن البرامج هي توابع تربط المُدخلات بالمُخرجات إلى أن أغلبها يقوم على فكرة قراءة الدخل (input) وتنفيذ عمليات بناءاً عليه وثم تعرض تلك النتائج (output). حتى تلك التي قد لاتطلب منك ادخال المعلومات مباشرة، تقوم بقراءتها إما من القرص أو من الانترنت أو من الحساسات. وتعرض البرامج نتائجها إما عبر الشاشة مباشرة، أو عبر الكتابة للقرص، أو كما في أبسط الحالات عبر إخبار نظام التشغيل بأنها انتهت بدون مشاكل. حتى برنامج الـ C البسيط هذا يفعل ذلك.
int main (void)
{
return 0;
}
💡 تعليمة الـ ;return 0
تخبر نظام التشغيل أن البرنامج انتهى بدون مشاكل
ماذا نقصد بأن كل شيء هو تابع؟ Link to heading
في لغات البرمجة التقليدية، نتكلم عن متحولات تحتوي قِيماً وتوابع تقوم بحسابات. في هذا المثال تتغير قيمة x
لأنه متحوّل تتحول قيمته.
double x = 5; // هو متحول x
x = Math.pow(x,4); // هو تابع pow
أما في البرمجة الوظيفية فلا يوجد تفريق بين الاثنين، فنقول أن الرمز x
هو تابع يُرجع القيمة 5
مثلاً. دائماً خمسة.
x :: Int -- يُرجع قيمة عدد صحيح x
x = 5 -- يرجع دائما 5 x
الكود السابق مكتوب بلغة Haskell، وكود الجافا التالي يُعادله.
int x() {
return 5;
}
هذا يعني أن المتحولات في البرنامج لاتتغير قيمتها أبداً ما قد يبدو غريباً في بادئ الأمر، الأمر الذي يعني أن البرنامج الوظيفي يمكن تشغيله بالتوازي، فلا آثار جانبية على المتحولات (لأنها لاتتغير).
الكلاسات (Classes) والواجهات (Interfaces) هي اللبنات الأساسية في البرمجة الغرضية (OOP) وتُربط ببعضها بالوراثة. بينما البرمجة الوظيفية تصف العمليات والإجراءات كتوابع على البيانات. وتُربط ببعضها بتركيب الدوال.
مثال Link to heading
لنكتب معاً برنامجاً وظيفياً يُفلتر مصفوفة أرقام من الأرقام الفردية، أي أنه يَطبع الأرقام الزوجية فقط. سنستخدم لغة جافاسكربت لأنها بسيطة وسهلة الفهم.
نبدأ بتعريف مصفوفة تحتوي الأرقام من 1 لـ 10.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
💡لاحظ أن المصفوفة معرفة كثابت const
، لأننا نريد ضمان أنها لن تتغير.
بعدها نقوم بتعريف تابع بسيط يُخبرنا فيما لو كان العدد زوجي أم سلبي.
const isEven = function(number) {
return number % 2 == 0;
}
كما سبق، نُعرّف isEven
كثابت من نوع تابع function
يأخذ الرقم number
كمُدخل ويُرجع قيمة بوليانية.
💡 كان بإمكاننا تعريف isEven
كتابع مباشرة function isEven(number)
، ولكن تعريفه كثابت أقرب نحوياً للأسلوب الوظيفي.
وأخيراً نُطبق عملية الفلترة التي تستدعي التابع isEven
على كل عنصر من عناصر المصفوفة numbers
لترى فيما لو كان يحقق الشرط أم لا.
numbers.filter(isEven);
بما أننا نتبع الأسلوب الوظيفي، فعملية الفلترة filter(isEven)
لَن تغيّر من عناصر numbers
، مايدعونا لحفظها في ثابت جديد.
const myNumbers = numbers.filter(isEven);
لنقل أنك الآن تريد فلترة العناصر الفردية وحذفها، وضرب العناصر المتبقية بـ 3.
فلنقم بتعريف التابع يضرب العدد بـ 3.
const triple = function(number){
return number * 3;
}
ماتبقى هو تطبيق التابع triple
على نتيجة الفلترة
const myNumbers = numbers.filter(isEven).map(triple);
فيصبح البرنامج الكامل كما يلي:
const isEven = function(number) {
return number % 2 == 0;
}
const triple = function(number){
return number * 3;
}
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const myNumbers = numbers.filter(isEven).map(triple);
console.log(myNumbers);
هذا البرنامج مثال بسيط على طريقة التفكير والكتابة الوظيفية، ومع ذلك تجدر الإشارة إلى أنه
- مُختصر: كل سطر يقوم بشيء محدد. لاوجود لعمليات معقدة تستلزم عدة سطور.
- جاف: لا تكرار. مع أن كلاً من
filter
وmap
يتشابهان، إلا أنهما مفصولان عن بعضهما. (قارن فيما لو كانا حلقتيfor
). - سهل التعديل: كل شي عبارة عن تابع، فمثلاً تعديل شرط الفلترة
filter
سهل ومحصور بتابعisEven
. - لا آثار جانبية: لاتوجد أية متحولات عامة (Global Variables). لاتوجد أية متحولات البتة. المصفوفة
numbers
لم تتغير بعد كل العمليات عليها. - لانعيد اختراع العجلة: حتى أبسط الأمور في البرنامج كحلقة
for
للمرور على عناصر مصفوفة يمكن أن تكون مكلفة، ولكن باستخدام توابع اللغة الجاهزة كـfilter
وmap
، لا داعي للقلق على أداء برنامجك فهذه التفاصيل تتكفل لك بها لغة البرمجة.
خلاصة Link to heading
قد تبدو البرمجة الوظيفية مقيِّدة في بادئ الأمر، فانعدام القدرة على تغيير قيم المتحولات يحتاج وقتاً للاعتياد عليه. الأمر الآخر هو عدم وجود مقابل مباشر للبُنى التقليدية كالجمل الشرطية (Conditionals) أو الحلقات (Loops). لكن هذي القيود تأتي مُقابل إيجابيات تفتقدها البرمجة التقليدية. التفكير بالطريقة الوظيفية والكتابة بها (وإن لم تكن بشكل كلّي) سيمرّنك على التفكير بالمتحولات وقيمها التي تتغير، أو فيما يحتاج كودك إلى تبسيط أو إذا ماكان هذا المتحول العام ضرورياً. قد لاتجد نفسك مطلقاً في مشروع بلغة برمجة وظيفية، ولكن الانضباط البرمجي الذي تفرضه البرمجة الوظيفية سيجعل منك مبرمجاً أفضل.