تعليق المترجم: هذه ترجمة لمقال عظيم كتبه دان أبراموف ، أحد مساهمي React. أمثلةه مكتوبة لـ JS ، لكنها ستكون واضحة للمطورين بأي لغة. الفكرة شائعة للجميع.
هل سمعت عن الآثار الجبرية؟
كانت محاولاتي الأولى لمعرفة من هم ولماذا يجب أن تثيرني غير ناجحة. لقد وجدت العديد من ملفات PDF ، لكنها أربكتني أكثر. (لسبب ما ، أغفو أثناء قراءة المقالات الأكاديمية.)
لكن زميلي سيباستيان استمر في وصفهم بالنموذج العقلي لبعض الأشياء التي نقوم بها في React. (يعمل سيباستيان في فريق React وطرح الكثير من الأفكار ، بما في ذلك الخطافات والتشويق.) في مرحلة ما ، أصبح ميم محليًا في فريق React ، وانتهت العديد من محادثاتنا بما يلي:
اتضح أن التأثيرات الجبرية مفهوم رائع ، وأنها ليست مخيفة كما بدا لي في البداية بعد قراءة ملفات PDF هذه. إذا كنت تستخدم React ، فلست بحاجة إلى معرفة أي شيء عنها ، ولكن إذا كنت مثلي مهتمًا ، فاقرأ.
(إخلاء المسئولية: أنا لست باحثًا في مجال لغات البرمجة وقد أفسد شيئًا ما في شرحي. لذا ، اسمحوا لي أن أعرف إذا كنت مخطئًا!)
ما زال في وقت مبكر من الإنتاج
تعد الآثار الجبرية حاليًا مفهومًا تجريبيًا من مجال دراسة لغات البرمجة. وهذا يعني أنه على عكس if
التعبيرات لـ async/await
أو حتى أو غير async/await
، على الأرجح لن تتمكن من استخدامها الآن في الإنتاج. يتم دعمها من قبل عدد قليل من اللغات التي تم إنشاؤها خصيصا لدراسة هذه الفكرة. هناك تقدم في تنفيذها في OCaml ، والتي ... لا تزال مستمرة . بمعنى آخر ، شاهد ، لكن لا تلمس يديك.
لماذا يجب أن يزعجني؟
تخيل أنك تكتب الشفرة باستخدام goto
، وأن شخصًا ما يخبرك عن وجود البنيات if
. أو ربما كنت غارقة في الجحيم رد الاتصال وشخص يظهر لك async/await
. رائع جدا ، أليس كذلك؟
إذا كنت من الأشخاص الذين يحبون تعلم ابتكارات البرمجة قبل سنوات قليلة من أن تصبح عصرية ، فقد حان الوقت للتأثير على الآثار الجبرية. وإن لم يكن ذلك ضروريا. هذه هي الطريقة للتحدث عن async/await
في عام 1999.
حسنًا ، ما نوع هذه الآثار؟
قد يكون الاسم مربكًا بعض الشيء ، لكن الفكرة بسيطة. إذا كنت معتادًا على كتل try/catch
، فستفهم بسرعة تأثيرات الجبر.
دعونا نتذكر try/catch
. لنفترض أن لديك وظيفة تلقي استثناءات. ربما توجد عدة مكالمات متداخلة بينها وبين catch
:
function getName(user) { let name = user.name; if (name === null) { throw new Error(' '); } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } catch (err) { console.log(", : ", err); }
نلقي استثناءًا داخل getName
، لكنه "ينبثق" من خلال makeFriends
إلى أقرب catch
. هذه هي الخاصية الرئيسية try/catch
. الكود الوسيط غير مطلوب للإهتمام بمعالجة الأخطاء.
على عكس رموز الخطأ في لغات مثل C ، عند استخدام try/catch
لن تضطر إلى تمرير الأخطاء يدويًا من خلال كل مستوى متوسط لمعالجة الخطأ في المستوى العلوي. استثناءات يطفو على السطح تلقائيا.
ما علاقة هذا بالتأثيرات الجبرية؟
في المثال أعلاه ، بمجرد أن نرى خطأً ، لن نتمكن من متابعة تنفيذ البرنامج. عندما نجد أنفسنا في catch
، سيتوقف تنفيذ البرنامج العادي.
انتهى كل شيء. فوات الاوان. أفضل ما يمكننا فعله هو التعافي من الفشل وربما تكرار ما كنا نفعله بطريقة ما ، لكن لا يمكننا "العودة" بطريقة سحرية إلى ما كنا فيه ونفعل شيئًا آخر. ومع التأثيرات الجبرية ، يمكننا ذلك.
هذا مثال مكتوب بلهجة JavaScript الافتراضية (دعنا نسميها ES2025 للمتعة) ، والذي يسمح لنا بمواصلة العمل بعد اسم المستخدم المفقود:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
(أعتذر لجميع القراء من عام 2025 الذين يبحثون على الإنترنت عن "ES2025" وسقط في هذه المقالة. إذا بحلول ذلك الوقت سوف تصبح التأثيرات الجبرية جزءًا من JavaScript ، سأكون سعيدًا بتحديث المقال!)
بدلا من throw
ونحن نستخدم perform
الكلمة الافتراضية. وبالمثل ، فبدلاً من try/catch
نستخدم try/handle
الافتراضي. بناء الجملة الدقيق لا يهم هنا - لقد توصلت إلى شيء لتوضيح الفكرة.
إذن ما الذي يحدث هنا؟ دعنا نلقي نظرة فاحصة.
بدلاً من إلقاء خطأ ، نقوم بتنفيذ التأثير . تمامًا كما يمكننا رمي أي كائن ، هنا يمكننا تمرير بعض القيمة للمعالجة . في هذا المثال ، أقوم بتمرير سلسلة ، لكن يمكن أن يكون كائنًا أو أي نوع بيانات آخر:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; }
عندما نلقي استثناءًا ، يبحث المحرك عن أقرب معالج try/catch
في مكدس الاستدعاءات. وبالمثل ، عندما نقوم بتنفيذ تأثير ما ، سيبحث المحرك عن أقرب معالج تأثير try/handle
في أعلى الحزمة.
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
يسمح لنا هذا التأثير بتحديد كيفية التعامل مع الموقف عندما لا يتم تحديد الاسم. الجديد هنا (مقارنة بالاستثناءات) هو resume with
الافتراضية resume with
:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
هذا شيء لا يمكنك فعله مع try/catch
. يسمح لنا بالعودة إلى حيث قمنا بإجراء التأثير وتمرير شيء من المعالج . :
function getName(user) { let name = user.name; if (name === null) {
يستغرق الأمر بعض الوقت للراحة ، لكن من الناحية النظرية ، لا يختلف هذا كثيرًا عن try/catch
الإرجاع مع العودة.
لاحظ ، مع ذلك ، أن التأثيرات الجبرية هي أداة أكثر فاعلية من مجرد try/catch
. خطأ الاسترداد هو مجرد واحدة من العديد من حالات الاستخدام الممكنة. لقد بدأت بهذا المثال فقط لأنه كان أسهل بالنسبة لي لفهمه.
وظيفة لا يوجد لديه اللون
الآثار الجبرية لها آثار مثيرة للاهتمام على الشفرة غير المتزامنة.
في اللغات ذات async/await
عادة ما يكون للوظائف "لون" ( روسي ). على سبيل المثال ، في JavaScript ، لا يمكننا فقط جعل getName
غير متزامن دون إصابة makeFriends
ووظائف الاتصال الخاصة به makeFriends
. قد يكون هذا ألمًا حقيقيًا إذا كان جزءًا من الكود يحتاج أحيانًا إلى أن يكون متزامنًا وأحيانًا غير متزامن.
تعمل مولدات JavaScript بطريقة مماثلة : إذا كنت تعمل مع المولدات ، فيجب أن تعرف جميع الشفرات الوسيطة أيضًا عن المولدات.
حسنًا ، ما علاقة الأمر بها؟
للحظة ، دعنا ننسى التزامن / ننتظر ونعود إلى مثالنا:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
ماذا لو لم يستطع معالج التأثيرات إرجاع "اسم الفراغ" بشكل متزامن؟ ماذا لو أردنا الحصول عليها من قاعدة البيانات؟
اتضح أنه يمكننا استدعاء resume with
بشكل غير متزامن من معالج التأثير لدينا دون إجراء أي تغييرات على getName
أو makeFriends
:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { setTimeout(() => { resume with ' '; }, 1000); } }
في هذا المثال ، ندعو resume with
ثانية فقط في وقت لاحق. يمكنك أن تنظر في resume with
رد الاتصال ، والتي يمكنك الاتصال مرة واحدة فقط. (يمكنك أيضًا التباهي بالأصدقاء من خلال تسمية هذا الشيء " استمرار محدود لمرة واحدة" (لم يتم تلقي المصطلح " استمرار محدد" ترجمة مستقرة إلى اللغة الروسية - الترجمة تقريبًا.).)
الآن يجب أن تكون آليات التأثير الجبري أكثر وضوحًا. عندما نلقي خطأً ، يقوم محرك جافا سكريبت بتدوير المجموعة من خلال تدمير المتغيرات المحلية في العملية. ومع ذلك ، عندما ننفذ التأثير ، يقوم محركنا الافتراضي بإنشاء رد اتصال (في الواقع "إطار استمراري" ، تقريبًا. الترجمة.) مع بقية وظيفتنا ، والاستئناف resume with
سوف نسميها.
مرة أخرى ، تذكير: بناء الجملة والكلمات الرئيسية المحددة يتم اختراعها بالكامل لهذه المقالة فقط. النقطة ليست في ذلك ، ولكن في الميكانيكا.
ملاحظة النظافة
تجدر الإشارة إلى أن الآثار الجبرية نشأت نتيجة لدراسة البرمجة الوظيفية. بعض المشاكل التي يحلونها فريدة فقط في البرمجة الوظيفية. على سبيل المثال ، في اللغات التي لا تسمح بتأثيرات جانبية عشوائية (مثل Haskell) ، يجب عليك استخدام مفاهيم مثل monads لسحب التأثيرات من خلال البرنامج. إذا كنت قد قرأت يومًا البرنامج التعليمي الأحادي ، فأنت تعلم أنه قد يكون من الصعب فهمه. تساعد الآثار الجبرية على القيام بشيء مشابه بجهد أقل.
هذا هو السبب في أن معظم المناقشات حول الآثار الجبرية غير مفهومة تماما بالنسبة لي. ( لا أعرف هاسكل و "أصدقائه".) ومع ذلك ، أعتقد أنه حتى في لغة غير نظيفة مثل جافا سكريبت ، يمكن أن تكون التأثيرات الجبرية أداة قوية للغاية لفصل "ماذا" عن "كيف" في الكود.
إنها تتيح لك كتابة التعليمات البرمجية التي تصف ما تفعله:
function enumerateFiles(dir) { const contents = perform OpenDirectory(dir); perform Log('Enumerating files in ', dir); for (let file of contents.files) { perform HandleFile(file); } perform Log('Enumerating subdirectories in ', dir); for (let directory of contents.dir) {
ثم لفها لاحقًا بشيء يصف "كيف" تفعل ذلك:
let files = []; try { enumerateFiles('C:\\'); } handle(effect) { if (effect instanceof Log) { myLoggingLibrary.log(effect.message); resume; } else if (effect instanceof OpenDirectory) { myFileSystemImpl.openDir(effect.dirName, (contents) => { resume with contents; }); } else if (effect instanceof HandleFile) { files.push(effect.fileName); resume; } }
مما يعني أن هذه الأجزاء يمكن أن تصبح مكتبة:
import { withMyLoggingLibrary } from 'my-log'; import { withMyFileSystem } from 'my-fs'; function ourProgram() { enumerateFiles('C:\\'); } withMyLoggingLibrary(() => { withMyFileSystem(() => { ourProgram(); }); });
بخلاف المزامنة / الانتظار أو المولدات ، لا تتطلب التأثيرات الجبرية تعقيد الوظائف "الوسيطة". قد تكون دعوتنا إلى enumerateFiles
عميقة داخل برنامجنا ، ولكن طالما يوجد في مكان ما يوجد معالج تأثير لكل من التأثيرات التي يمكنه تنفيذها ، فسيستمر عمل التعليمات البرمجية الخاصة بنا.
تسمح لنا معالجات التأثير بفصل منطق البرنامج عن تطبيقات محددة لآثاره دون رقصات غير ضرورية وكود boilerplate. على سبيل المثال ، يمكننا إعادة تعريف السلوك تمامًا في الاختبارات من أجل استخدام نظام الملفات المزيف وإجراء لقطات من السجلات بدلاً من عرضها على وحدة التحكم:
import { withFakeFileSystem } from 'fake-fs'; function withLogSnapshot(fn) { let logs = []; try { fn(); } handle(effect) { if (effect instanceof Log) { logs.push(effect.message); resume; } }
نظرًا لأن الدوال لا تحتوي على "لون" (لا يجب أن يعرف الرمز الوسيط التأثيرات) ، ويمكن تكوين معالجات التأثير (يمكن تداخلها) ، يمكنك إنشاء تجريدات تعبيرية للغاية معهم.
أنواع ملاحظة
نظرًا لأن التأثيرات الجبرية تأتي من اللغات المكتوبة بشكل ثابت ، فإن معظم النقاش حولها يركز على كيفية التعبير عنها في الأنواع. لا شك أن هذا أمر مهم ، لكنه قد يعقد أيضًا فهم المفهوم. لهذا السبب لا يتحدث هذا المقال عن الأنواع على الإطلاق. ومع ذلك ، أود أن أشير إلى أنه عادة ما يتم ترميز حقيقة أن وظيفة ما يمكن أن تؤدي تأثير في توقيع من نوعه. وبالتالي ، ستتم حمايتك من الموقف عند إجراء تأثيرات غير متوقعة ، أو لا يمكنك تتبع مصدرها.
يمكنك هنا ذكر أن تأثيرات الجبر تقنيًا "تعطي اللون" للوظائف باللغات المكتوبة بشكل ثابت ، لأن التأثيرات جزء من توقيع الكتابة. انها حقا. ومع ذلك ، فإن إصلاح تعليق الكتابة التوضيحية للدالة الوسيطة لتضمين تأثير جديد ليس بحد ذاته تغييرًا دلاليًا - على عكس إضافة المتزامن أو تحويل الوظيفة إلى مولد. يمكن أن يساعد استنتاج الكتابة أيضًا في تجنب الحاجة إلى تغييرات متتالية. يتمثل الاختلاف المهم في أنه يمكنك "منع" الآثار عن طريق إدخال كعب روتين أو تنفيذ مؤقت (على سبيل المثال ، استدعاء التزامن لتأثير غير متزامن) ، والذي ، إذا لزم الأمر ، يسمح لك بمنع تأثيره على الشفرة الخارجية - أو تحويله إلى تأثير آخر.
هل أحتاج إلى تأثيرات جبرية في JavaScript؟
بصراحة ، أنا لا أعرف. إنها قوية جدًا ، ويمكن القول إنها قوية جدًا بالنسبة إلى لغة مثل JavaScript.
أعتقد أنها يمكن أن تكون مفيدة جدًا للغات التي يكون التداخل فيها نادرًا وحيث تدعم المكتبة القياسية التأثيرات تمامًا. إذا قمت أولاً بإجراء perform Timeout(1000), perform Fetch('http://google.com')
وقمت perform ReadFile('file.txt')
perform Timeout(1000), perform Fetch('http://google.com')
، perform ReadFile('file.txt')
، وكان لغتك "مطابقة الأنماط" وكتابة ثابتة للتأثيرات ، ثم هذا يمكن أن يكون بيئة برمجة لطيفة جدا.
ربما ستترجم هذه اللغة في جافا سكريبت!
ماذا يجب أن نفعل هذا مع React؟
ليست كبيرة جدا يمكنك حتى القول إنني أسحب بومة على الكرة الأرضية.
إذا شاهدت حديثي حول Time Slicing and Suspense ، فإن الجزء الثاني يتضمن مكونات تقرأ البيانات من ذاكرة التخزين المؤقت:
function MovieDetails({ id }) {
(يستخدم التقرير واجهة برمجة تطبيقات مختلفة قليلاً ، ولكن هذا ليس هو الموضوع.)
يعتمد هذا الكود على وظيفة React لعينات البيانات المسماة " Suspense
" ، والتي هي قيد التطوير النشط حاليًا. الشيء المثير للاهتمام هنا ، بالطبع ، هو أن البيانات قد لا تكون موجودة في movieCache - في هذه الحالة ، نحتاج إلى القيام بشيء أولاً ، لأنه لا يمكننا متابعة التنفيذ. من الناحية الفنية ، في هذه الحالة ، تطالب الدعوة إلى قراءة () الوعد (نعم ، رمي الوعد - عليك ابتلاع هذه الحقيقة). هذا يتوقف التنفيذ. React يعترض هذا الوعد ويتذكر أنه من الضروري تكرار تقديم شجرة المكونات بعد وفاء الوعد.
هذا ليس تأثير جبري في حد ذاته ، على الرغم من أن إنشاء هذه الحيلة كان مستوحى منهم. تحقق هذه الخدعة نفس الهدف: بعض الكود الموجود أدناه في مكدس الاستدعاء أدنى مؤقتًا من شيء أعلى في مكدس الاستدعاءات (في هذه الحالة ، React) ، في حين أن جميع الوظائف الوسيطة لا يجب أن تعرف عنها أو تكون "مسمومة" بواسطة المتزامن أو المولدات. بالطبع ، لا يمكننا "فعليًا" استئناف التنفيذ في جافا سكريبت ، ولكن من وجهة نظر React ، فإن إعادة عرض شجرة المكون بعد إذن الوعد هو نفسه تقريبًا. يمكنك الغش عندما يفترض نموذج البرمجة الخاص بك العاطفية!
الخطافات هي مثال آخر يمكن أن يذكرك بالتأثيرات الجبرية. أحد الأسئلة الأولى التي يطرحها الناس هي: إلى أين يتصل useState بـ "معرفة" العنصر الذي يشير إليه؟
function LikeButton() {
لقد شرحت هذا بالفعل في نهاية هذه المقالة : في كائن React هناك حالة قابلة للتغيير "المرسل الحالي" ، مما يشير إلى التطبيق الذي تستخدمه حاليًا (على سبيل المثال ، في react-dom
). وبالمثل ، هناك خاصية مكون حالية تشير إلى بنية البيانات الداخلية LikeButton. وإليك كيف يكتشف useState ما يجب القيام به.
قبل أن يعتادوا على ذلك ، غالبًا ما يظن الناس أنه يشبه الاختراق القذر لسبب واضح. من الخطأ الاعتماد على حالة عامة قابلة للتغيير. (ملاحظة: كيف تعتقد أن تطبيق try / catch في محرك JavaScript؟)
ومع ذلك ، من الناحية النظرية ، يمكنك اعتبار useState () بمثابة تأثير لتنفيذ State () ، والذي تتم معالجته بواسطة React عند تنفيذ المكون الخاص بك. هذا "يفسر" لماذا React (ما يستدعي المكون الخاص بك) يمكن أن توفر لها الحالة (أعلى في مكدس الاستدعاءات ، بحيث يمكن أن توفر معالج التأثير). في الواقع ، يعد تطبيق الحالة الصريح أحد أكثر الأمثلة شيوعًا في الكتب المدرسية عن الآثار الجبرية التي واجهتها.
مرة أخرى ، بالطبع ، هذه ليست الطريقة التي يعمل بها React بالفعل ، لأنه ليس لدينا أي تأثيرات جبرية في JavaScript. بدلاً من ذلك ، يوجد حقل مخفي نحفظ فيه المكون الحالي ، بالإضافة إلى حقل يشير إلى "المرسل" الحالي مع استخدام useState. كتحسين للأداء ، هناك تطبيقات useState منفصلة للتركيبات والتحديثات . ولكن إذا كنت الآن ملتوية جدًا بموجب هذا الرمز ، فيمكنك اعتبارهم معالجات التأثير العادي.
بإيجاز ، يمكننا القول أنه في JavaScript JavaScript يمكن أن تعمل كتقريب أولي لتأثيرات I / O (بشرط أن يمكن تنفيذ الرمز بأمان لاحقًا ، وطالما أنه غير مرتبط بوحدة المعالجة المركزية) ، والحقل المتغير " المرسل "المستعادة في المحاولة / يمكن أن يكون بمثابة تقريب تقريبي لمعالجات التأثيرات المتزامنة.
يمكنك الحصول على تطبيق عالي الجودة للتأثيرات باستخدام المولدات ، لكن هذا يعني أنه سيتعين عليك التخلي عن الطبيعة "الشفافة" لوظائف JavaScript وعليك القيام بكل شيء باستخدام المولدات. وهذا "جيد ، هذا ..."
أين يمكن معرفة المزيد
أنا شخصياً فوجئت بمدى تأثير الآثار الجبرية التي اكتسبتها بالنسبة لي. كنت دائمًا ما أبذل قصارى جهدي لفهم المفاهيم المجردة ، مثل الموناديات ، لكن التأثيرات الجبرية أخذت ببساطة و "تحولت" في الرأس. آمل أن يساعدهم هذا المقال في "الانضمام" معك.
لا أعرف ما إذا كانت ستبدأ استخدامها بكميات كبيرة. أعتقد أنني سأصاب بخيبة أمل إذا لم تتجذر في أي من اللغات الرئيسية بحلول عام 2025. ذكرني للتحقق في خمس سنوات!
أنا متأكد من أنه يمكنك القيام به أكثر إثارة للاهتمام معهم ، ولكن من الصعب حقًا أن تشعر بقوتها حتى تبدأ في كتابة التعليمات البرمجية واستخدامها. إذا أثار هذا المنشور فضولك ، فإليك بعض الموارد الإضافية التي يمكنك من خلالها قراءة المزيد من التفاصيل:
أشار العديد من الأشخاص أيضًا إلى أنه إذا حذفت جانب الكتابة (كما فعلت في هذه المقالة) ، فيمكنك العثور على استخدام مبكر لهذه التقنية في نظام الحالة في Common Lisp. , , call/cc .