خطأ جافا سكريبت أنيق في التعامل مع إما أحادي

دعنا نتحدث قليلا عن كيفية تعاملنا مع الأخطاء. في JavaScript ، لدينا وظيفة لغة مضمنة للعمل مع استثناءات. نرفق الكود الإشكالي في try...catch build. يتيح لك ذلك تحديد مسار تنفيذ عادي في قسم try ، ثم التعامل مع جميع الاستثناءات في قسم catch . ليس خيارا سيئا. يتيح لك ذلك التركيز على المهمة الحالية دون التفكير في كل خطأ محتمل. بالتأكيد أفضل من انسداد الكود الخاص بك مع ifs لا نهاية لها.

من دون try...catch يصعب التحقق من نتائج كل دالة استدعاء لقيم غير متوقعة. هذا هو تصميم مفيد. لكنها تعاني من بعض المشاكل. وهذه ليست الطريقة الوحيدة للتعامل مع الأخطاء. في هذه المقالة ، سننظر في استخدام إما إما monad كبديل try...catch .

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

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

مثال المشكلة


قبل مناقشة مشاكل الاستثناءات ، دعونا نتحدث عن سبب وجودها ولماذا try...catch ظهرت كتل try...catch . للقيام بذلك ، دعونا ننظر إلى مشكلة حاولت جعلها واقعية على الأقل جزئيًا. تخيل أننا نكتب وظيفة لعرض قائمة بالإشعارات. لقد نجحنا بالفعل (بطريقة ما) في إعادة البيانات من الخادم. لكن لسبب ما ، قرر مهندسو الواجهة الخلفية إرساله بتنسيق CSV ، وليس JSON. قد تبدو البيانات الأولية مثل هذا:

  الطابع الزمني ، والمحتوى ، وعرضها ، هريف
 2018-10-27T05: 33: 34 + 00: 00 ، دعاك madhatter إلى الشاي ، غير مقروء ، https: //example.com/invite/tea/3801
 2018-10-26T13: 47: 12 + 00: 00 ، ذكرت @ queenofhearts في مناقشة "بطولة الكروكيه" ، تمت مشاهدتها ، https: //example.com/discussions/croquet/1168
 2018-10-25T03: 50: 08 + 00: 00 ، أرسل إليك @ cheshirecat ابتسامة غير مقروءة ، https: //example.com/interactions/grin/88 

نريد عرضه في HTML. قد يبدو مثل هذا:

 <ul class="MessageList"> <li class="Message Message--viewed"> <a href="https://example.com/invite/tea/3801" class="Message-link">@madhatter invited you to tea</a> <time datetime="2018-10-27T05:33:34+00:00">27 October 2018</time> <li> <li class="Message Message--viewed"> <a href="https://example.com/discussions/croquet/1168" class="Message-link">@queenofhearts mentioned you in 'Croquet Tournament' discussion</a> <time datetime="2018-10-26T13:47:12+00:00">26 October 2018</time> </li> <li class="Message Message--viewed"> <a href="https://example.com/interactions/grin/88" class="Message-link">@cheshirecat sent you a grin</a> <time datetime="2018-10-25T03:50:08+00:00">25 October 2018</time> </li> </ul> 

لتبسيط المهمة ، ما عليك سوى التركيز على معالجة كل صف من بيانات CSV في الوقت الحالي. لنبدأ ببعض الوظائف البسيطة لمعالجة السلاسل. الأول يقسم السلسلة النصية إلى حقول:

 function splitFields(row) { return row.split('","'); } 

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

بعد تقسيم البيانات ، نريد إنشاء كائن. بحيث يطابق كل اسم خاصية رؤوس CSV. لنفترض أننا قمنا بالفعل بتحليل شريط العنوان بطريقة أو بأخرى (المزيد حول ذلك لاحقًا). لقد وصلنا إلى نقطة قد يحدث فيها خطأ ما. حصلنا على خطأ للمعالجة. نلقي خطأ إذا كان طول السلسلة لا يتطابق مع شريط العنوان. ( _.zipObject هي وظيفة lodash ).

 function zipRow(headerFields, fieldData) { if (headerFields.length !== fieldData.length) { throw new Error("Row has an unexpected number of fields"); } return _.zipObject(headerFields, fieldData); } 

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

 function addDateStr(messageObj) { const errMsg = 'Unable to parse date stamp in message object'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const d = new Date(messageObj.datestamp); if (isNaN(d)) { throw new Error(errMsg); } const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; return {datestr, ...messageObj}; } 

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

 const rowToMessage = _.template(`<li class="Message Message--<%= viewed %>"> <a href="<%= href %>" class="Message-link"><%= content %></a> <time datetime="<%= datestamp %>"><%= datestr %></time> <li>`); 

