এই পর্বে যা শিখবেন
for…of-এর ভেতরে আসলে কী ঘটে — iterable, iterator আরnext()-এর কথোপকথন হাতে চালিয়ে দেখা- কোন built-in জিনিস iterable, কোনগুলো নয়, আর নিজের class (Range, Playlist) কীভাবে iterable বানাবেন
function*,yieldআরyield*— generator কীভাবে থামে, আবার চলে, আর কখন শেষ হয়- অসীম generator,
takeআর lazy পাইপলাইন; array-এর সাথে তুলনা আর ES2025-এর iterator helper next(value),return(),throw(),try…finallyদিয়ে পরিষ্কার, আর async generator ওfor await…of-এর প্রথম পরিচয়
১ · for…of-এর পেছনের চুক্তি — iterable আর iterator
পর্ব ১২-এ for…of দেখেছেন: for (const item of cart) লিখলে একটি একটি করে পণ্য আসে। পর্ব ১৭-এ spread আর destructuring দেখেছেন, পর্ব ১৮-এ Map আর Set ঘুরেছেন। এই সবকিছু কাজ করে একটিমাত্র নিয়মের ওপর। এই পর্বে আমরা সেই নিয়মটি খুলে দেখব, তারপর নিজের হাতে বানাব।
প্রশ্নটি সহজ: for…of কীভাবে জানে একটি array-এর পরের মান কোনটি, একটি string-এর পরের অক্ষর কোনটি, বা একটি Map-এর পরের জোড়া কোনটি? এদের ভেতরের গঠন সম্পূর্ণ আলাদা। উত্তর হলো — for…of জানে না, জানতেও চায় না। সে শুধু জিনিসটিকে বলে, “আমাকে একজন সাহায্যকারী দাও”, তারপর সেই সাহায্যকারীকে বারবার বলে, “পরেরটা দাও”।
একটি ব্যাংকের টোকেন ব্যবস্থার কথা ভাবুন। কাউন্টারের কর্মী জানেন না লাইনে কে দাঁড়িয়ে আছেন, কতজন আছেন। তিনি শুধু বোতাম চাপেন — “পরের জন”। মেশিন একটি নম্বর দেখায়, অথবা জানায় “আর কেউ নেই”। কর্মীর কাজ একই থাকে, লাইনটি যেমনই হোক। এখানে লাইনটি হলো iterable (যার মানগুলো একে একে নেওয়া যায়), টোকেন মেশিন হলো iterator (যে মনে রাখে এখন কোথায় আছে), আর কর্মী হলো for…of. সকালে নতুন করে শুরু করতে নতুন টোকেন রোল লাগে — একটি iterator একবার শেষ হলে আর ফেরে না।
চুক্তিটির দুটি অংশ, আর দুটিই ES2015 (ES6) থেকে আছে:
- Iterable protocol: জিনিসটির একটি method থাকবে, যার নাম একটি বিশেষ Symbol —
Symbol.iterator. এটি ডাকলে একটি iterator ফেরত আসবে। - Iterator protocol: iterator-এর একটি
next()method থাকবে, যা প্রতিবার একটি ছোট object দেবে:{ value: পরের মান, done: false }, অথবা মান শেষ হলে{ value: undefined, done: true }.
নামটি string না হয়ে Symbol কেন? যাতে কারও নিজের "iterator" নামের property-র সাথে সংঘর্ষ না হয় — পুরোনো কোড ভাঙে না। Symbol-এর পুরো গল্প পর্ব ২৮-এ। এখন একটি array-এর iterator হাতে নিয়ে নিজেই next() ডেকে দেখুন। “পরে” চাপুন।
1const letters = ['ক', 'খ'];2const it = letters[Symbol.iterator]();3it.next();4it.next();5it.next();6it.next();
| পরের index | |
| next() দিল | |
| for…of হলে |
- Iterator চাওয়া।
letters[Symbol.iterator]()একটি নতুন iterator দেয়। এটি array-এর কপি নয় — শুধু array-টির দিকে একটি আঙুল, আর একটি ছোট কাউন্টার যে মনে রাখে পরের index ০. - প্রথম next(). Iterator index ০-এর মান দেয় আর কাউন্টার এক বাড়ায়। ফল একটি object:
value-তে'ক', আরdone: falseমানে “আরও থাকতে পারে”। - দ্বিতীয় next(). এবার
'খ'. খেয়াল করুন, শেষ মান দেওয়ার সময়ওdoneএখনোfalse— iterator আগে থেকে বলে না যে এটিই শেষ। - তৃতীয় next(). আর মান নেই, তাই
{ value: undefined, done: true }.for…ofঠিক এইdone: trueদেখেই লুপ থামায়, আর এই শেষ object-এরvalueকোথাও ব্যবহার করে না। - আবার ডাকলে? শেষ হওয়া iterator শেষই থাকে — বারবার
done: true. আবার শুরু থেকে ঘুরতে চাইলে array থেকে নতুন iterator নিতে হবে। এই “একবারই ব্যবহারযোগ্য” স্বভাব পরে generator-এও দেখবেন।
Iterable হলো উৎস — যাকে বারবার নতুন iterator চাওয়া যায়। Iterator হলো একবারের যাত্রা — যে মনে রাখে কতদূর এসেছে। for…of = “[Symbol.iterator]() ডাকো, তারপর done: true না আসা পর্যন্ত next() ডাকো”।
এই আলাদা করাটাই নকশার সৌন্দর্য। একই array-এর ওপর দুটি লুপ একসাথে চালালে (একটির ভেতরে আরেকটি) প্রতিটি লুপ নিজের iterator পায়, তাই কেউ কারও জায়গা গুলিয়ে ফেলে না। আর যে জিনিস মান দেয়, তাকে জানতে হয় না কে মান নিচ্ছে — লুপ, spread, নাকি অন্য কিছু।
২ · কে iterable আর কে নয় — আর কারা iterable খায়
JavaScript-এর অনেক built-in জিনিস নিজে থেকেই iterable. প্রতিটি কী দেয়, তা জানা থাকলে অনেক বাগ এড়ানো যায়:
| জিনিস | for…of প্রতিবার দেয় | মনে রাখুন |
|---|---|---|
| Array | প্রতিটি মান, index-এর ক্রমে | keys(), values(), entries()-ও iterator দেয় |
| String | প্রতিটি code point | emoji ভাঙে না; কিন্তু “বা” আসে দুই ভাগে — ব আর া |
| Map | [key, value] জোড়া | ঢোকানোর ক্রমে (পর্ব ১৮) |
| Set | প্রতিটি অনন্য মান | ঢোকানোর ক্রমে |
| arguments | প্রতিটি argument | array নয়, তবু iterable |
| NodeList | প্রতিটি DOM node | querySelectorAll-এর ফল (পর্ব ৩৭) |
| Typed array, generator object | প্রতিটি মান | generator নিচে বিস্তারিত |
সাধারণ object {} | — চলে না | TypeError: order is not iterable |
সাধারণ object কেন iterable নয়? কারণ “object-এর পরের মান” কথাটির কোনো একক অর্থ নেই — key দেব, value দেব, নাকি জোড়া? ভাষা তাই অনুমান না করে আপনাকেই বেছে নিতে বলে: Object.keys(o), Object.values(o) বা Object.entries(o) — তিনটিই array দেয়, আর array iterable.
যারা iterable “খায়”
চুক্তির অন্য দিকে আছে যারা iterable নেয়। এই তালিকা দেখলে বুঝবেন একবার চুক্তি মানলে কত কিছু বিনা খরচে পাওয়া যায়:
for (const x of it)— লুপ[...it]আরfn(...it)— spreadconst [a, b] = it— array destructuring (প্রয়োজনের বেশি মান চায় না)Array.from(it, mapFn),new Map(it),new Set(it),Object.fromEntries(it)Promise.all(it)আর এর ভাইবোনেরা (পর্ব ৩৩), আর generator-এরyield*(অংশ ৫)
for…in আর for…of গুলিয়ে ফেলবেন না। for…in iteration protocol ব্যবহার করে না — সে object-এর enumerable property-র নাম string হিসেবে দেয়। Array-তে for…in দেয় "0", "1" (string!), string-এ দেয় index, আর Set বা Map-এ কিছুই দেয় না — লুপ নীরবে একবারও চলে না, কোনো error ছাড়াই। মান চাইলে সবসময় for…of.
৩ · নিজের iterable বানানো — একটি Range class
Python-এ range(1, 10, 2) আছে, JavaScript-এ নেই। চলুন বানাই — আর সেই সুযোগে চুক্তিটি নিজের হাতে মানি। পর্ব ২৬-এ class দেখেছেন; এখানে একটি class-এ শুধু একটি বাড়তি method লাগবে, যার নাম [Symbol.iterator] (বর্গ বন্ধনী মানে নামটি একটি হিসাব করা মান — এখানে Symbol). এই method প্রতিবার ডাকা হলে একটি নতুন iterator object বানিয়ে দেবে, যার নিজস্ব cur কাউন্টার closure-এ (পর্ব ২২) বন্দি থাকবে।
const it = ["ক", "খ"][Symbol.iterator]();
console.log(it.next(), it.next(), it.next());
class Range {
constructor(start, end, step = 1) { Object.assign(this, { start, end, step }); }
[Symbol.iterator]() {
let cur = this.start;
const { end, step } = this;
return {
next() {
if (cur > end) return { value: undefined, done: true };
cur += step;
return { value: cur - step, done: false };
},
};
}
}
const odd = new Range(1, 10, 2);
console.log([...odd], Math.max(...odd));
const [first, second] = odd;
console.log(first, second, Array.from(odd, (n) => n * 10));
console.log([..."👍বা"].length, "👍বা".length);
const order = { id: 7, total: 450 };
try { for (const x of order) console.log(x); }
catch (e) { console.log(`${e.name}: ${e.message}`); }প্রথম লাইন stepper-এর তিনটি { value, done } object. তারপর [ 1, 3, 5, 7, 9 ] 9 — শুধু একটি method যোগ করায় spread আর Math.max(...odd) দুটোই কাজ করল। 1 3 [ 10, 30, 50, 70, 90 ]: destructuring শুধু প্রথম দুটি মান নিল, আর Array.from-এর দ্বিতীয় argument প্রতিটি মান বদলে দিল। খেয়াল করুন, একই odd তিনবার ঘোরানো গেল — কারণ Range নিজে iterator নয়, iterable; প্রতিবার নতুন iterator বানায়। 3 4: 👍 একটি code point কিন্তু দুটি UTF-16 code unit, তাই spread গোনে ৩টি মান (👍, ব, া) আর .length গোনে ৪. শেষ লাইনটি Edge/Chrome-এর বার্তা TypeError: order is not iterable — V8 variable-এর নামটিই বার্তায় বসায়।
এভাবে হাতে iterator লেখা কাজ করে, কিন্তু খেয়াল করুন কত কিছু সামলাতে হলো: কাউন্টার, শেষ হলো কি না, ঠিক আকারের object ফেরত দেওয়া। আরেকটি বাস্তব উদাহরণ ভাবুন — একটি গানের Playlist class, যা ভেতরে একটি private array (#songs) রাখে। বাইরের কোডকে array-টি দিতে চান না (কেউ বদলে ফেলবে), কিন্তু চান for (const song of playlist) কাজ করুক। সমাধান: [Symbol.iterator]() { return this.#songs.values(); } — array-এর নিজের iterator দিয়ে দেওয়া, array নয়। ছোট, নিরাপদ, আর সাধারণ।
ঐচ্ছিক return() method: একটি iterator চাইলে next()-এর পাশে return() method-ও রাখতে পারে। লুপ মাঝপথে থামলে (break, return, বা error) for…of এটি ডেকে জানায়, “আর লাগবে না” — ফাইল বা সংযোগ বন্ধ করার সুযোগ। হাতে লেখা iterator-এ এটি প্রায়ই ভুলে যাওয়া হয়; generator-এ এটি নিজে থেকেই আসে (অংশ ৭)।
৪ · generator — থেমে থেমে চলা function
সাধারণ function একবার ডাকলে শুরু থেকে শেষ পর্যন্ত একটানা চলে, একটি মান ফেরত দেয়, আর তার সব local variable হারিয়ে যায়। পর্ব ০৫-এ দেখেছেন কেন: function-এর stack frame call stack-এ থাকে, আর function শেষ হলেই frame সরে যায়।
এখন কল্পনা করুন এমন একটি function, যে মাঝপথে থেমে একটি মান দিতে পারে, তারপর কেউ চাইলে ঠিক সেখান থেকে আবার চলতে পারে — সব local variable আগের মতো রেখে। এটাই generator. লেখার নিয়ম: function-এর পরে একটি তারকা — function* — আর ভেতরে যেখানে থামতে চান সেখানে yield.
ক্রিকেট ম্যাচে বৃষ্টি নামলে খেলা থামে, কিন্তু স্কোরবোর্ড মুছে ফেলা হয় না — রান, উইকেট, কোন বোলারের কত ওভার, সব লেখা থাকে। বৃষ্টি থামলে ঠিক সেই বল থেকে খেলা আবার শুরু হয়। Generator-এর yield হলো এই বৃষ্টি-বিরতি, আর next() হলো আম্পায়ারের “খেলা শুরু”।
তিনটি জিনিস প্রথমবার দেখলে অবাক লাগে। এক, generator function ডাকলে body-র একটি লাইনও চলে না — শুধু একটি generator object ফেরত আসে। দুই, এই object একই সাথে iterator (এর next() আছে) আর iterable (এর [Symbol.iterator]() নিজেকেই ফেরত দেয়)। তিন, body চলে শুধু next() ডাকলে, আর পরের yield-এ পৌঁছে থেমে যায়। কোন next()-এ কোন লাইন চলে, ধাপে ধাপে দেখুন:
1function* steps() {2 console.log('শুরু');3 yield 1;4 console.log('মাঝখান');5 yield 2;6 console.log('শেষ');7 return 'ok';8}9const g = steps();10g.next(); // চারবার ডাকা হবে
| অবস্থা | |
| next() দিল | |
| frame কোথায় |
steps()ডাকা। কোনো লাইন চলেনি, কিছুই ছাপা হয়নি! শুধু একটি generator object তৈরি হলো, যার ভেতরে function-এর একটি প্রস্তুত frame — শুরুর লাইনে আঙুল রেখে অপেক্ষা করছে।- প্রথম
g.next(). Body শুরু থেকে চলে: “শুরু” ছাপা হয়, তারপরyield 1. এখানে function থামে, আরnext()ফেরত দেয়{ value: 1, done: false }. Frame call stack থেকে নেমে generator object-এর ভেতরে জমা থাকে। - দ্বিতীয়
g.next(). Frame আবার stack-এ ওঠে, আর ঠিকyield 1-এর পর থেকে চলে — লাইন ৪ আর ৫. “মাঝখান” ছাপা হয়, ফল{ value: 2, done: false }. লাইন ২ আর চলে না। - তৃতীয়
g.next(). “শেষ” ছাপা হয়, তারপরreturn 'ok'.return-এর মান আসেdone: true-এর সাথে:{ value: 'ok', done: true }. Generator এখন সম্পূর্ণ শেষ। - চতুর্থ
g.next(). কোনো লাইন চলে না — শেষ generator সবসময়{ value: undefined, done: true }দেয়।[...steps()]লিখলে ফল হতো[ 1, 2 ]—'ok'নেই, কারণ spread আরfor…ofdone: true-এর মান উপেক্ষা করে।
yield x মানে “x দাও আর এখানেই থামো”। return x মানে “x দাও আর চিরতরে শেষ করো” — আর for…of/spread সেই শেষ মান দেখে না। তাই লুপে যে মান চান, সব yield করুন; return রাখুন শুধু থামার জন্য।
ভেতরে কী ঘটে? সাধারণ function-এর frame call stack-এ থাকে, তাই function থামলে frame হারায়। Generator-এর frame থাকে heap-এ, generator object-এর সাথে বাঁধা। next() ডাকলে engine সেই frame সাময়িকভাবে stack-এ তুলে চালায়, yield-এ আবার নামিয়ে রাখে। এটি একটি coroutine-এর সরল রূপ — যে function নিজে থেমে নিয়ন্ত্রণ ফিরিয়ে দিতে পারে। তবে JavaScript-এর generator “stackless”: থামতে পারে শুধু নিজের body-তে, ভেতরের কোনো সাধারণ function-এর মধ্যে yield লেখা যায় না।
Generator দিয়ে আগের Range কত ছোট হয়, দেখুন। Method-এর নামের আগে * দিলে সেটি generator method:
class Range {
constructor(start, end, step = 1) { Object.assign(this, { start, end, step }); }
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i += this.step) yield i;
}
}
[...new Range(1, 10, 2)]; // [ 1, 3, 5, 7, 9 ]প্রায় এগারো লাইনের হাতে লেখা iterator এখন একটি লুপ। কাউন্টার i একটি সাধারণ local variable — generator থামলেও হারায় না। { value, done } object, শেষ হলে done: true, আর লুপ থামলে return() — সবই engine নিজে বানিয়ে দেয়। প্রতিবার for…of শুরু হলে [Symbol.iterator]() ডাকা হয়, তাই প্রতিবার একটি নতুন generator — Range বারবার ঘোরানো যায়। Generator arrow function হতে পারে না (*() => বলে কিছু নেই), আর new দিয়েও ডাকা যায় না।
৫ · অসীম generator, take আর yield*
Array-এর আকার সীমিত — মেমরিতে যতটুকু ধরে। কিন্তু generator একটি মান বানায় কেবল যখন কেউ চায়, তাই while (true) লুপ থাকলেও ভয় নেই: প্রতিটি yield-এ সে থামে, আর কেউ না চাইলে পরের মান কখনো হিসাবই হয় না। এই ধরনের অসীম উৎসের দুটি বাস্তব উদাহরণ:
- অর্ডার id: একটি দোকানের অ্যাপে প্রতিটি নতুন অর্ডারের id চাই —
ORD-0001,ORD-0002… কতগুলো লাগবে, কেউ জানে না। - Fibonacci ক্রম: ০, ১, ১, ২, ৩, ৫… গণিতে এর শেষ নেই।
অসীম উৎস থেকে নিরাপদে কয়েকটি মান নিতে একটি ছোট সহায়ক লাগে — take(source, n), যে nটি মান দিয়ে থেমে যায়। এটিও একটি generator. আর yield* দিয়ে একটি generator আরেকটি iterable-এর সব মান পরপর পার করে দিতে পারে। নিচের কোডে সব একসাথে, শেষ লাইনে ES2025-এর iterator helper-সহ:
function* orderIds() {
let n = 1;
while (true) yield "ORD-" + String(n++).padStart(4, "0");
}
function* fib() {
let [a, b] = [0, 1];
while (true) { yield a; [a, b] = [b, a + b]; }
}
function* take(source, n) {
if (n <= 0) return;
for (const x of source) { yield x; if (--n === 0) return; }
}
function* evens(source) {
for (const x of source) { console.log(" পরীক্ষা:", x); if (x % 2 === 0) yield x; }
}
function* job() {
try { yield 1; yield 2; yield 3; } finally { console.log("পরিষ্কার করা হলো"); }
}
const ids = orderIds();
console.log(ids.next().value, ids.next().value);
console.log([...take(fib(), 10)]);
console.log([...take(evens(fib()), 2)]);
const three = take(fib(), 3);
console.log([...three], [...three]);
for (const v of job()) { if (v === 2) break; console.log(v); }
console.log(fib().map((x) => x * 10).take(5).toArray());ORD-0001 ORD-0002 — প্রতিটি next() একটি করে নতুন id; n generator-এর ভেতরে বেঁচে থাকে। [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ] — অসীম fib() থেকে take ঠিক ১০টি নিয়ে থামল; দশম মান দেওয়ার পর return করায় for…of ভেতরের fib-কেও বন্ধ করে দেয়। তারপর চারটি “পরীক্ষা” লাইন আর [ 0, 2 ]: দুটি জোড় সংখ্যা পেতে উৎস থেকে মাত্র চারটি মান (০, ১, ১, ২) টানা হলো — এটাই laziness, অংশ ৬-এর বিষয়। [ 0, 1, 1 ] [] — একই generator দ্বিতীয়বার spread করলে খালি, কারণ generator একবারই ঘোরা যায়। 1 তারপর “পরিষ্কার করা হলো”: break-এ for…of generator-এর return() ডাকে, আর finally চলে। শেষ লাইন [ 0, 10, 10, 20, 30 ] iterator helper দিয়ে — Edge/Chrome 122+ আর Node 22+-এ চলে, Node 20-এ নয় (সেখানে fib().map is not a function).
yield* — কাজ অন্যকে বুঝিয়ে দেওয়া
yield* (তারকাসহ yield) একটি iterable নিয়ে তার প্রতিটি মান একে একে বাইরে পাঠায়, যেন সেগুলো এই generator নিজেই yield করছে। একটি Playlist যদি কয়েকটি অ্যালবামে ভাগ করা থাকে:
function* playAll(albums) {
for (const album of albums) {
yield `— ${album.name} —`;
yield* album.songs; // array-ও iterable, তাই চলে
}
}
const albums = [
{ name: "লোকগীতি", songs: ["ভাটিয়ালি-১", "ভাটিয়ালি-২"] },
{ name: "ব্যান্ড", songs: ["রক-১"] },
];
[...playAll(albums)];
// প্রত্যাশিত আউটপুট: [ '— লোকগীতি —', 'ভাটিয়ালি-১', 'ভাটিয়ালি-২', '— ব্যান্ড —', 'রক-১' ]yield* album.songs লেখা for (const s of album.songs) yield s;-এর সংক্ষিপ্ত রূপ — তবে একটু বেশি শক্তিশালী। ভেতরের iterable যদি generator হয়, তবে তার return-এর মান yield* expression-এর মান হয়ে আসে (const r = yield* inner();), আর বাইরে থেকে পাঠানো next(value), return(), throw() সরাসরি ভেতরের generator-এ পৌঁছে যায়। Tree-এর মতো নেস্টেড কাঠামো ঘোরাতে এটি খুব কাজের: function* walk(node) { yield node; for (const c of node.children) yield* walk(c); } — recursion আর generator একসাথে।
৬ · Lazy পাইপলাইন — generator বনাম array
পর্ব ১৬-এ array method দিয়ে পাইপলাইন বানিয়েছেন: orders.filter(…).map(…).slice(0, 2). এটি eager — filter পুরো array শেষ করে একটি নতুন array বানায়, তারপর map আরেকটি, তারপর slice দুটি রেখে বাকি ফেলে দেয়। ১০ লাখ অর্ডার থেকে প্রথম দুটি চাইলে ১০ লাখবার পরীক্ষা হয়।
Generator দিয়ে একই পাইপলাইন lazy: প্রতিটি ধাপ একটি generator, যে আগের ধাপ থেকে একটি মান টানে, কাজ করে, পরের ধাপে পাঠায়। শেষ ধাপ যখন যথেষ্ট পায়, পুরো শিকল থেমে যায়। এখানে ডেটা “ঠেলা” হয় না, “টানা” হয় — শেষ থেকে শুরুর দিকে। নিচে উৎস হলো ১, ২, ৩, ৪… (অসীম), ধাপগুলো: জোড় রাখো → বর্গ করো → প্রথম দুটি নাও।
1const nums = naturals(); // 1, 2, 3, …2const even = filter(nums, isEven);3const sq = map(even, (x) => x * x);4const out = take(sq, 2);5console.log([...out]);
| উৎস দিল | |
| filter | |
| map | |
| take-এর ফল | |
| উৎস থেকে টানা মোট |
- শিকল তৈরি। চারটি generator object তৈরি হলো, একটি আরেকটির ভেতরে। কিন্তু এখনো একটি সংখ্যাও হিসাব হয়নি — generator ডাকলে body চলে না, মনে আছে? অসীম উৎস থাকা সত্ত্বেও এখানে কোনো খরচ নেই।
- Spread প্রথম মান চায়।
takeচায়map-এর কাছে,mapচায়filter-এর কাছে,filterচায় উৎসের কাছে। উৎস দেয় ১.filterদেখে বিজোড়, বাদ দেয়, আর নিজেই আবার উৎসের কাছে চায় — ওপরের কেউ টেরও পায় না। - ২ আসে। জোড়, তাই
filteryield করে;mapবর্গ করে ৪ বানায়;take৪ পেয়ে বাইরে দেয়। একটি মান পুরো শিকল পার হলো, অন্য কোনো মান হিসাব হয়নি। - দ্বিতীয় মান চাওয়া। আবার পুরো শিকল ধরে অনুরোধ যায়। উৎস দেয় ৩ — বিজোড়, বাদ। কোনো মাঝের array নেই; প্রতিটি ধাপ এক সময়ে একটিমাত্র মান ধরে রাখে।
- ৪ আসে। পাশ, বর্গ ১৬,
take-এর ঝুলিতে দুটি হলো।takeএখনreturnকরে, আর ভেতরেরfor…ofশিকলের প্রতিটি generator-এরreturn()ডাকে — উৎস বন্ধ। - হিসাব। ফল
[ 4, 16 ], আর উৎস থেকে টানা হলো মাত্র ৪টি সংখ্যা। Array দিয়ে লিখলে আগে পুরো উৎস array বানাতে হতো — আর উৎস অসীম হলে প্রোগ্রাম কখনো শেষই হতো না।
বাস্তবে lazy পাইপলাইন কোথায় লাগে? পণ্যের তালিকা পাতায় পাতায়: একটি দোকানের ১০,০০০ পণ্য থেকে “স্টকে আছে আর দাম ৫০০ টাকার কম” এমন প্রথম ২০টি দেখাতে হবে — lazy হলে সাধারণত শুধু প্রথম কয়েকশো পণ্য পরীক্ষা হয়। বড় ফাইল লাইন ধরে পড়া: ৫ GB-র একটি log ফাইল পুরোটা মেমরিতে তুললে সার্ভার ভেঙে পড়বে; লাইন ধরে টেনে নিলে মেমরিতে থাকে এক সময়ে একটি লাইন। Node.js-এ readline module ঠিক এভাবে লাইন দেয় (পর্ব ৪৯-এ দেখবেন)।
ES2025-এর iterator helper — lazy পাইপলাইন ভাষার ভেতরে
পর্ব ১৬-এর ভাবনার প্রশ্নে দেখেছিলেন, এতদিন lazy পাইপলাইনের জন্য filter, map, take-এর মতো generator নিজে লিখতে হতো। ES2025 থেকে iterator helper এগুলো সরাসরি দেয়: built-in iterator আর সব generator object-এর ওপর .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), .toArray(), .some(), .every(), .find(), .forEach().
// Edge/Chrome 122+ আর Node 22+ — Node 20-এ নেই
const cheap = products.values() // array → iterator
.filter((p) => p.inStock && p.price < 500)
.map((p) => p.name)
.take(20)
.toArray(); // এখানে এসে তবেই কাজ শুরু
Iterator.from(myIterator).drop(10); // হাতে লেখা {next} object-কেও helper দেওয়াদেখতে array method-এর মতোই, কিন্তু প্রতিটি ধাপ একটি নতুন lazy iterator দেয়, array নয়। কাজ শুরু হয় শুধু শেষে toArray() বা for…of মান চাইলে, আর ২০টি পাওয়া মাত্র থামে। দুটি সূক্ষ্ম কথা। এক, helper আছে Iterator.prototype-এ, তাই অংশ ৩-এর মতো হাতে লেখা { next() {…} } object-এ এগুলো নেই — Iterator.from() দিয়ে মুড়ে নিতে হয়। দুই, iterator-এ sort নেই, থাকতেও পারে না: সাজাতে হলে আগে সব মান দেখতে হয়, যা laziness-এর বিপরীত। Node 20-এ চালাতে হলে নিজের ছোট generator (take, filter) লিখুন।
তাহলে কি সবসময় lazy? না। ছোট তালিকায় (কয়েকশো বা কয়েক হাজার) array method সাধারণত সমান বা বেশি দ্রুত, পড়তে সহজ, আর বারবার ব্যবহার করা যায়। প্রতিটি generator ধাপে প্রতিটি মানের জন্য বাড়তি function কল আর { value, done } object তৈরির খরচ আছে (engine অনেকটা optimize করে, তবু শূন্য নয়)। Lazy বেছে নিন যখন ডেটা বিশাল বা অসীম, যখন “প্রথম কয়েকটি পেলেই হবে”, অথবা যখন ডেটা ধীরে ধীরে আসে।
৭ · দুই দিকের কথা — next(value), return(), throw() আর finally
এতক্ষণ generator শুধু মান দিয়েছে। কিন্তু yield আসলে একটি expression — এরও একটি মান আছে। next(value)-এ যে মান পাঠান, থেমে থাকা yield expression সেই মান হয়ে যায়। অর্থাৎ generator বাইরে মান পাঠায়, বাইরে থেকে মান নেয়ও। একটি দোকানের ক্যাশ কাউন্টারের চলমান যোগফল দিয়ে দেখুন:
1function* runningTotal() {2 let sum = 0;3 while (true) {4 const x = yield sum;5 sum += x;6 }7}8const till = runningTotal();9till.next(); // শুরু করা10till.next(120);11till.next(80);12till.return();
| x | |
| sum | |
| next() দিল |
- তৈরি।
runningTotal()একটি generator দেয়, body এখনো চলেনি। - প্রথম
next()— “প্রাইম করা”। Body চলেsum = 0থেকেyield sumপর্যন্ত, আর ০ দিয়ে থামে। এখানে কোনো argument দিলেও তা হারিয়ে যেত — কারণ তখনো কোনোyieldমান নেওয়ার জন্য অপেক্ষা করছিল না। তাই দুই দিকের generator-এ প্রথমnext()সবসময় খালি। next(120). থেমে থাকাyield sumexpression-এর মান হয় ১২০, তাইx = 120.sumহয় ১২০, লুপ ঘুরে আবারyield sum— ফল ১২০.next(80). একইভাবেx = 80,sum = 200, ফল ২০০. লক্ষ করুন: একটিnext()কলে দুটি কাজ — আগেরyield-এর উত্তর দেওয়া, আর পরেরyield-এর মান নেওয়া।return(). Generator-কে বাইরে থেকে শেষ করা। থেমে থাকাyield-এর জায়গায় যেন একটিreturnঘটে; কোনোfinallyথাকলে সেটি চলে, তারপরdone: true. পরে আর যতnext(…)পাঠান, সবdone: true.
বাইরে থেকে নিয়ন্ত্রণের তিনটি method এক জায়গায়:
| Method | থেমে থাকা yield-এ কী ঘটে | কখন কাজে লাগে |
|---|---|---|
g.next(v) | yield expression-এর মান হয় v, body চলতে থাকে | মান পাঠানো, পরের মান চাওয়া |
g.return(v) | সেখানে যেন return v লেখা — finally চলে, তারপর { value: v, done: true } | আগেভাগে থামানো, পরিষ্কার করানো |
g.throw(err) | সেখানে যেন throw err — generator-এর try…catch ধরলে চলতে থাকে, না ধরলে error বাইরে আসে আর generator শেষ | ভেতরে ভুলের খবর পাঠানো (async/await-এর ভিত্তি) |
try…finally — কখনো পরিষ্কার না ভুলতে
অংশ ৫-এর কোডে দেখেছেন: for…of মাঝপথে break করলেও generator-এর finally চলেছে। কারণ লুপ আগেভাগে থামলে for…of নিজে থেকেই iterator-এর return() ডাকে। Destructuring-ও তাই করে — const [a] = gen() একটি মান নিয়ে বাকিটা বন্ধ করে দেয়। তাই একটি generator যদি কোনো সম্পদ খোলে (ফাইল, ডেটাবেস cursor, নেটওয়ার্ক সংযোগ), সেটি try { … } finally { বন্ধ করো }-এর ভেতরে রাখুন। ব্যবহারকারী যেভাবেই থামান — শেষ পর্যন্ত, break, বা error — পরিষ্কার হবেই।
একটি ব্যতিক্রম: কেউ যদি generator হাতে নিয়ে কয়েকবার next() ডেকে ফেলে রেখে দেয় — না শেষ পর্যন্ত, না return() — তবে finally কখনো চলবে না। Garbage collector generator object মুছে দেবে, কিন্তু finally চালাবে না। হাতে next() চালালে কাজ শেষে নিজেই g.return() ডাকুন।
৮ · async iterator আর async generator — অপেক্ষা করে আসা মান
এতক্ষণ প্রতিটি মান সাথে সাথে তৈরি হচ্ছিল। কিন্তু অনেক মান আসে সময় নিয়ে — সার্ভার থেকে পাতায় পাতায় পণ্যের তালিকা, নেটওয়ার্ক থেকে টুকরো টুকরো ডেটা, ফাইল থেকে লাইন। এসবের জন্য ES2018-এ এসেছে async iterator: চুক্তি প্রায় একই, শুধু method-এর নাম Symbol.asyncIterator, আর next() সরাসরি { value, done } না দিয়ে একটি Promise দেয়, যা পরে { value, done }-এ পূর্ণ হয়। এগুলো ঘোরার লুপ for await…of, আর সবচেয়ে সহজে বানানোর উপায় async generator — async function*, যার ভেতরে await আর yield দুটোই চলে।
উদাহরণ: একটি API একবারে ৩টি পণ্য দেয়, সাথে জানায় আরও আছে কি না। ব্যবহারকারীর কোডকে পাতার হিসাব জানাতে চাই না — সে শুধু একটির পর একটি পণ্য পাবে। নিচে আসল নেটওয়ার্কের বদলে একটি নকল async function (১০০ মিলিসেকেন্ড পরে উত্তর দেয়):
const PRODUCTS = Array.from({ length: 7 }, (_, i) => `পণ্য-${i + 1}`);
function fakeFetchPage(page) { // নকল API — পাতায় ৩টি করে
return new Promise((resolve) => setTimeout(() => resolve({
items: PRODUCTS.slice((page - 1) * 3, page * 3),
hasMore: page * 3 < PRODUCTS.length,
}), 100));
}
async function* allProducts() {
for (let page = 1; ; page++) {
const res = await fakeFetchPage(page); // এখানে অপেক্ষা
console.log(`পাতা ${page} এল`);
yield* res.items; // পাতার প্রতিটি পণ্য একে একে
if (!res.hasMore) return;
}
}
for await (const p of allProducts()) { // module বা async function-এর ভেতরে
console.log(p);
if (p === "পণ্য-4") break; // দরকারের বেশি পাতা আনা হয় না
}
// প্রত্যাশিত আউটপুট: পাতা 1 এল, পণ্য-1, পণ্য-2, পণ্য-3, পাতা 2 এল, পণ্য-4পাঠকের দিক থেকে কোডটি একটি সাধারণ লুপের মতো, অথচ ভেতরে প্রতিটি পাতার জন্য নেটওয়ার্কের অপেক্ষা হচ্ছে। Lazy-ও বটে: ৪ নম্বর পণ্যে break করায় তৃতীয় পাতা কখনো চাওয়াই হয়নি — সার্ভারের খরচ বাঁচল। break async generator-এর return() ডাকে, তাই finally থাকলে সেটিও চলত। Top-level for await চলে শুধু module-এ (পর্ব ৪৫); সাধারণ script-এ এটি একটি async function-এর ভেতরে রাখতে হয়। Promise আর await কীভাবে কাজ করে, তা পর্ব ৩৩ আর ৩৪-এ বিস্তারিত; আসল fetch দিয়ে পাতায় পাতায় API পড়া পর্ব ৩৫-এ।
generator থেকে async/await: ES2017-এর async/await আসার আগে, ২০১৩ সালের দিকে TJ Holowaychuk-এর co লাইব্রেরি একটি চতুর কৌশল জনপ্রিয় করে: generator থেকে একটি Promise yield করো; co সেটি পূর্ণ হওয়া পর্যন্ত অপেক্ষা করে ফলটি next(result) দিয়ে ভেতরে ফেরত পাঠায়, আর ভুল হলে throw(err). ফলে const user = yield getUser(id); — দেখতে synchronous, চলে asynchronous. Node.js-এর Koa framework-এর প্রথম সংস্করণের middleware পুরোটাই এই generator-এর ওপর লেখা ছিল; Koa 2 পরে async function-এ সরে যায়। আজকের await মূলত এই ধাঁচকেই ভাষার ভেতরে নিয়ে আসা — ভাবনার প্রশ্ন ৩-এ বিস্তারিত।
Python-এর generator প্রায় হুবহু একই — yield, yield from (JavaScript-এ yield*), send(v) (এখানে next(v)), close() (এখানে return()); পার্থক্য হলো Python শেষ জানায় StopIteration exception দিয়ে, JavaScript done: true দিয়ে (Python-এর iterator ও generator). Java-র Iterator (hasNext() আর next() দুটি আলাদা method) আর Iterable একই চুক্তি, আর Java-র Stream একই রকম lazy পাইপলাইন (Java-র Stream API). C++-এর iterator হলো container-এর ভেতরে একটি অবস্থান (begin()/end()), C++20-এর ranges/views lazy, আর C++23-এ coroutine-ভিত্তিক std::generator এসেছে (C++-এর iterator, ranges ও views).
- Iterable আর iterator-এর পার্থক্য কী? — Iterable-এর
[Symbol.iterator]()আছে, যা iterator দেয়; iterator-এরnext()আছে, যা{ value, done }দেয়। Iterable বারবার ঘোরা যায়, iterator একবার। - সাধারণ object-এ
for…ofচলে না কেন, আর কীভাবে চালাবেন? — এরSymbol.iteratorনেই;Object.entries(obj)দিয়ে ঘুরুন, অথবা নিজে[Symbol.iterator]যোগ করুন। - Generator function ডাকলে কী হয়? — Body চলে না; একটি generator object আসে, প্রতিটি
next()-এ body পরেরyieldপর্যন্ত চলে। function* g() { yield 1; return 2; }হলে[...g()]কী? —[ 1 ];return-এর মানdone: true-এর সাথে আসে, spread সেটি নেয় না।- Generator কোথায় কাজে লাগে? — Lazy/অসীম ক্রম, বড় ডেটা টুকরো টুকরো পড়া, custom iterable সহজে বানানো, আর ঐতিহাসিকভাবে async/await-এর ভিত্তি।
৯ · AI-এর যুগে — AI-এর লেখা iterator আর generator কোড যাচাই
AI-কে “একটি বড় CSV ফাইল থেকে প্রথম ১০টি ভুল লাইন খুঁজে দাও” বললে প্রায়ই পাবেন: পুরো ফাইল একবারে পড়ো, split("\n") দিয়ে array বানাও, তারপর filter আর slice(0, 10). ছোট ফাইলে চলে, ২ GB ফাইলে সার্ভার মেমরি শেষ। Generator-এর বিষয়ে AI-এর সাধারণ ভুলগুলো:
| AI-এর ভুল | কেন সমস্যা | কী চাইবেন |
|---|---|---|
বিশাল বা অসীম ডেটা আগে পুরো array বানানো ([...gen], Array.from) | মেমরি শেষ, অসীম হলে প্রোগ্রাম কখনো থামে না | generator পাইপলাইন বা iterator helper, শেষে take |
| একই generator দুবার ঘোরানো (একবার গুনতে, একবার দেখাতে) | দ্বিতীয়বার নীরবে খালি — কোনো error নেই | প্রতিবার নতুন generator, বা iterable class |
Array, string বা Set-এ for…in | index string হিসেবে আসে, Set-এ কিছুই আসে না | for…of |
হাতে লেখা iterator, return() নেই, কোনো সম্পদ খোলা | break করলে ফাইল বা সংযোগ খোলা থেকে যায় | generator + try…finally |
Node 20-এ iterator.map() বা .take() | TypeError — method নেই | লক্ষ্য runtime বলে দেওয়া; নিজের ছোট generator |
পরীক্ষায় শুধু for…of চালানো, return-এর মান আশা করা | for…of শেষ মান উপেক্ষা করে | যে মান লাগবে সেটি yield করা |
AI-কে lazy কোড লেখাতে চাইলে শর্তগুলো আগেই বলে দিন:
AI-এর লেখা iterator/generator কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Iterator আর generator শুনতে আধুনিক মনে হয়, কিন্তু এদের শিকড় ১৯৭০-এর দশকের CLU আর Icon ভাষায়। সেই ভাষাগুলো কোন সমস্যার সমাধান খুঁজছিল?
১৯৭০-এর দশকের শুরুতে প্রোগ্রামিং গবেষণার বড় প্রশ্ন ছিল data abstraction — একটি ডেটা কাঠামোর ভেতরের গঠন লুকিয়ে শুধু তার কাজগুলো বাইরে দেখানো। MIT-তে Barbara Liskov আর তাঁর দল ১৯৭৪–৭৫ সালের দিকে CLU ভাষা তৈরি করেন ঠিক এই ধারণা পরীক্ষা করতে। কিন্তু একটি সমস্যা থেকে গেল: একটি set বা tree-এর ভেতরের গঠন লুকালে বাইরের কোড তার সব উপাদান ঘুরবে কীভাবে? ভেতরের array বা pointer দিয়ে দিলে abstraction ভেঙে যায়; সব উপাদান একটি নতুন তালিকায় কপি করে দিলে সময় আর মেমরি নষ্ট।
CLU-এর সমাধান ছিল iterator — একটি বিশেষ ধরনের procedure, যা yield দিয়ে একটি একটি করে উপাদান দেয়, আর প্রতিটি উপাদানের জন্য ডাকা লুপের body চলে, তারপর iterator ঠিক যেখানে থেমেছিল সেখান থেকে চলে। অর্থাৎ yield শব্দটি আর “থেমে আবার চলা”র ধারণা দুটোই প্রায় পঞ্চাশ বছর পুরোনো। Liskov পরে এই data abstraction-এর কাজের জন্য (আরও অনেক অবদানের সাথে) ২০০৮ সালে Turing Award পান।
প্রায় একই সময়ে University of Arizona-তে Ralph Griswold Icon ভাষা তৈরি করেন (১৯৭৭ সালের দিকে), যা string প্রক্রিয়াকরণের জন্য বিখ্যাত SNOBOL-এর উত্তরসূরি। Icon-এ প্রায় যেকোনো expression একটি generator হতে পারত — একাধিক ফল দিতে পারত, আর “goal-directed evaluation” নামের নিয়মে প্রয়োজন হলে পরের ফল চাওয়া হতো। যেমন একটি string-এ একটি শব্দ কোথায় কোথায় আছে, তা একটি expression থেকে একে একে বেরিয়ে আসত।
CLU-এর iterator সীমিত ছিল — শুধু for লুপের ভেতরে ব্যবহার করা যেত, হাতে next() ডাকার উপায় ছিল না। পরের প্রজন্মের ভাষাগুলো (Python, C#, তারপর JavaScript) এটিকে একটি object বানায় যা যেকোনো জায়গায় রাখা যায়, পাঠানো যায়, হাতে চালানো যায়। Design pattern-এর বিখ্যাত বই (১৯৯৪) “Iterator pattern”-কে আলাদা নাম দেয় — যেকোনো সংগ্রহ ঘোরার একটি সাধারণ interface.
মূল উপলব্ধি: Iterator জন্মেছিল একটি নকশার সমস্যা থেকে — ভেতর না দেখিয়ে সব উপাদান দেওয়া। JavaScript-এর Symbol.iterator ঠিক এই কাজই করে: Map বা আপনার Playlist নিজের ভেতরের গঠন লুকিয়ে রাখে, তবু for…of সবার ওপর একইভাবে চলে।
প্র ০২JavaScript-এর generator দেখতে প্রায় Python-এর মতো। এটি কাকতালীয় নয় — Python-এর PEP 255 থেকে ES2015 পর্যন্ত কীভাবে এই ধারণা এল?
২০০১ সালে Python-এর PEP 255 “Simple Generators” প্রস্তাব করা হয় (লেখক Neil Schemenauer, Tim Peters আর Magnus Lie Hetland), আর এটি Python 2.2-এ যুক্ত হয়। প্রস্তাবের যুক্তি ছিল ঠিক অংশ ৩-এর সমস্যা: হাতে iterator লিখতে গেলে অবস্থা (কাউন্টার, কোথায় আছি) object-এর property-তে রাখতে হয়, আর কোড একটি state machine-এ পরিণত হয় যা পড়া কঠিন। Generator-এ অবস্থা থাকে সাধারণ local variable আর “কোড কোন লাইনে থেমেছে” — তাতেই। প্রস্তাবে সরাসরি CLU আর Icon-এর কথা উল্লেখ ছিল। পরে PEP 342 (২০০৫, Python 2.5) send(), throw(), close() যোগ করে generator-কে দুই দিকের coroutine বানায়, আর PEP 380 (Python 3.3) আনে yield from.
JavaScript-এ এর প্রথম রূপ আসে Mozilla-র হাত ধরে। ২০০৬ সালে Firefox 2-এর সাথে আসা “JavaScript 1.7”-এ Brendan Eich-এর দল Python থেকে সরাসরি অনুপ্রাণিত হয়ে generator আর iterator যোগ করে — তখন সাধারণ function-এর ভেতরে yield লিখলেই সেটি generator হয়ে যেত। এটি শুধু Firefox-এ চলত, মানদণ্ডের অংশ ছিল না।
ES4 নামের উচ্চাভিলাষী সংস্করণ ২০০৮ সালে বাতিল হলেও generator-এর ধারণা টিকে যায়, আর ES2015 (ES6)-এ মানদণ্ডে আসে — তবে কয়েকটি গুরুত্বপূর্ণ বদলসহ। function*-এর তারকা যোগ হয়, যাতে শুধু body-তে একটি yield দেখে function-এর আচরণ পুরো বদলে না যায় (কোড পড়ে বোঝা যায় এটি generator), আর পুরোনো কোডে yield নামের variable থাকলেও ভাঙে না। শেষ জানানো হয় exception দিয়ে নয়, { value, done } object দিয়ে — Python-এর StopIteration অন্য ভুলের সাথে গুলিয়ে যাওয়ার সমস্যা (যা পরে PEP 479 দিয়ে Python-কে ঠিক করতে হয়েছিল) এভাবে এড়ানো যায়। পুরোনো Mozilla-র generator পরে Firefox থেকে সরিয়ে ফেলা হয়।
মূল উপলব্ধি: ভাষাগুলো একে অন্যের কাছ থেকে শেখে — আর ভালো ধার নেওয়া মানে হুবহু কপি নয়। JavaScript Python-এর ধারণা নিয়েছে, কিন্তু তারকা চিহ্ন আর done flag দিয়ে নিজের দুটি পুরোনো সমস্যা (পেছনের সামঞ্জস্য আর exception-এর অপব্যবহার) সমাধান করেছে।
প্র ০৩async/await আসার আগেই মানুষ generator দিয়ে “synchronous দেখতে asynchronous কোড” লিখত। generator ঠিক কোন ক্ষমতার জন্য এটি সম্ভব হলো, আর Babel কীভাবে পুরোনো ব্রাউজারে generator চালাত?
পর্ব ৩২-এ দেখবেন callback hell — একটির ভেতরে আরেকটি callback, ভুল সামলানো প্রতিটি স্তরে। Promise (পর্ব ৩৩) এটিকে শিকলে বদলাল, কিন্তু .then().then() তখনো সাধারণ কোডের মতো পড়া যায় না। মানুষ চাইছিল এমন কোড: const user = getUser(id); const orders = getOrders(user); — যেন অপেক্ষাটা অদৃশ্য।
Generator-এর তিনটি ক্ষমতা মিলে এটি সম্ভব করে: থামা (yield), ভেতরে মান পাঠানো (next(value)), আর ভেতরে error পাঠানো (throw(err)). ধরুন generator yield getUser(id) লিখে একটি Promise বাইরে দিল আর থামল। বাইরের একটি “runner” Promise পূর্ণ হওয়া পর্যন্ত অপেক্ষা করে, তারপর gen.next(user) ডাকে — yield expression-এর মান হয়ে যায় user. Promise ব্যর্থ হলে gen.throw(err), আর generator-এর ভেতরের সাধারণ try…catch সেটি ধরে। এই runner-ই ছিল ২০১৩ সালের দিকে TJ Holowaychuk-এর co লাইব্রেরি, আর Koa framework-এর প্রথম সংস্করণ এর ওপর দাঁড়িয়ে ছিল। তাত্ত্বিকভাবে এটি coroutine-এর ধারণা — এমন routine যা নিজে থেমে নিয়ন্ত্রণ অন্যকে দেয়, শব্দটি ১৯৫০-এর দশকের শেষে Melvin Conway-এর কাজ থেকে এসেছে।
ES2017-এর async/await মূলত এই ধাঁচের ভাষা-স্তরের রূপ: async function হলো একটি generator আর runner একসাথে, await হলো promise-কে yield করা। C# ২০১২ সালে (C# 5) async/await এনে দেখিয়েছিল ধারণাটি কতটা জনপ্রিয় হতে পারে; JavaScript-এর নকশায় সেই অভিজ্ঞতা আর co-এর অভিজ্ঞতা দুটোই কাজে লাগে।
পুরোনো ব্রাউজারে (যেমন Internet Explorer) generator ছিল না। Facebook-এর regenerator (২০১৩) প্রকল্প একটি generator function-কে সাধারণ ES5 কোডে রূপান্তর করত: function-টিকে একটি বড় switch-ভিত্তিক state machine-এ ভাঙা, যেখানে প্রতিটি yield একটি case, আর local variable-গুলো একটি context object-এ তোলা। Babel বহু বছর ধরে generator আর async function — দুটোকেই এভাবে রূপান্তর করত। অর্থাৎ অংশ ৩-এর হাতে লেখা iterator-এর মতো কোডই একটি transpiler স্বয়ংক্রিয়ভাবে লিখে দিত।
মূল উপলব্ধি: একটি সাধারণ, শক্তিশালী মৌলিক ধারণা (থামা আর আবার চলা) থেকে পরে বিশেষ সুবিধা (async/await) গড়ে ওঠে। Generator বুঝলে await-কে আর জাদু মনে হয় না — এটি একটি থেমে থাকা function, যাকে Promise পূর্ণ হলে আবার জাগানো হয়।
প্র ০৪Haskell-এর মতো ভাষায় সবকিছুই lazy — অসীম তালিকা সেখানে স্বাভাবিক জিনিস। JavaScript কেন eager ভাষা, আর laziness-এর দাম কী?
Haskell (প্রথম সংস্করণ ১৯৯০) একটি “non-strict” ভাষা: একটি expression-এর মান হিসাব হয় শুধু যখন সত্যিই দরকার পড়ে। সেখানে take 5 [1..] লেখা সাধারণ — [1..] একটি অসীম তালিকা, কিন্তু যতটুকু চাওয়া হয় ততটুকুই তৈরি হয়। ধারণাটির শিকড় ১৯৭০-এর দশকের গবেষণায়, আর বিখ্যাত পাঠ্যবই SICP (১৯৮৫) “stream” নামে একই জিনিস Scheme-এ শেখায় — একটি তালিকা যার বাকি অংশ একটি অপেক্ষমাণ হিসাব।
Laziness-এর সুবিধা স্পষ্ট: অসীম কাঠামো, অপ্রয়োজনীয় কাজ বাদ, আর “উৎপাদক” আর “ভোক্তা”-কে আলাদা করে লেখা — fib() জানে না কেউ কতগুলো চাইবে, take জানে না মান কোথা থেকে আসছে। এই আলাদা করার ক্ষমতা নিয়ে John Hughes-এর বিখ্যাত প্রবন্ধ “Why Functional Programming Matters” (১৯৮৯) laziness-কে মডুলার কোডের একটি মূল হাতিয়ার বলে।
কিন্তু দামও আছে। এক, কখন কী চলবে বোঝা কঠিন — একটি হিসাব অনেক পরে, অন্য জায়গায় চলতে পারে। JavaScript-এ side effect সর্বত্র (DOM বদলানো, console.log, নেটওয়ার্ক); সব lazy হলে কোন লাইনের পর কী ঘটে বলা কঠিন হতো। অংশ ৫-এর “পরীক্ষা” লাইনগুলো এর ছোট উদাহরণ — সেগুলো চলেছে লুপ বানানোর সময় নয়, মান চাওয়ার সময়। দুই, মেমরির অপ্রত্যাশিত ব্যবহার — Haskell-এ অসমাপ্ত হিসাবের স্তূপ (“thunk”) জমে মেমরি ফুরিয়ে যাওয়া একটি পরিচিত সমস্যা (“space leak”). তিন, প্রতিটি মানের জন্য বাড়তি সামলানোর খরচ।
JavaScript তাই মাঝামাঝি পথ নিয়েছে: ভাষা eager, কিন্তু যেখানে laziness দরকার সেখানে আপনি নিজে বেছে নিতে পারেন — generator, iterator helper, async iterator. Python, C# (LINQ), Java (Stream), Rust (iterator) — প্রায় সব জনপ্রিয় ভাষা এই “ডিফল্ট eager, ইচ্ছা হলে lazy” পথে চলে।
মূল উপলব্ধি: Laziness একটি শক্তিশালী হাতিয়ার, কিন্তু ভাষার ডিফল্ট হিসেবে নয় — অন্তত যে ভাষায় side effect প্রচুর। স্পষ্টভাবে বেছে নেওয়া laziness সুবিধা দেয়, আর কোথায় সেটি ঘটছে তা কোড দেখেই বোঝা যায়।
প্র ০৫ES2015-এ iterator এলেও এর ওপর map/filter-এর মতো method আসতে প্রায় দশ বছর লাগল। ES2025-এর iterator helper কী বদলাল, আর নকশায় কোন কঠিন প্রশ্নগুলো ছিল?
২০১৫ থেকে JavaScript-এ iterator ছিল, কিন্তু তার ওপর কাজ করার কোনো সরঞ্জাম ছিল না। একটি generator থেকে প্রথম পাঁচটি জোড় সংখ্যা চাইলে হয় নিজে filter/take generator লিখতে হতো (এই পর্বে যা করলাম), নয়তো [...gen] দিয়ে array বানাতে হতো — যা অসীম generator-এ অসম্ভব। ফলে বেশিরভাগ মানুষ generator এড়িয়ে array-তেই থাকত, আর lazy পাইপলাইন রয়ে গেল ছোট ছোট npm লাইব্রেরির জগতে।
Iterator helper-এর প্রস্তাব TC39-এ আসে ২০১৮–১৯ সালের দিকে। প্রথম কঠিন প্রশ্ন: method কোথায় বসবে? সব built-in iterator (array iterator, Map iterator, generator object) একটি সাধারণ prototype থেকে আসে, যাকে মানদণ্ডে বলা হতো %IteratorPrototype% — কিন্তু এর কোনো প্রকাশ্য নাম ছিল না। প্রস্তাবটি একটি global Iterator constructor প্রকাশ করে, যাতে Iterator.prototype.map ইত্যাদি সবাই পায়। এতে ওয়েবের পুরোনো সমস্যা দেখা দেয়: নতুন global নাম বা prototype-এর property চালু হলে আগে থেকে চলা কিছু কোডের সাথে সংঘর্ষ হতে পারে। বাস্তবায়নের সময় এমন সংঘর্ষ ধরা পড়ায় কয়েকটি property-র আচরণ বিশেষভাবে সংজ্ঞায়িত করতে হয়েছে, যাতে পুরোনো সাইট না ভাঙে।
দ্বিতীয় প্রশ্ন: হাতে লেখা iterator-এর কী হবে? অংশ ৩-এর মতো একটি সাধারণ { next() } object এই prototype থেকে আসে না। তাই Iterator.from() যোগ হয়, যা যেকোনো iterator বা iterable-কে helper-যুক্ত iterator-এ মুড়ে দেয়। তৃতীয় প্রশ্ন: কোনগুলো রাখা হবে না? sort, reverse, length বাদ — কারণ এগুলোর জন্য পুরো ক্রম আগে দেখতে হয়। আর async iterator-এর helper আলাদা প্রস্তাবে রাখা হয়েছে, কারণ সেখানে একসাথে কতগুলো মান অপেক্ষায় রাখা যাবে (concurrency) — এই প্রশ্ন আরও জটিল।
প্রস্তাবটি ২০২৪ সালে চূড়ান্ত হয়ে ES2025-এ আসে; Chrome/Edge 122 আর Node 22 থেকে এটি চলে, Node 20-এ নয়। ফলে এখন arr.values().filter(f).map(g).take(5).toArray() — array method-এর পরিচিত চেহারায়, কিন্তু lazy. Java-র Stream (২০১৪) বা C#-এর LINQ (২০০৭) এই সুবিধা অনেক আগেই দিয়েছিল; JavaScript দেরিতে এলেও generator-এর সাথে মিলে এখন একটি পূর্ণ ছবি দেয়।
মূল উপলব্ধি: একটি চুক্তি (iteration protocol) থাকা আর তার চারপাশে ব্যবহারযোগ্য সরঞ্জাম থাকা দুটি আলাদা জিনিস। সরঞ্জাম না থাকলে মানুষ ভালো ধারণাও এড়িয়ে চলে — আর ওয়েবে প্রতিটি নতুন global নামের পেছনে পুরোনো সাইট না ভাঙার সতর্ক হিসাব থাকে।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — আগে অনুমান করুন, তারপর console-এ চালিয়ে মিলিয়ে নিন।
-
আউটপুট অনুমান করুন:
function* g() { try { yield 1; yield 2; } finally { console.log("বন্ধ"); } }তারপরconst [a] = g(); console.log(a);— কী কী ছাপা হবে, কোন ক্রমে?প্রথমে
বন্ধ, তারপর1. Destructuring একটি মান নিল (a = 1), তারপর আর লাগবে না বলে iterator-এরreturn()ডাকল — generatoryield 1-এ থেমে ছিল, সেখানে যেনreturnঘটল, তাইfinallyচলল। এসব destructuring লাইনের ভেতরেই ঘটে, তাইconsole.log(a)-এর আগে।yield 2কখনো চলেনি। -
Iterable, iterator, নাকি কোনোটিই নয়? (ক)
[1, 2](খ)[1, 2].values()(গ)function* f() {}-এরf(ঘ)f()(ঙ){ a: 1 }(চ)"আম"(ক) iterable — iterator নয়, এর
nextনেই। (খ) iterator, এবং iterable-ও (built-in iterator-এর[Symbol.iterator]()নিজেকে দেয়)। (গ) কোনোটিই নয় — এটি একটি function, যা ডাকলে iterator দেয়। (ঘ) generator object — iterator আর iterable দুটোই, তবে একবারই ঘোরা যায়। (ঙ) কোনোটিই নয় —for…of-এ TypeError. (চ) iterable — code point ধরে আ, ম. -
বাগ খুঁজুন: একজন লিখেছেন
const rows = readRows(file);(readRowsএকটি generator), তারপরconsole.log("মোট", [...rows].length);আরfor (const r of rows) save(r);— একটি সারিও সেভ হচ্ছে না, কোনো error-ও নেই। আরেকজনfor (const i in "ঢাকা")লিখে অক্ষর আশা করছেন। দুটিতে কী ঘটছে?প্রথমটিতে spread generator-টিকে শেষ পর্যন্ত ঘুরে ফেলেছে; দ্বিতীয় লুপ একটি শেষ হওয়া generator পায়, যার প্রথম
next()-ইdone: true— লুপ একবারও চলে না। সমাধান: একবারই ঘুরুন আর ঘোরার সময় গুনুন (let n = 0; for (const r of rows) { save(r); n++; }), অথবা দুই জায়গার জন্যreadRows(file)দুবার ডাকুন। দ্বিতীয়টিতেfor…inঅক্ষর নয়, index দেয় — string হিসেবে"0","1","2","3". অক্ষর চাইলেfor (const ch of "ঢাকা"). -
ছোট কোড লিখুন: একটি SMS সেবা একবারে সর্বোচ্চ ১০০টি নম্বরে বার্তা পাঠাতে দেয়।
chunk(iterable, size)নামে একটি generator লিখুন, যা যেকোনো iterable থেকেsizeআকারের array একে একে দেয়; শেষ টুকরো ছোট হতে পারে।[...chunk([1, 2, 3, 4, 5], 2)]দেবে[ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]. কোড Node 20-এও চলতে হবে।function* chunk(iterable, size) { let batch = []; for (const x of iterable) { batch.push(x); if (batch.length === size) { yield batch; batch = []; } } if (batch.length > 0) yield batch; }. প্রতিবার পূর্ণ টুকরো yield করার পর একটি নতুন array শুরু করতে হয় —batch.length = 0লিখলে আগে দেওয়া টুকরোটিও খালি হয়ে যেত, কারণ সেটি একই array. লুপ শেষে বাকি থাকলে শেষ ছোট টুকরো। এটি lazy, তাই একটি অসীম উৎস বা ১০ লাখ নম্বরের generator-এর ওপরও চলে — এক সময়ে মেমরিতে থাকে মাত্র ১০০টি নম্বর।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJavaScript-এ for (const x of thing) চলার সময় ভেতরে ধাপগুলো কোন ক্রমে ঘটে, সাজান (▲▼ চেপে বা টেনে)।
- 🔑
thing[Symbol.iterator]()ডেকে একটি iterator নেওয়া হয় - 👉 iterator-এর
next()ডাকা হয় - 🔍 ফলের
doneদেখা হয় - ▶️
done: falseহলেvalueযায়x-এ, লুপের body চলে, আবারnext() - 🏁
done: trueএলে লুপ শেষ — সেই ফলেরvalueউপেক্ষিত
JavaScript-এ const it = [10][Symbol.iterator](); it.next(); এর পরে আবার it.next() ডাকলে কী ফেরত আসে?
- { value: 10, done: false }
- { value: 10, done: true }
- { value: undefined, done: true }
- TypeError
function* g() { console.log("A"); yield 1; } লেখার পর const x = g(); চালালে ঠিক তখন console-এ কী ছাপা হয়?
- কিছুই না
- A
- A আর 1
- TypeError: g is not a constructor
JavaScript-এ function* g() { yield 1; yield 2; return 3; } হলে [...g()] কী দেয়?
- [ 1, 2, 3 ]
- [ 1, 2 ]
- [ 3 ]
- []
const gen = (function* () { yield "ক"; yield "খ"; })(); এর পরে [...gen] একবার চালিয়ে, দ্বিতীয়বার [...gen] চালালে কী পাওয়া যায়?
- [ 'ক', 'খ' ]
- TypeError: gen is not iterable
- [ 'খ' ]
- []
JavaScript-এ নিচের কোনটি নিজে থেকে iterable নয়, অর্থাৎ for…of-এ দিলে TypeError দেয়?
- "বাংলাদেশ"
- new Map([["a", 1]])
- { a: 1, b: 2 }
- একটি সাধারণ function-এর arguments
function* t() { let s = 0; while (true) { s += yield s; } } আর const c = t(); c.next(); c.next(5); — এরপর c.next(7).value কত?
- 7
- 12
- 5
- 0
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখতে পাবেন।
মনে রাখুন
- Iteration protocol:
[Symbol.iterator]()→ iterator →next()→{ value, done }. এটি মানলেইfor…of, spread, destructuring,Array.fromচলে। সাধারণ object iterable নয়। - Iterable বারবার, iterator একবার। Generator object iterator, তাই একবারই ঘোরা যায় — দ্বিতীয়বার নীরবে খালি।
function*ডাকলে body চলে না; প্রতিটিnext()-এ পরেরyieldপর্যন্ত চলে থামে, local variable সহ।return-এর মানfor…ofদেখে না।- Generator lazy — অসীম ক্রম আর বড় ডেটার পাইপলাইন সম্ভব; সাথে
takeবাbreakরাখুন। ES2025-এর iterator helper একই কাজ সহজ করে, কিন্তু Node 20-এ নেই। next(v)ভেতরে মান পাঠায় (প্রথমটি বাদে),return()/breakfinallyচালায়,throw()ভেতরে error পাঠায়;async function*+for await…ofঅপেক্ষা করে আসা মানের জন্য।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৮ · Symbol, Proxy আর Reflectপরবর্তী পর্বWell-known Symbol, আর object-এর আচরণ মাঝখানে বদলানোর কৌশল।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Python-এর iterator ও generatorতুলনা করুনএকই ধারণা — StopIteration, send, yield from আর মেমরির হিসাব।
- Java-র Stream APIপাশের ট্র্যাকLazy পাইপলাইন অন্য ভাষায় — intermediate আর terminal operation.
- C++-এর iteratorগভীরেContainer-এর ভেতরে অবস্থান হিসেবে iterator, আর iterator category.