تُعيد الدوالّ العادية قيمة واحدة فقط لا غير (أو لا تُعيد شيئًا).
بينما يمكن للمولّدات إعادة (أو إنتاج yeild) أكثر من قيمة واحدةً بعد الأخرى حسب الطلب. تعمل المولّدات عملًا جميلًا جدًا مع الكائنات المكرَّرة (Iterables) في جافاسكربت وتتيح لنا إنشاء سيول البيانات بسهولة بالغة.
الدوال المولدة
لإنشاء مولّد علينا استعمال صياغة مميّزة: function*
أو ما يسمّونه ”الدالة المولِّدة“.
هذا شكلها:
function* generateSequence() { yield 1; yield 2; return 3; }
يختلف سلوك الدوال المولِّدة عن تلك العادية، فحين تُستدعى الدالة لا تُشغّل الشيفرة فيها، بل تُعيد كائنًا مميزًا نسمّيه ”كائن المولّد“ ليُدير عملية التنفيذ.
خُذ نظرة:
function* generateSequence() { yield 1; yield 2; return 3; } // تنشئ الدالة المولِّدة كائن مولّد let generator = generateSequence(); alert(generator); // [object Generator]
الشيفرة الموجودة بداخل الدالّة لم تُنفذ بعد:
التابِع الأساسي للمولّد هو next()
. متى استدعيناه بدأ عملية التنفيذ حتّى يصل أقرب جملة yield <value>
(يمكن ألّا نكتب value
وستصير القيمة undefined
). بعدها يتوقّف تنفيذ الدالة مؤقّتًا وتُعاد القيمة value
إلى الشيفرة الخارجية.
ناتج التابِع next()
لا يكون إلّا كائنًا له خاصيتين:
-
value
: القيمة التي أنتجها المولّد. -
done
: القيمةtrue
لو اكتملت شيفرة الدالة، وإلّاfalse
.
فمثلًا هنا نُنشِئ مولّدًا ونأخذ أوّل قيمة أنتجها:
function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); let one = generator.next(); // هنا alert(JSON.stringify(one)); // {value: 1, done: false}
حاليًا أخذنا القيمة الأولى فقط، وسير تنفيذ الدالة موجود في السطر الثاني:
فلنستدعِ generator.next()
ثانيةً الآن. سنراه واصل تنفيذ الشيفرة وأعاد القيمة المُنتَجة yield
التالية:
let two = generator.next(); alert(JSON.stringify(two)); // {value: 2, done: false}
والآن إن شغّلناه مرّة ثالثة سيصل سير التنفيذ إلى عبارة return
ويُنهي الدالة:
let three = generator.next(); alert(JSON.stringify(three)); // {value: 3, done: true} // لاحِظ قيمة done
الآن اكتمل المولّد بقيمة value:3
ويمكننا الآن معالجتها. عرفنا ذلك من done:true
.
الاستدعاءات اللاحقة على generator.next()
لن تكون منطقية الآن. ولو حصلت فستُعيد الدالة الكائن نفسه: {done: true}
.
ملاحظة: الصياغة function* f(…)
أم function *f(…)
؟ في الحقيقة كِلاهما صحيح. ولكن عادةً تكون الصياغة الأولى مفضلة أكثر من الثانية. وتشير النجمة *
على أنها دالة مولّد، إذ تصف النوع وليس الاسم، لذلك يجب أن ندمج الكلمة المفتاحية function
بالنجمة.
المولدات قابلة للتكرار
نفترض أنّك توقّعت ذلك حين رأيت التابِع next()
، إذ أن المولدات قابلة للتكرار iterable، فيمكننا المرور على عناصره عبر for..of
:
function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); for(let value of generator) { alert(value); // 1 ثمّ 2 }
هذا أجمل من استدعاء .next().value
، أم لا؟
ولكن… لاحِظ: يعرض المثال أعلاه 1
ثمّ 2
وفقط. لن يعرض 3
مطلقًا!
هذا لأنّ عملية التكرار لـِ for..of
تتجاهل قيمة value
الأخيرة حين تكون done: true
. لذا لو أردنا أن تظهر النتائج كلّها لعملية تكرار for..of
، فعلينا إعادتها باستعمال yield
:
function* generateSequence() { yield 1; yield 2; yield 3; // هكذا } let generator = generateSequence(); for(let value of generator) { alert(value); // 1 ثمّ 2 ثمّ 3 }
نظرًا من كون المولّدات قابلة للتكرار، يمكننا استدعاء جميع الدوالّ المتعلّقة بذلك، مثل: معامل «البقية» ...
:
function* generateSequence() { yield 1; yield 2; yield 3; } let sequence = [0, ...generateSequence()]; alert(sequence); // 0, 1, 2, 3
يحول التابع ...generateSequence()
في الشيفرة أعلاه كائن المولد القابل للتكرار إلى مصفوفة من العناصر (لمزيد من المعلومات أحيلك إلى هذا المقال "المُعاملات «البقية» ومُعامل التوزيع").
استعمال المولدات على أنها مكرّرات
سابقًا في فصل "المُعاملات «البقية» ومُعامل التوزيع" أنشأنا كائن range
يمكن تكراره والذي يعيد القيم بين قيميتين from..to
.
لنتذكّر الشيفرة معًا:
let range = { from: 1, to: 5, // تستدعي حلقة for..of هذه الدالّة مرة واحدة في البداية [Symbol.iterator]() { // ...ستعيد كائن مُكرُر // لاحقًا ستعمل حلقة for..of مع ذلك الكائن وتطلب منه القيم التالية return { current: this.from, last: this.to, // تستدعى next() في كلّ تكرار من خلال الحلقة for..of next() { // يجب أن تعيد القيم ككائن {done:.., value :...} if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; // عملية التكرار تمرُّ عبر range من range.from إلى range.to alert([...range]); // 1,2,3,4,5
يمكننا استعمال دالة المولِّدة كمكرّرات من خلال Symbol.iterator
.
إليك نفس الكائن range
, ولكن بطريقة أكثر إيجازًا:
let range = { from: 1, to: 5, *[Symbol.iterator]() { // اختصارًا لـِ [Symbol.iterator]: function*() for(let value = this.from; value <= this.to; value++) { yield value; } } }; alert( [...range] ); // 1,2,3,4,5
الشيفرة تعمل إذ يُعيد range[Symbol.iterator]()
الآن مولّدًا، وما تتوقّعه for..of
هي توابِع تلك المولّدات بعينها:
-
إذ لها التابِع
.next()
-
وتُعيد القيم على النحو الآتي
{value: ..., done: true/false}
بالطبع هذه ليست مصادفة أضيفت المولّدات إلى لغة جافاسكربت مع الأخذ بعين الاعتبار للمكرّرات لتنفيذهم بسهولة.
إن التنوع في المولدات أعطى شيفرة موجزة أكثر الشيفرة الأصلية لكائن range
، وجميعهم لديهم نفس الخصائص الوظيفية.
ملاحظة: المولّدات يمكن أن تولد قيمًا للأبد في الأمثلة أعلاه أنشأنا متتالية منتهية من القيم، ولكن باستخدام المولد نستطيع أن ننتج قيمًا إلى الأبد. على سبيل المثال لننشئ متتالية غير منتهية من الأرقام العشوائية الزائفة.
ومن المؤكد أن هذا المولّد سيحتاجُ إلى طريقة لإيقافه مثل: break
(أو return
) في حلقة for..of
. وإلا فإن الحلقة ستستمر إلى الأبد.
تراكب المولّدات
تراكب المولّدات (Generator composition) هي ميزة خاصّة للمولّدات تتيح لها ”تضمين“ المولّدات الأخرى فيها دون عناء.
فمثلًا لدينا هذه الدالة التي تُولّد سلسلة من الأعداد:
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; }
نريد الآن إعادة استعمالها لتوليد سلسلة أعداد معقّدة أكثر من هذه أعلاه:
-
أولًا الأرقام
0..9
(ورموز المحارف اليونيكوديّة هو من 48 إلى 57) -
ثمّ الأحرف الأبجدية الإنجليزية بالحالة الكبيرة
A..Z
(ورموزها من 65 إلى 90) -
ثمّ الأحرف الأبجدية الإنجليزية بالحالة الصغيرة
a..z
(ورموزها من 97 إلى 122)
يمكننا استعمال هذه السلسلة لإنشاء كلمات السرّ باختيار المحارف منها مثلًا (ويمكننا إضافة المحارف الخاصّة أيضًا). ولكن لذلك علينا توليدها أولًا.
علينا في الدوالّ العادية (لتضمين النواتج من دوالّ أخرى) استدعاء تلك الدوالّ وتخزين نواتجها ومن ثمّ ربطها في نهاية الدالة الأمّ.
وفي المولّدات نستعمل الصياغة المميّزة yield*
لتضمين (أو تركيب) مولّدين داخل بعض.
المولّد المركّب:
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generatePasswordCodes() { // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); } let str = ''; for(let code of generatePasswordCodes()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z
توجه yield*
التنفيذ إلى مولّد آخر. مما يعني أن yield* gen
ستكرر عبر المولّد gen
وستُعيد نتائجه للخارج. أي كما او أنتجت هذه القيم بمولّد خارجي.
النتيجة نفسها كما لو أننا ضمنَا الشيفرة من المولّدات المتداخلة:
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generateAlphaNum() { // yield* generateSequence(48, 57); for (let i = 48; i <= 57; i++) yield i; // yield* generateSequence(65, 90); for (let i = 65; i <= 90; i++) yield i; // yield* generateSequence(97, 122); for (let i = 97; i <= 122; i++) yield i; } let str = ''; for(let code of generateAlphaNum()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z
تراكب المولّدات هو فعلًا طريقة بديهيّة لإدخال أكثر من مولّد في مولّد واحد، ولا تستعمل ذاكرة إضافية لتخزين النواتج البينيّة.
عبارة ”yield“ باتجاهين اثنين
إلى هنا نرى المولّدات تشبه كائنات المكرّرات كثيرًا، فقط أنّ لها صياغة مميّزة لتوليد القيم. ولكن وعلى أرض الواقع، المولّدات أكثر مرونة وفاعليّة.
إذ أنّ yield
تعمل بالمجيء وبالإياب: فلا تُعيد الناتج إلى الخارج فحسب بل أيضًا يمكنها تمرير القيمة إلى داخل المولّد.
لذلك علينا استدعاء generator.next(arg)
بتمرير وسيط واحد. هذا الوسيط سيكون ناتج yield
.
الأفضل لو نرى مثالًا:
function* gen() { // نمرّر سؤالًا إلى الشيفرة الخارجية وننتظر إجابةً عليه let result = yield "2 + 2 = ?"; // (*) alert(result); } let generator = gen(); let question = generator.next().value; // <-- تُعيد yield القيمة generator.next(4); // --> نمرّر القيمة إلى المولّد
-
يكون الاستدعاء الأوّل للتابِع
generator.next()
دومًا دون تمرير أيّ وسيط. يبدأ الاستدعاء التنفيذَ ويُعيد ناتج أوّلyield "2+2=?"
. هنا يُوقف المولّد التنفيذ مؤقّتًا (على ذلك السطر). -
ثمّ (كما نرى في الصورة) يُوضع ناتج
yield
في متغير السؤالquestion
في الشيفرة التي استدعت المولّد. -
وعند
generator.next(4)
يُواصل المولّد عمله ويستلم4
ناتجًا:let result = 4
.
لاحِظ أنّه ليس على الشيفرة الخارجية استدعاء next(4)
مباشرةً وفي الحال، بل يمكن أن تأخذ الوقت الذي تريد. سيبقى المولّد منتظرًا ولن تكون مشكلة.
مثال:
// نُواصل عمل المولّد بعد زمن معيّن setTimeout(() => generator.next(4), 1000);
كما نرى فعلى العكس تمامًا من الدوال العادية، يمكن للمولّد ولشيفرة الاستدعاء تبادل النتائج بتمرير القيم إلى next/yield
.
ليتوضّح هذا أكثر سنرى مثالًا آخر فيه استدعاءات أكثر:
function* gen() { let ask1 = yield "2 + 2 = ?"; alert(ask1); // 4 let ask2 = yield "3 * 3 = ?" alert(ask2); // 9 } let generator = gen(); alert( generator.next().value ); // "2 + 2 = ?" alert( generator.next(4).value ); // "3 * 3 = ?" alert( generator.next(9).done ); // true
إليك صورة سير التنفيذ:
-
استدعاء
.next()
الأوّل يبدأ التنفيذ، ويصل إلى أوّل عبارةyield
. - يُعاد الناتج إلى الشيفرة الخارجية.
-
يمرّر استدعاء
.next(4)
الثاني القيمة4
إلى المولّد ثانيةً على أنّها ناتج أوّل مُنتَجyield
، ويُواصل التنفيذ. -
يصل التنفيذ إلى عبارة
yield
الثانية، وتصير هي ناتج الاستدعاء. -
يُمرّر
next(9)
الثالث القيمة9
إلى المولّد على أنّها ناتج ثاني مُنتَجyield
ويُواصل التنفيذ حتّى يصل نهاية الدالة، بذلك تكونdone: true
.
تشبه هذه لعبة تنس الطاولة، حيث يمرّر كلّ تابِع next(value)
(باستثناء الأوّل طبعًا) القيمة إلى المولّد فتصير ناتج المُنتَج yield
الحالي، ومن ثمّ
generator.throw
يمكن للشيفرات الخارجية تمرير القيم إلى المولّدات على أنّها نواتج yield
(كما لاحظنا من الأمثلة أعلاه).
ويمكنها أيضًا بدء (أو رمي) خطأ أيضًا. هذا طبيعي إذ الأخطاء هي نواتج، نوعًا ما.
علينا استدعاء التابِع generator.throw(err)
لتمرير الأخطاء إلى عبارة yield
. في هذه الحال يُرمى الخطأ err
عند السطر الذي فيه yield
.
فمثلًا تؤدّي عبارة "2 + 2 = ?"
هنا إلى خطأ:
function* gen() { try { let result = yield "2 + 2 = ?"; // (1) alert("The execution does not reach here, because the exception is thrown above"); } catch(e) { alert(e); // أظهر الخطأ } } let generator = gen(); let question = generator.next().value; generator.throw(new Error("The answer is not found in my database")); // (2)
الخطأ الذي رميَ في المولد عند السطر (2)
يقودنا إلى الخطأ في السطر (1)
مع yield
. في المثال أعلاه التقطت try..catch
الخطأ وثمّ عرضته.
لو لم نلتقطه سيكون مآله (مآل أيّ استثناء آخر غيره) أن ”يسقط“ من المولّد إلى الشيفرة التي استدعت المولّد.
هل يمكننا التقاط الخطأ في سطر شيفرة الاستدعاء الّتي تحتوي على generator.throw
، (المشار إليه (2)
)، هكذا؟
function* generate() { let result = yield "2 + 2 = ?"; // خطأ في هذا السطر } let generator = generate(); let question = generator.next().value; try { generator.throw(new Error("The answer is not found in my database")); } catch(e) { alert(e); // عرض الخطأ }
إن لم نلتقط الخطأ هنا فعندئذ وكما هي العادة ستنهار الشيفرة الخارجية (إن كانت موجودة) وإذا لم يكتشف الخطأ أيضًا عندها سينهار السكربت بالكامل.
خلاصة
-
تُنشِئ الدوال المولِّدة
function* f(…) {…}
المولّدات. -
يوجد المُعامل
yield
داخل المولدات (فقط). -
تتبادل الشيفرة الخارجية مع المولدات النتائج من خلال استدعاءات
next/yield
.
نادرًا ما تستخدم المولّدات في الإصدار الحديث من جافاسكربت. لكن في بعض الأحيان نستخدمها لتبادل البيانات بين الشيفرة المستدعاة أثناء تنفيذ شيفرة وحيدة. وبالتأكيد إنها رائعة لتوليد أشياء قابلة للتكرار.
في الفصل التالي، سنتعرف على مولدات غير متزامنة، والّتي تُستخدم لقراءة تدفقات البيانات غير المتزامنة (على سبيل المثال ، نرى على الإنترنت خدمات كثيرة تقدّم لنا البيانات على صفحات [paginated]) في حلقات for await ... of
.
في برمجة الوِب، غالبًا ما نعمل مع البيانات المتدفقة، لذا فهذه حالة استخدام أخرى مهمة جدًا.
تمارين
مولد أرقام شبه عشوائية
نواجه كثيرًا من الأحيان حاجة ماسّة إلى بيانات عشوائية.
إحدى هذه الأحيان هي إجراء الاختبارات، فنحتاج إلى بيانات عشوائية كانت نصوص أو أعداد أو أيّ شيء آخر لاختبار الشيفرات والبنى البرمجية.
يمكننا في جافاسكربت استعمال Math.random()
، ولكن لو حدث خطب ما فنودّ إعادة إجراء الاختبار باستعمال نفس البيانات بالضبط (كي نختبر هذه البيانات).
لهذا الغرض نستعمل ما يسمّى ”بمولّدات الأعداد شبه العشوائية المزروعة“. تأخذ هذه المولّدات "البذرة“ والقيمة الأولى ومن ثمّ تولّد القيم اللاحقة باستعمال معادلة رياضية بحيث أنّ كلّ بذرة تُنتج نفس السلسلة دائمًا، وهكذا يمكننا ببساطة استنساخ التدفق بالكامل من خلال بذورها فقط.
إليك مثال عن هذه المعادلة التي تولّد قيمًا موزّعة توزيعًا
next = previous * 16807 % 2147483647
لو استعملنا 1
…، فستكون القيم كالآتي:
-
16807
-
282475249
-
1622650073
- …وهكذا…
مهمّة هذا التمرين هو إنشاء الدالة المولِّدة pseudoRandom(seed)
فتأخذ البذرة seed
وتُنشِئ مولّدًا بالمعادلة أعلاه.
طريقة الاستعمال:
let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073
الحل
function* pseudoRandom(seed) { let value = seed; while(true) { value = value * 16807 % 2147483647 yield value; } }; let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073
لاحِظ أننا نستطيع تأدية ذات الأمر بالدوال العادية هكذا:
function pseudoRandom(seed) { let value = seed; return function() { value = value * 16807 % 2147483647; return value; } } let generator = pseudoRandom(1); alert(generator()); // 16807 alert(generator()); // 282475249 alert(generator()); // 1622650073
ستعمل هذه أيضًا. ولكن بعد ذلك سنفقدُ قابلية التكرار باستخدام for..of
واستخدام المولّد المركب، وتلك ممكن أن تكون مفيدة في مكان آخر.
ترجمة -وبتصرف- للفصل Generators من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.