سيكون من الجميل أيضًا طباعة خطأ إذا ما التقى:

 const showError = _.template(`<li class="Error"><%= message %></li>`); 

عندما يكون كل شيء في مكانه ، يمكنك تجميع وظيفة لمعالجة كل سطر.

 function processRow(headerFieldNames, row) { try { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); rowObjWithDate = addDateStr(rowObj); return rowToMessage(rowObj); } catch(e) { return showError(e); } } 

وبالتالي فإن وظيفة جاهزة. دعونا نلقي نظرة فاحصة على كيفية التعامل مع الاستثناءات.

استثناءات: الجزء الجيد


إذن ما الجيد في try...catch ؟ تجدر الإشارة إلى أنه في المثال أعلاه ، قد تتسبب أي من الخطوات الموجودة في كتلة try حدوث خطأ. في zipRow() و addDateStr() نرمي الأخطاء عمدا. وإذا نشأت مشكلة ، ما عليك سوى التقاط الخطأ وعرض أي رسالة على الصفحة. بدون هذه الآلية ، يصبح الرمز قبيحًا حقًا. إليك كيف قد يبدو. افترض أن الدالات لا ترمي أخطاء ، لكنها ترجع null .

 function processRowWithoutExceptions(headerFieldNames, row) { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); if (rowObj === null) { return showError(new Error('Encountered a row with an unexpected number of items')); } rowObjWithDate = addDateStr(rowObj); if (rowObjWithDate === null) { return showError(new Error('Unable to parse date in row object')); } return rowToMessage(rowObj); } 

كما ترون ، هناك عدد كبير من القوالب if . الكود هو أكثر مطول. ومن الصعب اتباع المنطق الأساسي. بالإضافة إلى ذلك ، لا يخبرنا لا شيء. لا نعرف حقًا سبب فشل استدعاء الوظيفة السابقة. علينا أن نخمن. نقوم بإنشاء رسالة خطأ وندعو showError() . هذا الرمز هو أقذر وأكثر إرباكاً.

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

مشاكل معالجة الاستثناء حاول ... catch


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

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

لقد توصلنا إلى بديل


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

 function processRowReturningErrors(headerFieldNames, row) { fields = splitFields(row); rowObj = zipRow(headerFieldNames, fields); if (rowObj instanceof Error) { return showError(rowObj); } rowObjWithDate = addDateStr(rowObj); if (rowObjWithDate instanceof Error) { return showError(rowObjWithDate); } return rowToMessage(rowObj); } 

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

تعدد الأشكال


كيف نفعل ذلك؟ هذه مشكلة صعبة. ولكن يمكن حلها بمساعدة سحر تعدد الأشكال . إذا لم تصادف تعدد الأشكال من قبل ، فلا تقلق. في جوهره ، هو "توفير واجهة واحدة للكيانات من أنواع مختلفة" (Straustrup ، B. "C ++ Glossary of Björn Straustrup"). في JavaScript ، هذا يعني أننا ننشئ كائنات بنفس الأساليب والتواقيع المسماة. لكن سلوك مختلف. والمثال الكلاسيكي هو تسجيل التطبيق. يمكننا إرسال مجلاتنا إلى أماكن مختلفة حسب البيئة التي نحن فيها. ماذا لو أنشأنا كائنين مسجلين ، على سبيل المثال؟

 const consoleLogger = { log: function log(msg) { console.log('This is the console logger, logging:', msg); } }; const ajaxLogger = { log: function log(msg) { return fetch('https://example.com/logger', {method: 'POST', body: msg}); } }; 

يعرّف كل من الكائنات وظيفة سجل تتوقع معلمة سلسلة واحدة. لكنهم يتصرفون بشكل مختلف. الجمال هو أنه يمكننا كتابة التعليمات البرمجية التي تستدعي .log() ، بغض النظر عن الكائن الذي يستخدمه. يمكن أن يكون consoleLogger أو ajaxLogger . كل شيء يعمل على أي حال. على سبيل المثال ، سيعمل الرمز أدناه بشكل جيد مع أي كائن:

 function log(logger, message) { logger.log(message); } 

