اذهب إلى المحتوى

المولدات Generators في جافاسكربت


صفا الفليج

تُعيد الدوالّ العادية قيمة واحدة فقط لا غير (أو لا تُعيد شيئًا).

بينما يمكن للمولّدات إعادة (أو إنتاج 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]

الشيفرة الموجودة بداخل الدالّة لم تُنفذ بعد:

generateSequence-1.png

التابِع الأساسي للمولّد هو 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}

حاليًا أخذنا القيمة الأولى فقط، وسير تنفيذ الدالة موجود في السطر الثاني:

generateSequence-2.png

فلنستدعِ generator.next()‎ ثانيةً الآن. سنراه واصل تنفيذ الشيفرة وأعاد القيمة المُنتَجة yield التالية:

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}

 

generateSequence-3.png

والآن إن شغّلناه مرّة ثالثة سيصل سير التنفيذ إلى عبارة return ويُنهي الدالة:

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true} // ‫لاحِظ قيمة done

 

generateSequence-4.png

الآن اكتمل المولّد بقيمة 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); // --> نمرّر القيمة إلى المولّد  

 

genYield2.png

  1. يكون الاستدعاء الأوّل للتابِع generator.next()‎ دومًا دون تمرير أيّ وسيط. يبدأ الاستدعاء التنفيذَ ويُعيد ناتج أوّل yield "2+2=?‎". هنا يُوقف المولّد التنفيذ مؤقّتًا (على ذلك السطر).
  2. ثمّ (كما نرى في الصورة) يُوضع ناتج yield في متغير السؤال question في الشيفرة التي استدعت المولّد.
  3. وعند 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

إليك صورة سير التنفيذ:

genYield2-2.png

  1. استدعاء .next()‎ الأوّل يبدأ التنفيذ، ويصل إلى أوّل عبارة yield.
  2. يُعاد الناتج إلى الشيفرة الخارجية.
  3. يمرّر استدعاء .next(4)‎ الثاني القيمة 4 إلى المولّد ثانيةً على أنّها ناتج أوّل مُنتَج yield، ويُواصل التنفيذ.
  4. يصل التنفيذ إلى عبارة yield الثانية، وتصير هي ناتج الاستدعاء.
  5. يُمرّر 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 …، فستكون القيم كالآتي:

  1. 16807
  2. 282475249
  3. 1622650073
  4. …وهكذا…

مهمّة هذا التمرين هو إنشاء الدالة المولِّدة 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


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...