مرحبا يا هبر!
نواصل اليوم بحثنا حول البرمجة الوظيفية في سياق EcmaScript ، والتي تعتمد مواصفاتها على JavaScript. في المقال السابق ، درسنا المفاهيم الأساسية: وظائف نقية ، lambdas ، مفهوم الحصانة. اليوم سوف نتحدث عن تقنيات FP أكثر تعقيدًا: التركيب ، الكاري والوظائف النقية. تمت كتابة المقال بأسلوب "المعاينة الزائفة" ، أي سنقوم بحل مشكلة عملية ، أثناء دراسة مفاهيم انتقال المرحلة ورمز ريفاكتور لتقريب هذه الأخيرة إلى المثل العليا للتحولات المرحلة.
لذلك دعونا نبدأ!
لنفترض أن لدينا مهمة: لإنشاء مجموعة من الأدوات للعمل مع متقلبات.
سياق متناظر
ذكر الجنس
كلمة أو عبارة تتم قراءتها بنفس الطريقة من اليسار إلى اليمين ومن اليمين إلى اليسار.
"P. "أذهب بسيف القاضي"
قد يبدو أحد التطبيقات الممكنة لهذه المهمة كما يلي:
function getPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase().split('').reverse().join('');
بالطبع ، هذا التنفيذ يعمل. يمكننا أن نتوقع أن getPalindrom سيعمل بشكل صحيح إذا قام api بإرجاع البيانات الصحيحة. دعوة إلى isPalindrom ('أنا ذاهب مع قاضي السيف') ستعود صوابًا ، وستعود الدعوة إلى isPalindrom ('ليس palindrome') كاذبة. هل هذا التنفيذ جيد من حيث المثل العليا للبرمجة الوظيفية؟ بالتأكيد ليست جيدة!
وفقًا لتعريف الوظائف الصرفة من هذه
المقالة :
الوظائف الخالصة (PF) - تُرجع دائمًا نتيجة متوقعة.
خصائص PF:
تعتمد نتيجة تنفيذ PF فقط على الوسائط التي تم تمريرها والخوارزمية التي تنفذ PF
لا تستخدم القيم العالمية
لا تقم بتعديل القيم الخارجية أو الوسيطات التي تم تمريرها
لا تكتب البيانات إلى الملفات أو قواعد البيانات أو أي مكان آخر
وما الذي نراه في مثالنا مع palindromes؟
أولاً ، هناك ازدواجية في الشفرة ، أي
تم انتهاك مبدأ
DRY . ثانياً ، تصل وظيفة getPalindrom إلى قاعدة البيانات. الثالثة ، وظائف تعديل الحجج الخاصة بهم. المجموع ، وظائفنا ليست نظيفة.
تذكر التعريف: البرمجة الوظيفية هي طريقة لكتابة التعليمات البرمجية من خلال تجميع مجموعة من الوظائف.
نحن نؤلف مجموعة من الوظائف لهذه المهمة:
const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1) const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
في السطر 1 ، أعلنا ثابت التعبير الثابت في شكل وظيفي. غالبًا ما يتم استخدام هذه الطريقة لوصف الثوابت في FP. في السطر 2 ، قمنا بتغليف طريقة String.prototype.replace في تجريد استبدال وظيفي بحيث يتوافق (استدعاء الاستبدال) مع عقد البرمجة الوظيفية. على السطر 3 ، تم إنشاء تجريد لـ String.prototype.toLowerCase بنفس الطريقة. في الرابع ، قاموا بتنفيذ دالة تنشئ سلسلة موسعة جديدة من السلسلة التي تم تمريرها. الشيكات الخامسة عن سلسلة المساواة.
يرجى ملاحظة أن ميزاتنا نظيفة للغاية! تحدثنا عن فوائد وظائف نقية في
مقال سابق.
نحن الآن بحاجة إلى تنفيذ فحص لمعرفة ما إذا كانت السلسلة عبارة عن طبقة متناظرة. سوف تركيبة من الوظائف تأتي لمساعدتنا.
تكوين الوظائف هو توحيد دالتين أو أكثر في دالة ناتجة معينة تنفذ سلوك تلك المدمجة في التسلسل الحسابي المطلوب.
قد يبدو التعريف معقدًا ، لكن من الناحية العملية ، يكون عادلاً.
يمكننا القيام بذلك:
isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', ' ')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', ' '))));
أو مثل هذا:
const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', ' ')); const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', ' '))); console.log(isStringsEqual(strA, strB));
أو أدخل مجموعة أخرى من المتغيرات التوضيحية لكل خطوة من الخوارزمية المنفذة. غالبًا ما يمكن رؤية هذا الرمز في المشروعات ، وهذا مثال نموذجي للتكوين - تمرير مكالمة إلى دالة ما كوسيطة إلى أخرى. ومع ذلك ، كما نرى ، في حالة وجود العديد من الوظائف ، فإن هذا النهج سيء ، لأنه هذا الرمز غير قابل للقراءة! ماذا الان؟ حسنًا ، برمجة وظيفية ، هل نختلف؟
في الواقع ، كما هو الحال عادة في البرمجة الوظيفية ، نحتاج فقط لكتابة وظيفة أخرى.
const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
تأخذ دالة الإنشاء قائمة بالوظائف القابلة للتنفيذ كوسائط ، وتحولها إلى صفيف ، وتخزينها في إغلاق ، وتقوم بإرجاع دالة تتوقع قيمة أولية. بعد تمرير القيمة الأولية ، يبدأ التنفيذ المتسلسل لجميع الوظائف من مجموعة fns. ستكون وسيطة الدالة الأولى هي القيمة الأولية x التي تم تمريرها ، وستكون وسيطات جميع العناصر اللاحقة نتيجة للقيمة السابقة. حتى نتمكن من إنشاء تركيبة من أي عدد من الوظائف.
عند إنشاء تراكيب وظيفية ، من المهم جدًا مراقبة أنواع معلمات الإدخال وقيم الإرجاع لكل وظيفة بحيث لا توجد أخطاء غير متوقعة ، لأن نمرر نتيجة الوظيفة السابقة إلى التالي.
ومع ذلك ، الآن نرى بالفعل مشاكل في تطبيق تقنية التكوين على الكود لدينا ، لأن الوظيفة:
const replace = (regexp, replacement, str) => str.replace(regexp, replacement);
تتوقع قبول 3 معلمات إدخال ، ونحن نرسل واحدة فقط لإنشاء. تقنية FP أخرى ، وهي Currying ، ستساعدنا في حل هذه المشكلة.
Currying هو تحويل دالة من العديد من الوسائط إلى دالة من وسيطة واحدة.
تذكر وظيفة الإضافة لدينا من المادة الأولى؟
const add = (x,y) => x+y;
يمكن أن يكون الكاري مثل هذا:
const add = x => y => x+y;
تأخذ الدالة x وتُرجع lambda التي تتوقع y وتنفذ الإجراء.
فوائد الكاري:
- الرمز يبدو أفضل ؛
- وظائف الكاري دائما نظيفة.
الآن نقوم بتحويل وظيفة الاستبدال لدينا بحيث تتطلب حجة واحدة فقط. نظرًا لأننا نحتاج إلى الدالة لاستبدال الأحرف في السلسلة بتعبير منتظم معروف مسبقًا ، يمكننا إنشاء وظيفة مطبقة جزئيًا.
const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str);
كما ترون ، نصلح إحدى الحجج بثابت. هذا يرجع إلى حقيقة أن الكاري هو في الواقع حالة خاصة للاستخدام الجزئي.
يقوم التطبيق الجزئي بالالتفاف على وظيفة باستخدام برنامج تجميع يقبل وسيطات أقل من الوظيفة نفسها ؛ ويجب أن يقوم برنامج الترجيع بإرجاع دالة تأخذ باقي الوسائط.
في حالتنا ، أنشأنا وظيفة replaceAllNotWordSymbolsGlobal ، وهو خيار استبدال مطبق جزئيًا. يقبل الاستبدال ، ويخزنه في الإغلاق ، ويتوقع سطر الإدخال الذي سيطلق عليه الاستبدال ، ونعود إلى الحالة الثابتة.
العودة إلى palindromes. إنشاء مجموعة من الوظائف للتوقيت palindrome:
const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse );
وتكوين وظائف للخط الذي سنقارن به متلازمة العين المحتملة:
const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, );
الآن تذكر ما قلناه أعلاه:
مثال تكوين نموذجي هو تمرير مكالمة إلى دالة واحدة كوسيطة إلى أخرى
واكتب:
const testString = ' ';
هنا لدينا حل عملي وحسن المظهر:
const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); const testString = ' '; const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString));
ومع ذلك ، لا نريد القيام بالكاري في كل مرة أو لإنشاء وظائف مطبقة جزئيًا بأيدينا. بالطبع لا نريد ، المبرمجون هم أشخاص كسالى. لذلك ، كما يحدث عادةً في FP ، سنكتب بضع وظائف أخرى:
const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } }
تأخذ وظيفة الكاري وظيفة لتكون curry ، يخزنها في الإغلاق ، وإرجاع امدا. يتوقع لامدا بقية الوسائط إلى الدالة. في كل مرة يتم فيها استلام وسيطة ، يتم التحقق لمعرفة ما إذا كانت جميع الوسائط المعلنة مقبولة. إذا تم قبولها ، فسيتم استدعاء الوظيفة وإرجاع النتيجة. إذا لم يكن كذلك ، يتم كبح الوظيفة مرة أخرى.
يمكننا أيضًا إنشاء دالة مطبقة جزئيًا لاستبدال التعبير العادي الذي نحتاجه بسلسلة فارغة:
const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), '');
يبدو أن كل شيء على ما يرام ، لكننا نشعر بالكمال ولا نحب العديد من الأقواس ، نود أن يكون أفضل ، لذلك سنكتب وظيفة أخرى أو ربما اثنتين:
const party = (fn, x) => (...args) => fn(x, ...args);
هذا هو تطبيق التجريد لإنشاء وظائف تطبيقية جزئية. يستغرق وظيفة وسيطة الأولى ، وإرجاع امدا يتوقع الباقي وتنفيذ هذه الوظيفة.
الآن نعيد كتابة الحفلة حتى نتمكن من إنشاء وظيفة مطبَّقة جزئيًا للعديد من الوسائط:
const party = (fn, ...args) => (...rest) => fn(...args.concat(rest));
تجدر الإشارة بشكل منفصل إلى أنه يمكن استدعاء الوظائف التي يتم تسويتها بهذه الطريقة مع أي عدد من الوسيطات أقل من المسموح بها (fn.length).
const sum = (a,b,c,d) => a+b+c+d; const fn = curry(sum); const r1 = fn(1,2,3,4);
دعنا نعود إلى palindromes لدينا. يمكننا أن نعيد كتابة كلمة "إستبدالنا جميعنا" وورديموسيمولسإمبلتيجلوبال دون أقواس إضافية:
const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), '');
دعونا نلقي نظرة على الكود كله:
تبدو رائعة ، لكن ماذا لو لم تكن سلسلة بالنسبة لنا ، ولكن ستأتي مجموعة؟ لذلك ، نضيف وظيفة أخرى:
const map = fn => (...args) => args.map(fn);
الآن إذا كان لدينا مجموعة لاختبار palindromes ، ثم:
const palindroms = [' ',' ',' '. ' '] map(checkPalindrom )(...palindroms );
هذه هي الطريقة التي حللنا بها المهمة من خلال كتابة مجموعات الميزات. انتبه إلى الأسلوب غير المجدي لكود التعليمات البرمجية - هذا اختبار محسوس للنقاء الوظيفي.
الآن نظرية أكثر بقليل. لا يؤدي استخدام الكاري إلى نسيان أنه في كل مرة تقوم فيها بتجربة دالة تقوم بإنشاء وظيفة جديدة ، أي حدد خلية ذاكرة لذلك. من المهم مراقبة هذا لتجنب التسريبات.
تحتوي المكتبات الوظيفية مثل ramda.js على وظائف إنشاء وإخراج معلومات. يؤلف ينفذ خوارزمية التكوين من اليمين إلى اليسار ، وأنبوب من اليسار إلى اليمين. لدينا وظيفة يؤلف هو التناظرية من الأنابيب من ramda. هناك نوعان من وظائف التكوين المختلفة في المكتبة منذ ذلك الحين التكوين من اليمين إلى اليسار ومن اليسار إلى اليمين هما عقدين مختلفين من البرمجة الوظيفية. إذا وجد أحد القراء مقالة تصف جميع العقود الحالية لـ FP ، ثم شاركها في التعليقات ، فسأقرأها بكل سرور وأضع علامة على التعليق!
يسمى عدد المعلمات الشكلية للدالة
arity . هذا هو أيضا تعريف مهم من وجهة نظر نظرية التحولات المرحلة.
استنتاج
في إطار هذه المقالة ، درسنا تقنيات البرمجة الوظيفية مثل التكوين ، الكاري والتطبيق الجزئي. بالطبع ، في المشروعات الحقيقية ، ستستخدم مكتبات جاهزة بهذه الأدوات ، لكن كجزء من المقالة ، قمت بتنفيذ كل شيء على JS الأصلي بحيث يمكن للقراء الذين ليس لديهم خبرة كبيرة في FP فهم كيفية عمل هذه التقنيات تحت غطاء محرك السيارة.
لقد اخترت أيضًا طريقة السرد - codreview المزيفة ، من أجل توضيح منطق منطقتي لتحقيق النقاء الوظيفي في الكود.
بالمناسبة ، يمكنك الاستمرار في تطوير هذه الوحدة من العمل مع palindromes وتطوير أفكارها ، على سبيل المثال ، تحميل خطوط من api ، وتحويلها إلى مجموعات الحروف وإرسالها إلى الخادم حيث سيتم إنشاء سلسلة palindrome وأكثر من ذلك بكثير ... حسب تقديرك.
سيكون من الجيد أيضًا التخلص من الازدواجية في عمليات هذه الخطوط:
replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase,
بشكل عام ، من الممكن والضروري تحسين الكود باستمرار!
حتى المقالات في المستقبل.