مثال آخر هو طريقة. .toString() لجميع كائنات JS. يمكننا كتابة طريقة. .toString() لأي فئة نقوم بإنشائها. بعد ذلك ، يمكنك إنشاء فئتين .toString() طريقة. .toString() مختلف. Left Right (بعد قليل سأشرح الأسماء).

 class Left { constructor(val) { this._val = val; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 class Right { constructor(val) { this._val = val; } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

الآن قم بإنشاء دالة تستدعي .toString() على هذين الكائنين:

 function trace(val) { console.log(val.toString()); return val; } trace(new Left('Hello world')); // ⦘ Left(Hello world) trace(new Right('Hello world')); // ⦘ Right(Hello world); 

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

اليسار واليمين


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

دعنا ننشئ طريقة تدير الوظيفة إذا كنا نسير على طريق جيد ، لكننا نتجاهلها بطريقة سيئة:

 /** * Left represents the sad path. */ class Left { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath() { // Left is the sad path. Do nothing } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path. */ class Right { constructor(val) { this._val = val; } runFunctionOnlyOnHappyPath(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

شيء مثل هذا:

 const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); leftHello.runFunctionOnlyOnHappyPath(trace); // does nothing rightHello.runFunctionOnlyOnHappyPath(trace); // ⦘ Hello world // ← "Hello world" 

ترجمة


نحن نقترب من شيء مفيد ، لكن ليس بعد. .runFunctionOnlyOnHappyPath() طريقة .runFunctionOnlyOnHappyPath() بإرجاع خاصية _val . كل شيء على ما يرام ، ولكن غير مريح للغاية إذا كنا نريد تشغيل أكثر من وظيفة. لماذا؟ لأننا لم نعد نعرف ما إذا كنا على الطريق الصحيح أو الخطأ. تختفي المعلومات بمجرد أخذ القيمة خارج اليسار واليمين. إذن ما يمكننا فعله هو إرجاع المسار الأيسر أو الأيمن مع وجود _val جديد في الداخل. ونحن سوف تقصير الاسم ، لأننا هنا. ما نقوم به هو ترجمة دالة من عالم القيم البسيطة إلى عالم اليسار واليمين. لذلك ، نحن نسمي طريقة map() :

 /** * Left represents the sad path. */ class Left { constructor(val) { this._val = val; } map() { // Left is the sad path // so we do nothing return this; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path */ class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

ندرج هذه الطريقة ونستخدم اليسار أو اليمين في بناء الجملة المجاني:

 const leftHello = new Left('Hello world'); const rightHello = new Right('Hello world'); const helloToGreetings = str => str.replace(/Hello/, 'Greetings,'); leftHello.map(helloToGreetings).map(trace); // Doesn't print any thing to the console // ← Left(Hello world) rightHello.map(helloToGreetings).map(trace); // ⦘ Greetings, world // ← Right(Greetings, world) 

لقد أنشأنا طريقين للتنفيذ. يمكننا وضع البيانات على مسار ناجح عن طريق استدعاء new Right() ، أو على المسار الفاشل عن طريق استدعاء new Left() .


يمثل كل فصل مسارًا: ناجحًا أو غير ناجح. لقد سرقت استعارة السكك الحديدية هذه من سكوت فلاشينا

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


استخدم .map() للتحرك على طول المسار

مع تقدمك ، يصبح من الصعب قليلاً في كل وقت كتابة اليسار أو اليمين ، لذلك دعونا نسمي هذه المجموعة ببساطة إما ("إما"). إما اليسار أو اليمين.

اختصارات لإنشاء أي من الكائنات


لذلك ، فإن الخطوة التالية هي إعادة كتابة دالات المثال الخاصة بنا بحيث تُرجع إما. اليسار للخطأ أو اليمين للقيمة. لكن قبل القيام بذلك ، استمتع ببعض المرح. دعنا نكتب بضع اختصارات. الأول هو طريقة ثابتة تسمى .of() . تقوم فقط بإرجاع يسار أو يمين جديد. قد يبدو الرمز كالتالي:

 Left.of = function of(x) { return new Left(x); }; Right.of = function of(x) { return new Right(x); }; 

بصراحة ، حتى Left.of() و Right.of() مملة للكتابة. لذلك أنا أميل نحو تسميات left() و right() أقصر:

 function left(x) { return Left.of(x); } function right(x) { return Right.of(x); } 

باستخدام هذه الاختصارات ، نبدأ في إعادة كتابة وظائف التطبيق:

 function zipRow(headerFields, fieldData) { const lengthMatch = (headerFields.length == fieldData.length); return (!lengthMatch) ? left(new Error("Row has an unexpected number of fields")) : right(_.zipObject(headerFields, fieldData)); } function addDateStr(messageObj) { const errMsg = 'Unable to parse date stamp in message object'; const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const d = new Date(messageObj.datestamp); if (isNaN(d)) { return left(new Error(errMsg)); } const datestr = `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`; return right({datestr, ...messageObj}); } 

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

بعد ذلك ، يمكننا البدء في معالجة الوظيفة الرئيسية التي تعالج سطر واحد. بادئ ذي بدء ، ضع السلسلة في إما مع right() ، ثم قم بترجمة splitFields :

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); // … } 

هذا يعمل بشكل جيد ، ولكن المشكلة تحدث إذا حاولت القيام بنفس الشيء مع zipRow() :

  function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow /* wait. this isn't right */); // ... } 

الحقيقة هي أن zipRow() تتوقع معلمتين. لكن الوظائف التي .map() إلى .map() تحصل على قيمة واحدة فقط من الخاصية ._val . يمكن تصحيح الموقف باستخدام إصدار zipRow() من zipRow() . قد يبدو مثل هذا:

 function zipRow(headerFields) { return function zipRowWithHeaderFields(fieldData) { const lengthMatch = (headerFields.length == fieldData.length); return (!lengthMatch) ? left(new Error("Row has an unexpected number of fields")) : right(_.zipObject(headerFields, fieldData)); }; } 

هذا التغيير zipRow يبسط تحويل zipRow ، لذلك سوف يعمل بشكل جيد مع .map() :

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)); // ... But now we have another problem ... } 

انضم


استخدام .map() لتشغيل splitFields() جيد ، حيث لا .splitFields() إما. ولكن عندما تضطر إلى تشغيل zipRow() ، تنشأ مشكلة لأنها تُرجع إما. لذلك عند استخدام .map() ينتهي بنا الأمر إلى الركض في داخل إما. إذا ذهبنا أبعد من ذلك ، ثم تعثر حتى يتم تشغيل .map() داخل .map() . هذا لن ينجح ايضا. نحن بحاجة إلى طريقة لدمج هذه المتداخلة إما. لذلك دعونا نكتب طريقة جديدة ، والتي سوف نسميها .join() :

 /** *Left represents the sad path. */ class Left { constructor(val) { this._val = val; } map() { // Left is the sad path // so we do nothing return this; } join() { // On the sad path, we don't // do anything with join return this; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path */ class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } join() { if ((this._val instanceof Left) || (this._val instanceof Right)) { return this._val; } return this; } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

الآن يمكننا "تفريغ" أصولنا:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.map(zipRow(headerFields)).join(); const rowObjWithDate = rowObj.map(addDateStr).join(); // Slowly getting better... but what do we return? } 

سلسلة


لقد قطعنا شوطا طويلا. ولكن عليك أن تتذكر .join() طوال الوقت ، وهو أمر مزعج. ومع ذلك ، لدينا نمط مكالمة متتالية شائعة .map() و. .join() ، لذلك دعونا ننشئ طريقة وصول سريع له. دعنا نسميها chain() ، لأنها تربط الوظائف التي تُرجع إلى اليسار أو اليمين.

 /** *Left represents the sad path. */ class Left { constructor(val) { this._val = val; } map() { // Left is the sad path // so we do nothing return this; } join() { // On the sad path, we don't // do anything with join return this; } chain() { // Boring sad path, // do nothing. return this; } toString() { const str = this._val.toString(); return `Left(${str})`; } } 

 /** * Right represents the happy path */ class Right { constructor(val) { this._val = val; } map(fn) { return new Right( fn(this._val) ); } join() { if ((this._val instanceof Left) || (this._val instanceof Right)) { return this._val; } return this; } chain(fn) { return fn(this._val); } toString() { const str = this._val.toString(); return `Right(${str})`; } } 

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


في حالة حدوث خطأ ، تتيح لك طريقة .chain () التبديل إلى المسار الأيسر. يرجى ملاحظة أن مفاتيح تعمل فقط طريقة واحدة.

حصلت على رمز نظافة قليلا:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr); // Slowly getting better... but what do we return? } 

تفعل شيئا مع القيم


اكتمال إعادة إنشاء الدالة processRow() تقريبًا. ولكن ماذا يحدث عندما نعيد القيمة؟ في النهاية ، نريد أن نتخذ إجراءات مختلفة حسب نوع الموقف الذي لدينا: يسار أو يمين. لذلك ، سوف نكتب وظيفة ستتخذ التدابير المناسبة:

 function either(leftFunc, rightFunc, e) { return (e instanceof Left) ? leftFunc(e._val) : rightFunc(e._val); } 

لقد خدعت واستخدمت القيم الداخلية للكائنات اليسرى أو اليمنى. لكن أدعي أنك لم تلاحظ هذا. الآن يمكننا إكمال مهمتنا:

 function processRow(headerFields, row) { const fieldsEither = right(row).map(splitFields); const rowObj = fieldsEither.chain(zipRow(headerFields)); const rowObjWithDate = rowObj.chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); } 

وإذا شعرنا بذكاء خاص ، فيمكننا مرة أخرى استخدام بناء الجملة المجاني:

 function processRow(headerFields, row) { const rowObjWithDate = right(row) .map(splitFields) .chain(zipRow(headerFields)) .chain(addDateStr); return either(showError, rowToMessage, rowObjWithDate); } 

كلا الإصدارين جميلة جدا. لا تصاميم try...catch. وليس إذا البيانات في وظيفة المستوى الأعلى. إذا كانت هناك مشكلة في سطر معين ، فنحن ببساطة نعرض رسالة خطأ في النهاية. ولاحظ أنه في processRow()ذكرنا لليسار أو اليمين المرة الوحيدة في البداية عندما نتصل right(). والباقي فقط الأساليب المستخدمة .map()و .chain()للاستخدام الدالة التالية.

ا ف ب ورفع


تبدو جيدة ، لكن يبقى النظر في سيناريو واحد أخير. باتباع مثالنا ، دعونا نرى كيف يمكنك معالجة جميع بيانات ملف CSV ، وليس فقط كل صف على حدة. سنحتاج إلى وظيفة مساعدة (المساعد) أو ثلاثة:

 function splitCSVToRows(csvData) { // There should always be a header row... so if there's no // newline character, something is wrong. return (csvData.indexOf('\n') < 0) ? left('No header row found in CSV data') : right(csvData.split('\n')); } function processRows(headerFields, dataRows) { // Note this is Array map, not Either map. return dataRows.map(row => processRow(headerFields, row)); } function showMessages(messages) { return `<ul class="Messages">${messages.join('\n')}</ul>`; } 

لذلك ، لدينا مساعد يقوم بتقسيم CSV إلى خطوط. ونعود إلى الخيار مع إما. الآن يمكنك استخدام .map()بعض وظائف lodash لاستخراج شريط العنوان من خطوط البيانات. لكننا نجد أنفسنا في موقف مثير للاهتمام ...

 function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); // What's next? } 

لدينا حقول رأس وصفوف بيانات جاهزة للعرض بها processRows(). ولكن headerFieldsأيضا dataRowsملفوفة في إما. نحتاج إلى طريقة لتحويل processRows()الدالة التي تعمل مع إما. للبدء ، نقوم بتنفيذ الكاري processRows.

 function processRows(headerFields) { return function processRowsWithHeaderFields(dataRows) { // Note this is Array map, not Either map. return dataRows.map(row => processRow(headerFields, row)); }; } 

الآن كل شيء جاهز للتجربة. لدينا headerFields، وهو إما ، ملفوفة حول مجموعة. ماذا سيحدث إذا ما أخذنا headerFieldsوالدعوة على ذلك .map()ل processRows()؟

 function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); // How will we pass headerFields and dataRows to // processRows() ? const funcInEither = headerFields.map(processRows); } 

باستخدام .map () ، يتم استدعاء دالة خارجية هنا processRows()، ولكن ليست دالة داخلية. بمعنى آخر ، تقوم processRows()بإرجاع دالة. ومنذ ذلك .map()، ما زلنا نعود. وبالتالي ، فإن النتيجة هي وظيفة داخل إما ، وهو ما يسمى funcInEither. يستغرق مجموعة من السلاسل ويعيد مجموعة من السلاسل الأخرى. نحن بحاجة إلى أن نأخذ هذه الوظيفة بطريقة ما ونطلق عليها قيمة في الداخل dataRows. للقيام بذلك ، أضف طريقة أخرى إلى فصولنا اليسار واليمين. سوف نسميها .ap()وفقا للمعايير .

كالعادة ، لا تفعل الطريقة شيئًا على المسار الأيسر:

  // In Left (the sad path) ap() { return this; } 

وبالنسبة للفئة المناسبة ، نتوقع إما أخرى مع وظيفة:

  // In Right (the happy path) ap(otherEither) { const functionToRun = otherEither._val; return this.map(functionToRun); } 

الآن يمكننا إكمال وظيفتنا الرئيسية:

  function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); const funcInEither = headerFields.map(processRows); const messagesArr = dataRows.ap(funcInEither); return either(showError, showMessages, messagesArr); } 

يتم .ap()فهم جوهر الطريقة على الفور قليلاً (تخلط مواصفات Fantasy Land بينها ، ولكن في معظم اللغات الأخرى تستخدم الطريقة بالعكس). إذا وصفت الأمر بطريقة أسهل ، فأنت تقول: "لدي وظيفة تستغرق عادةً قيمتين بسيطتين. أريد تحويلها إلى وظيفة تتطلب اثنين من ". إذا كان ذلك متاحًا ، .ap()يمكننا كتابة وظيفة ستفعل ذلك تمامًا. دعنا نسميها liftA2()، مرة أخرى وفقا للاسم القياسي. إنها تأخذ وظيفة بسيطة تتوقع حجة ، و "ترفع" للعمل مع "المتقدمين". (هذه هي الكائنات التي تحتوي على أسلوب .ap()وطريقة .of()). لذلك ، liftA2 اختصار لـ "الرفع التطبيقي ، معلمتان".

لذلك liftA2قد تبدو وظيفة مثل هذا:

 function liftA2(func) { return function runApplicativeFunc(a, b) { return b.ap(a.map(func)); }; } 

وظيفة المستوى الأعلى لدينا سوف تستخدمها على النحو التالي:

 function csvToMessages(csvData) { const csvRows = splitCSVToRows(csvData); const headerFields = csvRows.map(_.head).map(splitFields); const dataRows = csvRows.map(_.tail); const processRowsA = liftA2(processRows); const messagesArr = processRowsA(headerFields, dataRows); return either(showError, showMessages, messagesArr); } 

كود على CodePen .

حقا؟ هل هذا كل شيء؟


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

ولكن هناك عقبة واحدة. الاستثناءات تخفي الكثير عندما ترمي استثناءًا ، فإنك تنقل مشكلة معالجة الأخطاء إلى وظيفة أخرى. من السهل جدًا تجاهل استثناء سينبثق إلى أعلى مستوى. الجانب الجميل من إما هو أنه يتيح لك القفز من دفق البرنامج الرئيسي ، كما لو كان مع استثناء. ويعمل بصدق. يمكنك الحصول على اليمين أو اليسار. لا يمكنك التظاهر بأن خيار اليسار مستحيل. في النهاية ، يجب عليك سحب القيمة مع مكالمة مثل either().

أعلم أن هذا يبدو وكأنه نوع من التعقيد. لكن ألقِ نظرة على الكود الذي كتبناه (وليس الفصول ، ولكن الوظائف التي تستخدمها). لا يوجد الكثير من التعليمات البرمجية معالجة. إنه غائب تقريبًا ، باستثناء مكالمة either()في النهاية csvToMessages()وprocessRow(). هذا هو بيت القصيد. مع إما ، لديك معالجة خطأ نظيفة لا يمكن نسيانها عن طريق الخطأ. بدون إما ، ختم من خلال رمز وإضافة الحشو في كل مكان.

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

موارد إضافية


Source: https://habr.com/ru/post/ar457098/


All Articles