موضة زائلة أم جوهرة راسخة؟
كثيرا ما تظهر شطحات في عالم البرمجة لا تلبث أن تضمحل وفي هذه المقالة سننظر إلى أي مدى البرمجية الدالية هي موضة أم أنها جوهرة راسة أعيد اكتشافها.
ما هي البرمجة الدالية FP؟
الدالة function الرياضية هي علاقة تربط كل قيمة في مجالها بقيمة واحدة فقط في مداها. يعني تأخذ مدخلة ما فتعطينا مخرجا ما. وفي البرمجة هي جزء من الكود يتم استدعاؤه وتمرير مدخلات له (تسمى معاملات arguments) فيعيد لنا مخرجات return.
الدالة الصرفة النقية pure function في البرمجة هي الدالة التي دائما تعيد نفس المخرجات إذا أعطيتها نفس المدخلات (بمعنى أن عملها لا يعتمد على آثار جانبية كالحالة والمتغيرات غير المحلية/العامة والخصائص الستاتيكية وأجهزة الإدخال والتخزين ... وغيرها) وبدورها لا تعمل هذه الدالة آثارا جانبية غير إعادة النتيجة (فلا تكتب في ملف ولا في متغيرات عامة ...إلخ). من أمثلة ذلك دالة الجذر التربيعي تعطيها رقم فتعطيك جذره أو دالة طول النص تعطيها نص فتعيد لك كم حرف في ذلك النص وأيضا دالة أكبر عدد تعطيها مجموعة أعداد فتعطيك أكبر واحد بينهم.
تقوم فلسفة البرمجة الدالية functional programming paradigm على بناء البرمجيات من خلال تركيب دوال صرفة (التي ذكرناها في الفقرة السابقة) وتتجنب الحالات المشتركة والبيانات القابلة للتغيير mutable data والآثار الجانبية. وهي طريقة تصريحية declarative لا أمرية imperative.
افعل ولا تفعل
لا تفعل: بما انها ليست صيغة "أمرية" عليك التخلي عن الكثير من الصيغ التي تعودت عليها هذه قائمة بأشياء يفترض أن تتخلى عنها
- تدفق السيطرة control flow
- الجمل الشرطية والتفريعات
- الحلقات التكرارية
- المتغيرات البينية
- المؤقتة في الحلقات التكرارية
- تمرير البيانات للدوال مبكرا
- استدعاء الدوال داخل الدوال.
- التغيير mutability مثلا تغير خصائص كائن item.color = red لأن هذا يعتبر أثر جانبي. (البرمجة الدالية تعتمد على الثبات immutability)
- معالجة الاستثناءات exception عبر try/catch
افعل: هذه بعض الأشياء التي ستجدها كثيرا
- تدفق البيانات data flow يعني تعمل سمكرة ثم تفتح حنفية البيانات
- تركيب الدوال function composition
- استعمال المقابلة mapper مكان الحلقات التكرارية loop وهي دالة تعيد نتيجة تطبيق دالة على قائمة أشياء
- استعمال الترشيح/الفلترة filter مكان الشرط وهي دالة تأخذ قائمة وتعيد جزء منها
- الاختزال reduction وهي تأخذ قائمة وتطبق دالة على أول عنصرين (من اليمين أو اليسار) ثم آخر نتيجة مع العنصر التالي وهكذا ومن أمثلتها دالة مجموع القائمة أو مضروبها أو قيمتها القصوى.
- الدوال العليا higher order functions وهي دالة تعيد دالة
- اختزال المتغيرات البينية من خلال
- تأجيل تمرير البيانات: تركيب دالتين يعيد دالة جديدة دون ذكر متغير بيني مثلا f و g دالتين تركيبها f ∘ g ولا داع لذكر متغير بيني مثل x
- الدوال المدبوغة أو المكرية currying (نسبة لعالم الرياضيات Haskell Curry) أو الاستدعاء الجزئي partial calling: استدعاء دالة تأخذ 3 مدخلات مع تمرير مدخلتين لها يعيد دالة جديدة تستقبل المدخلة الناقصة
- استعمال ال Monad عوضا عن معالجة الاستثناءات مثل Maybe و Just
اللغات أو المكتبات المصصمة لأجل البرمجة الدالية النقية يجب أن تتصف بما يلي:
- الثبات immutable
- الدوال المكرية auto-curried
- الدوال الكرارة أولا iteratee-first
- البيانات آخرا data-last methods.
إن أشهر اللغات التي تطبق هذه المبادئ هي لغة البرمجة هاسكل Haskell والمسمى أيضا على اسم نفس عالم الرياضيات الذي ذكرناه عندما تحدثنا عن الدوال المكرية Curried
أمثلة تقليدية غير نقية
أمثلة من جافاسكربت
في جافاسكربت هناك مكتبتان شهيرتان هما underscore و lodash (تتوفر بنسخة حديث تدعم fp) لكنهما في صورتهما التقليدية مجرد سكر فوق صيغة .map و .filter و .reduce التي أصبحت تأتي مسبقا مع جافاسكربت. وإذا تصفحت وثائق المكتبتين ستجد طوفان من الدوال التي المفروض أنها تعمل كل شيء. هذا مثال على map و filter و reduce واستعمال دوال السهم
> const a=[1,2,3,4,5,6,7]; > a.map(i=>i*3) [ 3, 6, 9, 12, 15, 18, 21 ] > a.map(i=>i*3).filter(i=>i%2==1) [ 3, 9, 15, 21 ] > a1.map(i=>i*3).filter(i=>i%2==1).reduce((i,j)=>i+j) 48
شرح المثال بسيط جدا بدأنا بقائمة فيها 7 أعداد ثم ضربنا كل عدد في 3 عبر دالة سهمية تأخذ مدخلة i وتعيدها مضروبة في 3 ثم قمنا بتصفية النتائج الفردية (التي باقي قسمتها على 2 يساوي 1) فحصلنا على قائمة فيها 4 أعداد لأننا حذفنا 6 و 12 وأخيرا جمعناهم عبر reduce فحصلنا على قيمة واحدة هي 48.
لو أن عندنا قائمة بمستخدمين
ونريد حساب متوسط أعمارهم. ثم متوسط أعمار الرجال ومتوسط أعمار النساء. هذا جزء من الحل بطريقة دالية
واحدة من الدوال أعلاه ليست دالة صرفة لابد أنك عرفت أيها؟ نعم التي فيها تاريخ اليوم! بأي حال سأترك لكم متابعة الحل.
لو أن عندنا قائمة بمستخدمين
const users = [ {name: "omar", "gender": "male", "dob": "1992-05-17"}, {name: "jane", "gender": "female", "dob": "1997-01-15"}, ... ]
ونريد حساب متوسط أعمارهم. ثم متوسط أعمار الرجال ومتوسط أعمار النساء. هذا جزء من الحل بطريقة دالية
users.map(i=>i.dob).map(i=>i.substr(0,4)).map(i=>parseInt(i)).map(i=>(new Date).getFullYear()-i)
واحدة من الدوال أعلاه ليست دالة صرفة لابد أنك عرفت أيها؟ نعم التي فيها تاريخ اليوم! بأي حال سأترك لكم متابعة الحل.
أمثلة من بايثون
في بايثون هناك مكتبات أساسيات للقيام بالبرمجة الدالية هي
إلى جانب filter و map و zip وتراكيب القوائم على الطاير list comprehension. مسألة الأعداد من 1 حتى 7 السابقة في بايثون بالطريقة الدالية تصبح هكذا
- functools مثل partial و reduce
- itertools مثل cycle و repeat و chain و groupby و islice
- collections مثل defaultdict
- طرف ثالث: numpy و tensorflow
إلى جانب filter و map و zip وتراكيب القوائم على الطاير list comprehension. مسألة الأعداد من 1 حتى 7 السابقة في بايثون بالطريقة الدالية تصبح هكذا
>>> a=range(1,8)
>>> sum(filter(lambda i: i%2==1, map(lambda i: i*3, a)))
48
وإن كانت صيغة القوائم أجمل وأسهل للفهم
يمكن مراجعة موقع وثائق بايثون للأمثلة.
>>> sum([ i*3 for i in a if i%2==1]) 48
أمثلة نقية
جافاكربت مع Ramda ونحقق الشروق التي ذكرناها أعلاه كاملة. ولنبدأ بمثال الأعداد السبعة السابق
const f=R.pipe( R.map(R.multiply(3)), R.filter( i => i%2==1), R.sum ); const a=[1,2,3,4,5,6,7]; f(a);
لاحظ أننا عرفنا الدالة f ثم مررنا لها البيانات a. لنأخذها بالتدريج
استطعنا الضرب في 3 دون استعمال متغير مؤقت لأن الدالة R.multiply (مثل كل دوال Ramda) دالة مكرية curried يعني هي تستقبل رقمين وتضربهما لكن إذا أعطيتها رقم واحد فإنها تعيد دالة تستقبل الرقم الثاني
وقمنا بالعمل حلقة تكرارية تدور على قائمة الأرقام عبر المقابلة map والتي تأخذ الدالة أولا ثم قائمة العناصر ثانيا ولأنها مكرية يمكن تأجيل تمرير العناصر لها. تركيب عملية التصفية بعد عمل map يكون عبر R.pipe أو R.compose والفرق بينهما هو الترتيب.
استعمال دالة سهمية لإيجاد باقي القسمة ليس الطريقة المثالية في عالم البرمجية الدالية لأنه يستعمل متغير مؤقت i ويفضل اختزاله هكذا وهنا يمكننا استعمال الدالة المكرية R.modulo والتي تأخذ رقمين وتحسب باقي قسمة الأول على الثاني. لكن لأننا نريد أن نثبت المقسوم عليه الثاني ونكري المقسوم الأول فإننا نستعمل الشرطة التحتية __ والتي تعوض المتغير المكري
فتصبح الدالة هكذا
> const a=[1,2,3,4,5,6,7]; > R.multiply(3)(7); 21 > R.map(R.multiply(3), a); [3, 6, 9, 12, 15, 18, 21] > R.map(R.multiply(3))(a); [3, 6, 9, 12, 15, 18, 21] > R.filter(i=>i%2==1, R.map(R.multiply(3))(a)); [ 3, 9, 15, 21 ]
استطعنا الضرب في 3 دون استعمال متغير مؤقت لأن الدالة R.multiply (مثل كل دوال Ramda) دالة مكرية curried يعني هي تستقبل رقمين وتضربهما لكن إذا أعطيتها رقم واحد فإنها تعيد دالة تستقبل الرقم الثاني
وقمنا بالعمل حلقة تكرارية تدور على قائمة الأرقام عبر المقابلة map والتي تأخذ الدالة أولا ثم قائمة العناصر ثانيا ولأنها مكرية يمكن تأجيل تمرير العناصر لها. تركيب عملية التصفية بعد عمل map يكون عبر R.pipe أو R.compose والفرق بينهما هو الترتيب.
استعمال دالة سهمية لإيجاد باقي القسمة ليس الطريقة المثالية في عالم البرمجية الدالية لأنه يستعمل متغير مؤقت i ويفضل اختزاله هكذا وهنا يمكننا استعمال الدالة المكرية R.modulo والتي تأخذ رقمين وتحسب باقي قسمة الأول على الثاني. لكن لأننا نريد أن نثبت المقسوم عليه الثاني ونكري المقسوم الأول فإننا نستعمل الشرطة التحتية __ والتي تعوض المتغير المكري
> R.modulo(R.__, 2)(7) 1
const f=R.pipe( R.map(R.multiply(3)), R.modulo(R.__, 2), R.sum ); const a=[1,2,3,4,5,6,7]; f(a);
لاحظ أننا عرفنا الدالة f دون حلقات تكرارية ودون متغيرات محلية ولا جمل شرطية ... فقط تركيب دوال في بعضها البعض. يمكنك تجربة ذلك بنفسك بشكل تفاعلي هنا
الآن بخصوص مسألة الأعمار
تجد المثال كاملا هنا وهنا حل بديل بطريقة R.compose مكان R.pipe
الآن نأتي للفرع التالي من السؤال وهو حساب متوسط عمر النساء ومتوسط عمر الرجال. لفصلهم يمكن استعمال
الدالة partition تعيد قائمتين الأولى تحقق الشرط والثانية لا تحققه والشرط هنا أن قيمة gender هي female. لنفرض أننا سمينا الدالة الكبرى f باسم ageMean والثانية byGender فإن حل السؤال هو
وكاملا يكون الحل هكذا
تجد الحل كاملا هنا
هناك الكثير من الدوال عليك تعلمها مثل applySpec و splitEvery و maxBy و دوال خاصة بالوعود مثل andThen و otherwise و composeP و pipeP ومكتبة Maybe و هذا ملخص و هذه أمثلة وهناك عالم كامل خيالي. وهذا رابط ذو صلة
وماذا بعد؟ يمكنك أن تتخيل الاحداث events التي تحصل كسيال stream عبارة عن سلسلة يمكن العمل عليها بالبرمجة الدالية. يعني يمكنك سمكرة الواجهة الرسومية كاملة بطريقة تصريحية بطريقة مشابهة لما شرحناه هنا باستعمال Rx سواء RxJs في جافاسكربت أو RxJava لجافا وأندرويد لكنها أعم وتعمل على عدة قنوات متوازية وترسم فيما يعرف بمخططات الؤلؤ pearl diagram
لما سبق أنا لا أحب بناء تطبيقات بالكامل بالبرمجة الدالية بل فقط الاستفادة منها في الأجزاء تكون البرمجة الدالية مناسبة أكثر. هذه ليست أول مرة أقول عن شيء له مجتمع كبير وراؤه شركات كبيرة مثل غوغل أنه فكرة سيئة! أنا أرى (وهذا رأيي الشخصي) أنها لا تصلح لبناء تطبيق من أ إلى ي لكنها تصلح لتطبق على أجزاء معينة فقط عندما تكون البرمجة الدالية أنسب لتلك الأجزاء. ولا ليس الموضوع أن ذلك لأني لا أفهم الرياضيات أو لا أفهم هاسكل ... بالأصل تخصصي رياضيات! سأكون ذلك الطفل الذي يقول الإمبراطور يسير عاريا.
إن قمت باستعمال البرمجة الدالية لتركيب دوال معقدة وكان جزء من تلك التراكيب التي عملتها له معنى ويمكن اختباره بشكل منفصل احفظها باسم يسهل فهمه كما عملنا ageMean و byGender بشكل منفصل
عوضا عن تركيب ذلك كله دفعة واحدة. على الأقل يمكنك عمل اختبار وحدة للدالتين كل واحدة على حدى.
الآن بخصوص مسألة الأعمار
const year=(new Date).getFullYear(); const f=R.pipe( R.map(R.pipe( R.prop("dob"), R.invoker(2, 'substr')(0, 4), parseInt, R.subtract(year) )), R.mean );
الآن نأتي للفرع التالي من السؤال وهو حساب متوسط عمر النساء ومتوسط عمر الرجال. لفصلهم يمكن استعمال
const byGender=R.partition(R.propEq("gender", "female"))
الدالة partition تعيد قائمتين الأولى تحقق الشرط والثانية لا تحققه والشرط هنا أن قيمة gender هي female. لنفرض أننا سمينا الدالة الكبرى f باسم ageMean والثانية byGender فإن حل السؤال هو
const [femaleAgeMean, maleAgeMean]=R.pipe( byGender, R.map(ageMean) )(users);
وكاملا يكون الحل هكذا
const f=R.compose( R.map(R.compose( R.mean, R.map(R.compose( R.subtract(year), parseInt, R.invoker(2, 'substr')(0, 4), R.prop("dob"), )))), R.partition(R.propEq("gender", "female")) );
تجد الحل كاملا هنا
هناك الكثير من الدوال عليك تعلمها مثل applySpec و splitEvery و maxBy و دوال خاصة بالوعود مثل andThen و otherwise و composeP و pipeP ومكتبة Maybe و هذا ملخص و هذه أمثلة وهناك عالم كامل خيالي. وهذا رابط ذو صلة
وماذا بعد؟ يمكنك أن تتخيل الاحداث events التي تحصل كسيال stream عبارة عن سلسلة يمكن العمل عليها بالبرمجة الدالية. يعني يمكنك سمكرة الواجهة الرسومية كاملة بطريقة تصريحية بطريقة مشابهة لما شرحناه هنا باستعمال Rx سواء RxJs في جافاسكربت أو RxJava لجافا وأندرويد لكنها أعم وتعمل على عدة قنوات متوازية وترسم فيما يعرف بمخططات الؤلؤ pearl diagram
وقعنا في الفخ!
في مثال أعمار قائمة من المستخدمين رأينا كيف حصلنا على المطلوب منا دون حلقات تكرارية ودون جمل شرطية وتفريعات ودون خطوات أمرية ودون متغيرات بينية. ركبنا الدوال هذه كلها في دالة واحدة ثم مررنا لها القائمة. سمكرة ثم تدفق للبيانات في هذه السمكرة. لكن كيف نتتبع البرنامج trace؟ كيف نمشي فيه خطوة بخطوة step by step؟ أين نضع نقاط التوقف break points؟ بما أنها طريقة تصريحية declarative وليس أمرية imperative فلا يوجد خطوات ولا يوجد بيانات بينية. لذا لا يمكنك تمحيص الكود debug. عندما تعمل هنيئا لك! لكن إذا حصلت مشكلة فحظا أوفر لا يوجد طريقة للتتبع والتمحيص. مثلا لو كانت بعض البيانات ليس لها تاريخ ميلاد أو تاريخ ميلاد خاطىء أو غير نصي...إلخ.لما سبق أنا لا أحب بناء تطبيقات بالكامل بالبرمجة الدالية بل فقط الاستفادة منها في الأجزاء تكون البرمجة الدالية مناسبة أكثر. هذه ليست أول مرة أقول عن شيء له مجتمع كبير وراؤه شركات كبيرة مثل غوغل أنه فكرة سيئة! أنا أرى (وهذا رأيي الشخصي) أنها لا تصلح لبناء تطبيق من أ إلى ي لكنها تصلح لتطبق على أجزاء معينة فقط عندما تكون البرمجة الدالية أنسب لتلك الأجزاء. ولا ليس الموضوع أن ذلك لأني لا أفهم الرياضيات أو لا أفهم هاسكل ... بالأصل تخصصي رياضيات! سأكون ذلك الطفل الذي يقول الإمبراطور يسير عاريا.
إن قمت باستعمال البرمجة الدالية لتركيب دوال معقدة وكان جزء من تلك التراكيب التي عملتها له معنى ويمكن اختباره بشكل منفصل احفظها باسم يسهل فهمه كما عملنا ageMean و byGender بشكل منفصل
const ageMean=R.pipe( R.map(R.pipe( R.prop("dob"), R.invoker(2, 'substr')(0, 4), parseInt, R.subtract(year) )), R.mean ); const byGender=R.partition(R.propEq("gender", "female")); const ageMeanByGender=R.pipe( byGender, R.map(ageMean) );
عوضا عن تركيب ذلك كله دفعة واحدة. على الأقل يمكنك عمل اختبار وحدة للدالتين كل واحدة على حدى.
السلام عليكم ورحمة الله وبركاته
ردحذفأخي مؤيد جزاكم الله خير الجزاء على هذه المدونة
لقد بحثت كثيرا عن صفحة او موقع او ماشابه لاتواصل معكم ولكن بدون
توفيق ولحسن الحظ وجدت هذه المودونة و رائعة كالعادة انا دخلت عالم
اللينكس عن طريق كتابك و استخدمت توزيعة أعجوبة لينكس (لماذا توقفت؟ هل يمكنك اخباري؟)
وها أنا الان طالب دكتوراة في الرياضيات ومازلت استخدم لينكس. شعرت ببعض الحنين لأعجوبة
ولما تكتبه فأحببت ان اشكرك واسالك عما حل بها وجزاكم الله خير الجزاء