إذا جربت يدك في البرمجة الوظيفية ، فهذا يعني أنك ستصادف قريبًا مفهوم الوظائف البحتة. أثناء المتابعة ، ستجد أن المبرمجين الذين يفضلون النمط الوظيفي يبدو أنهم مهووسون بهذه الميزات. يقولون أن الوظائف النقية تسمح لك بالتحدث عن التعليمات البرمجية. يقولون أن الوظائف النقية هي كيانات من غير المرجح أن تعمل بشكل غير متوقع لدرجة أنها ستؤدي إلى حرب نووية حرارية. يمكنك أيضًا التعلم من هؤلاء المبرمجين أن الوظائف الخالصة توفر شفافية مرجعية. وهكذا - إلى ما لا نهاية.
بالمناسبة ، المبرمجين الوظيفيين على حق. الوظائف النقية جيدة. ولكن هناك مشكلة واحدة ...
يريد كاتب المادة ، التي نعرضها على انتباهك ، أن يتحدث عن كيفية التعامل مع الآثار الجانبية في الوظائف البحتة.
مشكلة الوظائف النقية
الوظيفة النقية هي وظيفة ليس لها آثار جانبية (في الواقع ، هذا ليس تعريفًا كاملاً لوظيفة نقية ، لكننا سنعود إلى هذا التعريف). ومع ذلك ، إذا كنت تفهم على الأقل شيئًا ما في البرمجة ، فأنت تعلم أن أهم شيء هنا هو بالضبط الآثار الجانبية. لماذا يحسب الرقم Pi حتى الرقم العشري المائة إذا لم يتمكن أحد من قراءة هذا الرقم؟ لعرض شيء ما على الشاشة أو الطباعة على طابعة ، أو تقديمه في شكل آخر ، يمكن الوصول إليه للإدراك ، نحتاج إلى استدعاء الأمر المناسب من البرنامج. وما الفائدة من استخدام قواعد البيانات إذا لم يكن بالإمكان كتابتها؟ لضمان تشغيل التطبيقات ، تحتاج إلى قراءة البيانات من أجهزة الإدخال وطلب المعلومات من موارد الشبكة. كل هذا لا يمكن أن يتم بدون آثار جانبية. ولكن على الرغم من هذه الحالة ، فإن البرمجة الوظيفية مبنية على وظائف نقية. فكيف يتمكن المبرمجون الذين يكتبون البرامج بأسلوب وظيفي من حل هذا التناقض؟
إذا أجبت على هذا السؤال باختصار ، فإن المبرمجين الوظيفيين يفعلون نفس الشيء مثل علماء الرياضيات: إنهم يغشون. على الرغم من هذا الاتهام ، يجب القول أنهم ، من وجهة نظر فنية ، ببساطة يتبعون قواعد معينة. لكنهم يجدون ثغرات في هذه القواعد ويوسعونها إلى أحجام لا تصدق. يفعلون ذلك بطريقتين رئيسيتين:
- يستفيدون من حقن التبعية. أسميها إلقاء مشكلة على السياج.
- إنهم يستخدمون المواشي ، والتي تبدو لي شكلاً متطرفًا من التسويف. وتجدر الإشارة هنا إلى أنه في Haskell يطلق عليه "IO functor" أو "IO monad " ، في PureScript يتم استخدام مصطلح "Effect" ، وهو ، في رأيي ، أفضل قليلاً لوصف جوهر Fun Funators.
حقن التبعية
حقن التبعية هي الطريقة الأولى للتعامل مع الآثار الجانبية. باستخدام هذا النهج ، نأخذ كل شيء يلوث الكود ونضعه في معلمات الوظيفة. ثم يمكننا اعتبار كل هذا جزءًا من مسؤولية بعض الوظائف الأخرى. سأشرح ذلك بالمثال التالي:
// logSomething :: String -> String function logSomething(something) { const dt = (new Date())toISOString(); console.log(`${dt}: ${something}`); return something; }
هنا أود أن أدون ملاحظة لأولئك الذين هم على دراية بتوقيعات الكتابة. إذا التزمنا بالقواعد بصرامة ، فسيتعين علينا مراعاة الآثار الجانبية هنا. لكننا سنتعامل مع هذا لاحقًا.
تحتوي
logSomething()
: فهي تقوم بإنشاء كائن
Date
وإخراج شيء ما إلى وحدة التحكم. أي أن وظيفتنا لا تؤدي فقط عمليات الإدخال والإخراج ، ولكنها تنتج أيضًا ، عندما يتم استدعاؤها في أوقات مختلفة ، نتائج مختلفة.
كيف تجعل هذه الوظيفة نظيفة؟ باستخدام تقنية حقن التبعية ، يمكننا أخذ كل ما يلوث الوظيفة ويجعلها معلمات وظيفية. ونتيجة لذلك ، بدلاً من قبول معلمة واحدة ، ستقبل وظيفتنا ثلاث معلمات:
// logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) { const dt = d.toIsoString(); return cnsl.log(`${dt}: ${something}`); }
الآن ، من أجل استدعاء الوظيفة ، نحتاج إلى نقل كل شيء إليها ملوثًا لها من قبل:
const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something);
هنا قد تعتقد أن كل هذا هراء ، وأننا نقلنا المشكلة بمستوى واحد فقط إلى الأعلى ، وهذا لم يضيف نقاءًا إلى التعليمات البرمجية الخاصة بنا. وأنت تعرف ، هذه هي الأفكار الصحيحة. هذه ثغرة في أنقى صورها.
يشبه هذا الجهل المزور: "لم أكن أعلم أن استدعاء طريقة
log
كائن
cnsl
سيؤدي إلى تنفيذ عبارة I / O. شخص ما سلمها لي ، لكنني لا أعرف من أين أتى كل ذلك. " هذا الموقف خاطئ.
وفي الواقع ، ما يحدث ليس غبيًا كما يبدو للوهلة الأولى.
logSomething()
نظرة على ميزات وظيفة
logSomething()
. إذا كنت تريد أن تفعل شيئًا غير نظيف ، فعليك أن تفعل ذلك بنفسك. لنفترض أنه يمكنك تمرير معلمات مختلفة لهذه الوظيفة:
const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = { log: () => { // }, }; logSomething(d, cnsl, "Off with their heads!"); // "Off with their heads!"
الآن لا تقوم وظيفتنا بشيء (فهي تُرجع فقط معلمة
something
). لكنها نقية تماما. إذا قمت بتسميته بنفس المعلمات عدة مرات ، فسوف يعيد نفس الشيء في كل مرة. وهذه هي النقطة الأساسية. من أجل جعل هذه الوظيفة غير نظيفة ، نحتاج إلى تنفيذ إجراءات معينة عمدًا. أو ، بعبارة أخرى ، كل شيء تعتمد عليه وظيفة ما في توقيعها. لا يصل إلى أي كائنات عمومية مثل
console
أو
Date
. هذا يضفي الطابع الرسمي على كل شيء.
بالإضافة إلى ذلك ، من المهم ملاحظة أنه يمكننا نقل وظائف أخرى إلى وظيفتنا ، والتي لم تكن نظيفة من قبل. نلقي نظرة على مثال آخر. تخيل أنه في شكل ما هناك اسم مستخدم ونحن بحاجة للحصول على قيمة الحقل المقابل لهذا النموذج:
// getUserNameFromDOM :: () -> String function getUserNameFromDOM() { return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; // "mhatter"
في هذه الحالة ، نحاول تحميل بعض المعلومات من DOM. لا تقوم الوظائف البحتة بذلك ، لأن
document
هو كائن عام يمكن أن يتغير في أي وقت. إحدى الطرق لتنظيف هذه الوظيفة هي تمريرها إلى كائن
document
العمومي كمعلمة. ومع ذلك ، لا يزال بإمكانك تمرير
querySelector()
. يبدو هذا:
// getUserNameFromDOM :: (String -> Element) -> String function getUserNameFromDOM($) { return $('#username').value; } // qs :: String -> Element const qs = document.querySelector.bind(document); const username = getUserNameFromDOM(qs); username; // "mhatter"
هنا ، مرة أخرى ، قد تتوصل إلى فكرة أن هذا أمر غبي. بعد كل شيء ، قمنا هنا ببساطة بإزالة من وظيفة
getUsernameFromDOM()
ما لا يسمح لنا
getUsernameFromDOM()
نظيفًا. ومع ذلك ، لم نتخلص من ذلك ، فقط نقل المكالمة إلى DOM إلى وظيفة أخرى ،
qs()
. قد يبدو أن النتيجة الوحيدة الملحوظة لهذه الخطوة هي أن الكود الجديد كان أطول من القديم. بدلاً من وظيفة نجسة واحدة ، لدينا الآن وظيفتان ، إحداهما لا تزال غير نظيفة.
انتظر قليلاً. تخيل أننا بحاجة إلى كتابة اختبار
getUserNameFromDOM()
. الآن ، بمقارنة الخيارين لهذه الوظيفة ، فكر في أيهما سيكون أسهل للعمل معه؟ لكي تعمل النسخة القذرة من الوظيفة على الإطلاق ، نحتاج إلى كائن مستند عمومي. علاوة على ذلك ، يجب أن يحتوي هذا المستند على عنصر بمعرف
username
. إذا كنت بحاجة إلى اختبار وظيفة مماثلة خارج المتصفح ، فستحتاج إلى استخدام شيء مثل JSDOM أو متصفح بدون واجهة مستخدم. يرجى ملاحظة أن كل هذا مطلوب فقط لاختبار وظيفة صغيرة بطول عدة أسطر. ومن أجل اختبار النسخة الثانية النظيفة من هذه الوظيفة ، يكفي القيام بما يلي:
const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`);
هذا لا يعني بالطبع أنه لاختبار هذه الوظائف ، ليست هناك حاجة إلى اختبارات التكامل التي يتم إجراؤها في متصفح حقيقي (أو ، على الأقل ، استخدام شيء مثل JSDOM). لكن هذا المثال يوضح شيئًا مهمًا للغاية ، وهو أن وظيفة
getUserNameFromDOM()
أصبحت
getUserNameFromDOM()
قابلة للتنبؤ بها تمامًا. إذا قمنا بتمرير
qsStub()
إليه ، فسوف يعيد دائمًا
mhatter
. "عدم القدرة على التنبؤ" انتقلنا إلى الوظيفة الصغيرة
qs()
.
إذا لزم الأمر ، يمكننا أن نأخذ آليات غير متوقعة إلى مستويات أبعد من الوظيفة الرئيسية. ونتيجة لذلك ، يمكننا نقلهم ، بشكل نسبي ، إلى "المناطق الحدودية" من المدونة. سيؤدي هذا إلى امتلاكنا لصدفة رقيقة من الكود النجس الذي يحيط بنواة تم اختبارها جيدًا ويمكن التنبؤ بها. وتبين أن التنبؤ بالشفرة قيم للغاية عندما ينمو حجم المشاريع التي أنشأها المبرمجون.
▍ مساوئ آلية حقن التبعية
باستخدام حقن التبعية ، يمكنك كتابة تطبيق كبير ومعقد. أعرف هذا ، بما أنني كتبت
هذا الطلب بنفسي. مع هذا النهج ، يتم تبسيط الاختبار ، وتصبح تبعيات الوظائف مرئية بوضوح. لكن حقن التبعية لا تخلو من العيوب. الشيء الرئيسي هو أنه عند استخدامه ، يمكن الحصول على تواقيع الوظائف الطويلة جدًا:
function app(doc, con, ftch, store, config, ga, d, random) { // } app(document, console, fetch, store, config, ga, (new Date()), Math.random);
في الواقع ، هذا ليس سيئا للغاية. تتجلى عيوب هذه الإنشاءات إذا كانت بعض المعلمات بحاجة إلى تمريرها إلى وظائف معينة مدمجة بعمق في وظائف أخرى. يبدو أن هناك حاجة لتمرير المعلمات من خلال العديد من مستويات مكالمات الوظائف. عندما يزيد عدد هذه المستويات ، يبدأ في الإزعاج. على سبيل المثال ، قد يكون من الضروري نقل الكائن الذي يمثل التاريخ من خلال 5 وظائف وسيطة ، بينما لا تستخدم أي من الوظائف المتوسطة هذا الكائن. على الرغم ، بالطبع ، لا يمكن القول أن مثل هذا الوضع هو شيء مثل كارثة عالمية. بالإضافة إلى ذلك ، هذا يجعل من الممكن رؤية تبعيات الوظائف بوضوح. ومع ذلك ، سواء كان الأمر كذلك ، فهذا لا يزال غير لطيف. لذلك ، نعتبر الآلية التالية.
functions وظائف كسول
دعونا نلقي نظرة على الثغرة الثانية التي يستخدمها أتباع البرمجة الوظيفية. وهو يتألف من الفكرة التالية: التأثير الجانبي ليس عرضًا جانبيًا حتى يحدث بالفعل. أعلم أن هذا يبدو غامضًا. لمعرفة ذلك ، خذ بعين الاعتبار المثال التالي:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
ربما يكون أحد الأمثلة غبيًا ، وأنا أعلم ذلك. إذا كنا بحاجة إلى الرقم 0 ، فلكي يظهر ، أدخله في المكان الصحيح في الرمز. وأعلم أيضًا أنك لن تكتب كود جافا سكريبت للتحكم في الأسلحة النووية. لكننا نحتاج إلى هذا الرمز لتوضيح التكنولوجيا المعنية.
إذن هنا مثال على دالة نجسة. تقوم بإخراج البيانات إلى وحدة التحكم وهي أيضًا سبب الحرب النووية. ومع ذلك ، تخيل أننا بحاجة إلى الصفر الذي ترجعه هذه الدالة. تخيل سيناريو نحتاج فيه إلى حساب شيء ما بعد إطلاق صاروخ. لنفترض أننا قد نحتاج إلى بدء مؤقت للعد التنازلي أو شيء من هذا القبيل. في هذه الحالة ، سيكون من الطبيعي تمامًا التفكير مسبقًا في إجراء الحسابات. ويجب أن نتأكد من أن الصاروخ يُطلق بالضبط عند الحاجة. لسنا بحاجة إلى إجراء حسابات بطريقة يمكن أن تؤدي بطريق الخطأ إلى إطلاق هذا الصاروخ. لذلك دعونا نفكر في ما يحدث إذا قمنا
fZero()
دالة
fZero()
في وظيفة أخرى
fZero()
ببساطة. لنفترض أنه سيكون شيئًا مثل المجمع الأمني:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() { return fZero; }
يمكنك استدعاء
returnZeroFunc()
عدة مرات كما تشاء. في هذه الحالة ، حتى يتم تنفيذ ما يتم إرجاعه ، نحن (نظريًا) في أمان. في حالتنا ، هذا يعني أن تنفيذ الكود التالي لن يؤدي إلى حرب نووية:
const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc();
الآن أكثر صرامة من ذي قبل ، دعونا نقترب من تعريف مصطلح "الوظيفة البحتة". سيتيح لنا هذا فحص وظيفة
returnZeroFunc()
بمزيد من التفصيل. لذلك ، تكون الوظيفة نظيفة في الظروف التالية:
- لا آثار جانبية ملحوظة.
- ربط الشفافية. وهذا يعني أن استدعاء مثل هذه الوظيفة بنفس قيم الإدخال يؤدي دائمًا إلى نفس النتائج.
returnZeroFunc()
نحلل الدالة
returnZeroFunc()
.
هل لديها أي آثار جانبية؟ اكتشفنا للتو أن استدعاء
returnZeroFunc()
لا يطلق صواريخ. إذا لم تقم باستدعاء ما ترجعه هذه الوظيفة ، فلن يحدث شيء. لذلك ، يمكننا أن نستنتج أن هذه الوظيفة ليس لها آثار جانبية.
هل هذه الميزة شفافة بشكل مرجعي؟ أي ، هل تعيد دائمًا نفس البيانات عند تمرير نفس بيانات الإدخال إليها؟ سوف نتحقق من ذلك ، مستفيدين من حقيقة أننا في جزء الكود أعلاه سمينا هذه الوظيفة عدة مرات:
zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true
يبدو كل شيء جيدًا ، ولكن وظيفة
returnZeroFunc()
ليست نظيفة تمامًا بعد. تشير إلى متغير خارج نطاقها الخاص. لحل هذه المشكلة ، نعيد كتابة الوظيفة:
// returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() { function fZero() { console.log('Launching nuclear missiles'); // return 0; } return fZero; }
الآن يمكن اعتبار الوظيفة نظيفة. ومع ذلك ، في هذه الحالة ، تلعب قواعد JavaScript ضدنا. وبالتحديد ، لم يعد بإمكاننا استخدام عامل التشغيل
===
للتحقق من الشفافية المرجعية للدالة. ويرجع ذلك إلى حقيقة أن
returnZeroFunc()
سيعيد دائمًا مرجعًا جديدًا إلى الوظيفة. صحيح ، يمكن التحقق من شفافية الرابط من خلال فحص الرمز بنفسك. سيظهر هذا التحليل أنه مع كل استدعاء دالة ، فإنه يعيد رابطًا إلى نفس الوظيفة.
أمامنا ثغرة صغيرة وأنيقة. ولكن هل يمكن استخدامه في المشاريع الحقيقية؟ الجواب على هذا السؤال إيجابي. ومع ذلك ، قبل التحدث عن كيفية استخدام هذا عمليًا ، سنطور فكرتنا قليلاً. وبالتحديد ،
fZero()
الوظيفة الخطرة
fZero()
:
// fZero :: () -> Number function fZero() { console.log('Launching nuclear missiles'); // return 0; }
سنحاول استخدام الصفر الذي تم إرجاعه بواسطة هذه الوظيفة ، لكننا سنفعل ذلك حتى لا تبدأ (حتى الآن) حرب نووية. للقيام بذلك ، قم بإنشاء دالة تأخذ الصفر الذي تم إرجاعه بواسطة الدالة
fZero()
وإضافة دالة إليه:
// fIncrement :: (() -> Number) -> Number function fIncrement(f) { return f() + 1; } fIncrement(fZero); // // 1
هذا حظ سيئ ... بدأنا بطريق الخطأ حربًا نووية. دعنا نحاول مرة أخرى ، ولكن هذه المرة لن نعيد أي رقم. بدلاً من ذلك ، نرجع دالة ترجع يومًا ما رقمًا:
// fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) { return () => f() + 1; } fIncrement(zero); // [Function]
الآن يمكنك التنفس بسهولة. تم تجنب الكارثة. نواصل الدراسة. بفضل هاتين الوظيفتين ، يمكننا إنشاء مجموعة كاملة من "الأرقام المحتملة":
const fOne = fIncrement(zero); const fTwo = fIncrement(one); const fThree = fIncrement(two);
بالإضافة إلى ذلك ، يمكننا إنشاء العديد من الوظائف التي تبدأ أسماؤها بوظائف
f
(دعنا نطلق عليها وظائف
f*()
) ، المصممة للعمل مع "الأرقام المحتملة":
// fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) { return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) { return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) { return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); // , . !
انظر ماذا فعلنا هنا؟ مع "الأرقام المحتملة" يمكنك أن تفعل نفس الشيء مع الأرقام العادية. يطلق علماء الرياضيات على هذا
التشكل . يمكن دائمًا تحويل الرقم العادي إلى "رقم ممكن" عن طريق وضعه في دالة. يمكنك الحصول على "الرقم الممكن" عن طريق استدعاء الوظيفة. وبعبارة أخرى ، لدينا تعيين بين الأرقام العادية و "الأرقام المحتملة". هذا ، في الواقع ، أكثر إثارة للاهتمام مما قد يبدو. قريبا سنعود إلى هذه الفكرة.
التقنية المذكورة أعلاه باستخدام دالة المجمع هي استراتيجية صالحة. يمكننا الاختباء وراء الوظائف بقدر الضرورة. وبما أننا لم نقم حتى الآن باستدعاء أي من هذه الوظائف ، فإن جميعها ، من الناحية النظرية ، نقية. ولا أحد يبدأ الحرب. في الكود العادي (ليس مرتبطًا بالصواريخ) ، نحتاج بالفعل إلى آثار جانبية في النهاية. يسمح لنا التفاف كل ما نحتاجه في دالة بالتحكم الدقيق في هذه التأثيرات. نختار الوقت الذي تظهر فيه هذه التأثيرات.
وتجدر الإشارة إلى أنه ليس من المناسب جدًا استخدام الإنشاءات الموحدة مع أكوام من الأقواس في كل مكان لإعلان الوظائف. كما أن إنشاء إصدارات جديدة لكل وظيفة ليس نشاطًا ممتعًا. تحتوي JavaScript على بعض الوظائف المضمنة الرائعة مثل
Math.sqrt()
. سيكون من الرائع إذا كانت هناك طريقة لاستخدام هذه الوظائف العادية مع "قيمنا المعلقة". في الواقع ، سنتحدث عن هذا الآن.
تأثير Functor
هنا سوف نتحدث عن المرافقات ممثلة بأشياء تحتوي على "وظائفنا المؤجلة". لتمثيل الجنازة ، سنستخدم كائن
Effect
. سنضع
fZero()
في مثل هذا الكائن. ولكن قبل القيام بذلك ، سنجعل هذه الوظيفة أكثر أمانًا:
// zero :: () -> Number function fZero() { console.log('Starting with nothing'); // , , . // . return 0; }
الآن نحن نصف وظيفة المُنشئ لإنشاء كائنات من نوع
Effect
:
// Effect :: Function -> Effect function Effect(f) { return {}; }
لا يوجد شيء مثير للاهتمام هنا بشكل خاص لذلك سنعمل على هذه الميزة. لذا ، نريد استخدام الدالة
fZero()
المعتادة مع الكائن
Effect
. لتقديم مثل هذا السيناريو ، سنكتب طريقة تقبل وظيفة عادية وتطبقها يومًا ما على "القيمة المعلقة" الخاصة بنا. وسنفعل ذلك بدون استدعاء دالة
Effect
. نسمي
map()
الوظائف هذه
map()
. لها مثل هذا الاسم نظرًا لأنه يخلق تعيينًا بين الوظيفة المعتادة ووظيفة
Effect
. قد يبدو مثل هذا:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); } } }
الآن ، إذا كنت تراقب عن كثب ما يحدث ، فقد يكون لديك أسئلة حول وظيفة
map()
. تبدو مشابهة للأغنية بشكل مريب. سنعود إلى هذه المشكلة لاحقًا ، ولكننا الآن سنختبر ما لدينا الآن في العمل:
const zero = Effect(fZero); const increment = x => x + 1;
لذا ... الآن ليس لدينا الفرصة لمراقبة ما حدث هنا. لذلك ، دعنا نعدل
Effect
أجل ، إذا جاز التعبير ، الحصول على فرصة "جذب الزناد":
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } } } const zero = Effect(fZero); const increment = x => x + 1; // . const one = zero.map(increment); one.runEffects(); // // 1
إذا لزم الأمر ، يمكننا الاستمرار في استدعاء وظيفة
map()
:
const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero) .map(increment) .map(double) .map(cube); eight.runEffects();
هنا ، ما يحدث بدأ بالفعل يصبح أكثر إثارة للاهتمام. نسميها "جنازة". كل هذا يعني أن الكائن
Effect
له وظيفة
map()
ويطيع بعض
القواعد . ومع ذلك ، هذه ليست قواعد تمنع أي شيء. هذه القواعد حول ما يمكنك القيام به. هم أشبه بامتيازات. نظرًا لأن كائن
Effect
هو ممر ، فإنه يطيع هذه القواعد. على وجه الخصوص ، هذا هو ما يسمى "قاعدة التكوين".
يبدو هذا:
إذا كان هناك كائن
Effect
يسمى
e
ووظيفتين ،
f
و
g
، فإن
e.map(g).map(f)
يعادل
e.map(x => f(g(x)))
.
وبعبارة أخرى ، فإن طريقتين
map()
متتاليتين تعادل تكوين وظيفتين. هذا يعني أن كائنًا من نوع
Effect
يمكنه تنفيذ إجراءات مشابهة لما يلي (تذكر أحد الأمثلة أعلاه):
const incDoubleCube = x => cube(double(increment(x)))
عندما نفعل ما هو موضح هنا ، نضمن لك الحصول على نفس النتيجة التي نحصل عليها باستخدام نسخة من هذا الرمز مع استدعاء ثلاثي
map()
. يمكننا استخدام هذا عند إعادة هيكلة الرمز ، ويمكننا التأكد من أن الرمز سيعمل بشكل صحيح. في بعض الحالات ، يمكن أن يؤدي تغيير نهج إلى آخر إلى تحسين الأداء.
أقترح الآن التوقف عن تجربة الأرقام والحديث عما يبدو أكثر مثل الرمز المستخدم في المشاريع الحقيقية.
etطريقة ()
يقبل مُنشئ الكائن
Effect
، كوسيطة ، دالة. هذا مريح ، لأن معظم الآثار الجانبية التي نريد تأجيلها هي وظائف. على سبيل المثال ، هما
Math.random()
و
console.log()
. ومع ذلك ، في بعض الأحيان تحتاج إلى وضع قيمة في كائن
Effect
ليست وظيفة. , ,
window
. , . , ( -, , , Haskell
pure
):
// of :: a -> Effect a Effect.of = function of(val) { return Effect(() => val); }
, , , -. , , . HTML- . , . . على سبيل المثال:
window.myAppConf = { selectors: { 'user-bio': '.userbio', 'article-list': '#articles', 'user-name': '.userfullname', }, templates: { 'greet': 'Pleased to meet you, {name}', 'notify': 'You have {n} alerts', } };
,
Effect.of()
,
Effect
:
const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']);
▍ Effect
. ,
Effect
. ,
getElementLocator()
,
Effect
, . DOM,
document.querySelector()
— , . :
// $ :: String -> Effect DOMElement function $(selector) { return Effect.of(document.querySelector(s)); }
, ,
map()
:
const userBio = userBioLocator.map($);
, , .
div
,
map()
, , . ,
innerHTML
, :
const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML));
, .
userBio
, . , , , . , ,
Effect('user-bio')
. , , , :
Effect(() => '.userbio');
— . :
Effect(() => window.myAppConf.selectors['user-bio']);
,
map()
, ( ). , ,
$
, :
Effect(() => $(window.myAppConf.selectors['user-bio']))
, :
Effect( () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) );
Effect.of
, :
Effect( () => Effect( () => document.querySelector(window.myAppConf.selectors['user-bio']) ) );
, , , .
Effect
.
▍ join()
? ,
Effect
. , , .
Effect
.runEffect()
. . , - , , , , . , .
join()
.
Effect
,
runEffect()
, . , .
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } } }
, :
const userBioHTML = Effect.of(window) .map(x => x.myAppConf.selectors['user-bio']) .map($) .join() .map(x => x.innerHTML);
▍ chain()
,
.map()
,
.join()
, . , , . , ,
Effect
. ,
.map()
.join()
. , ,
Effect
:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } chain(g) { return Effect(f).map(g).join(); } } }
chain()
- , ,
Effect
( ,
). HTML- :
const userBioHTML = Effect.of(window) .map(x => x.myAppConf.selectors['user-bio']) .chain($) .map(x => x.innerHTML);
-. . ,
flatMap
. , , — , ,
join()
. Haskell, ,
bind
. , - , ,
chain
,
flatMap
bind
— .
▍ Effect
Effect
, . . , DOM, , ? , , , . , . —
.
// tpl :: String -> Object -> String const tpl = curry(function tpl(pattern, data) { return Object.keys(data).reduce( (str, key) => str.replace(new RegExp(`{${key}}`, data[key]), pattern ); });
. :
const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str});
, . . (
name
pattern
)
Effect
.
tpl()
, ,
Effect
.
,
map()
Effect
tpl()
:
pattern.map(tpl);
, .
map()
:
map :: Effect a ~> (a -> b) -> Effect b
:
tpl :: String -> Object -> String
,
map()
pattern
, ( ,
tpl()
)
Effect
.
Effect (Object -> String)
pattern
Effect
. .
Effect
, .
ap()
:
// Effect :: Function -> Effect function Effect(f) { return { map(g) { return Effect(x => g(f(x))); }, runEffects(x) { return f(x); } join(x) { return f(x); } chain(g) { return Effect(f).map(g).join(); } ap(eff) { // - ap, , eff ( ). // map , eff ( 'g') // g, f() return eff.map(g => g(f())); } } }
.ap()
:
const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str})); const pattern = win.map(w => w.myAppConfig.templates('greeting')); const greeting = name.ap(pattern.map(tpl));
, … , ,
.ap()
. , ,
map()
,
ap()
. , , .
. , . , , ,
Effect
,
ap()
. , :
// liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) { return y.ap(x.map(f)); // : // return x.map(f).chain(g => y.map(g)); });
liftA2()
, , .
liftA3()
:
// liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d) const liftA3 = curry(function liftA3(f, a, b, c) { return c.ap(b.ap(a.map(f))); });
,
liftA2()
liftA3()
Effect
. , ,
ap()
.
liftA2()
:
const win = Effect.of(window); const user = win.map(w => w.myAppConfig.selectors['user-name']) .chain($) .map(el => el.innerHTML) .map(str => ({name: str}); const pattern = win.map(w => w.myAppConfig.templates['greeting']); const greeting = liftA2(tpl)(pattern, user);
?
, , , . ? ,
Effect
ap()
. , ? ?
: « , , ».
:
▍
— . , , , .
const pattern = window.myAppConfig.templates['greeting'];
, , , :
const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting'));
— , , , , . . — , , . , , , , , , . , . — . , , , . , .
. .
▍ Effect
, , . -
Facebook
Gmail
. ? .
, . . CSV- . . , , , . , . , . , , , .
, . ,
map()
reduce()
, . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .
, , . , . لا يشبه أي شيء؟ , , , . . , .
TensorFlow , .
TensorFlow, , . «». , , :
node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2)
Python, JavaScript. ,
Effect
,
add()
, (
sess.run()
).
print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) # node3: Tensor("Add_2:0", shape=(), dtype=float32) # sess.run(node3): 7.0
, (7.0) ,
sess.run()
. , . , , , .
الملخص
, . , .
Effect
.
, , , , , . , , .
Effect
, , , . , .
— . , . , , . . . , .
أعزائي القراء! ?