8.9 Iterators and Generators (ইটারেটর এবং জেনেরেটর)

 

Iterators (ইটারেটর)

ইটারেটর একটা অবজেক্টকে ইটারেট করতে সাহায্য করে। বেশিরভাগ সময় একটা অ্যারে অবজেক্টকে ইটারেট করার জন্য ইটারেটর ব্যবহার করা হলেও অন্যান্য অবজেক্ট যেমন ম্যাপ অবজেক্ট, স্ট্রিংকেও ইটারেট করা যায়। Iterable protocol কে implement করার মাধ্যমে যে কোনো অবজেক্টকে ইটারেবল বানানো যায়। ইটারেট করা জন্য for…of লুপ ব্যবহার করা হয়।

ব্যাসিক্যালি @@iterator function কে implement করে একটা অবজেক্টকে ইটারেবল বানানো যায়। এই ফাংশনটি একটা ইটারেটর অবজেক্ট রিটার্ন করে যার মধ্যে একটা next ফাংশন থাকে। এই next ফাংশনটি একটি অবজেক্ট রিটার্ন করে যার দুটো attribute – done এবং value। প্রথমটি একটি বুলিয়ান যার দ্বারা লুপটা খুব সাবধানতার সাথে হ্যান্ডেল করা হয় যাতে লুপটা infinite loop না হয়ে যায়। নিচে একটা উদাহরণ দেওয়া হলো-

const userNamesGroupedByLocation = {
  Tokio: [
    'Aiko',
    'Chizu',
    'Fushigi',
  ],
  'Buenos Aires': [
    'Santiago',
    'Valentina',
    'Lola',
  ],
  'Saint Petersburg': [
    'Sonja',
    'Dunja',
    'Iwan',
    'Tanja',
  ],
};


userNamesGroupedByLocation[Symbol.iterator] = function() {
  const cityKeys = Object.keys(this);
  let cityIndex = 0;
  let userIndex = 0;

  return {
    next: () => {
      if (cityIndex > cityKeys.length - 1) {
        return {
          value: undefined,
          done: true,
        };
      }

      const users = this[cityKeys[cityIndex]];
      const user = users[userIndex];

      const isLastUser = userIndex >= users.length - 1;

      userIndex++;
      if (isLastUser) {
        userIndex = 0;
        cityIndex++
      }

      return {
        done: false,
        value: user,        
      };
    },
  };
};

 

Generators (জেনেরেটর)

ইটারেটর প্রোগ্রাম করার সময় খুবই সতর্ক থাকতে হয়, না হলে সিরিয়াস লেভেলের বাগ হ্যান্ডেল করা এবং ইন্টারনাল লজিক ম্যানেজ করা কঠিন এবং চ্যালেঞ্জিং হয়ে যায়। এই ক্ষেত্রে জেনারেটর একটা খুবই উপকারী টুল। একটা ফানশন ডিফাইন করার মাধ্যমে জেনারেটরের সাহায্যে ইটারেটর তৈরি করা যায় যেটা less error-prone এবং efficient ইটারেটর তৈরি করে। জেনারেটর আর ইটারেটরের একটা গুরুত্বপূর্ণ বৈশিষ্ট্য হচ্ছে যে তারা প্রয়োজনানুসারে execution stop আর continue করার সুযোগ দেয়।

জেনারেটর ফাংশন তৈরি করার জন্য ফাংশনের নামের আগে (*) বসাতে হয়। ফাংশনের কোন অংশ বা লাইন ইটারেট করতে হবে তা বোঝার জন্য yield ব্যবহার করা হয়। এর ফলে next function সেই লাইনের স্টেট্মেন্ট একটা ইটারেটর অবজেক্ট হিসেবে রিটার্ন করে। যখন ফাংশনটি শেষে পৌঁছে যায়, তখন value = undefined এবং done=true অটো সেট হয়ে যায়।

function* stringGenerator() {
  yield 'hi';
  yield 'hi';
}

const strings = stringGenerator();

strings.next();	 // {value: "hi", done: false}
strings.next();	 // {value: "hi", done: false}
strings.next();	 // {value: undefined, done: true}

next function এর বদলে return function ব্যবহার করলে পরবর্তী ইটারেশনে লুপ থেকে বের হয়ে যাবে, তখন value = undefined এবং done=true সেট হয়ে যাবে।

strings.next(); 	 // { value: "hi", done: false }
strings.return();	 // { value: "hi", done: true }
strings.next();	 // { value: undefined, done: true }

Throw function implement করার মাধ্যমে এক্সিকিউশন টার্মিনেট করে এরর থ্রো করা যায়, তখন পরবর্তী ইটারেশনে value = undefined এবং done=true রিটার্ন করবে।

strings.next(); 		 // { value: "hi", done: false }
strings..throw('Bam!');	 // Bam!
strings.next();		 // { value: undefined, done: true }

জেনারেটর ব্যবহার করার একটি অন্যতম কেইস হচ্ছে Unique ID generate করা। জেনারেটরের মাধ্যমে একটা ইনফিনিট লুপ তৈরি করে প্রতিটি ইটারেশনে নতুন আইডি তৈরি করা যায়। প্রয়োজনানুসারে next function কল করে নতুন আইডি পাওয়া যাবে, বাকিটা জেনারেটর বুঝে নেবে।