অল্প সল্প ইটারেটর: ফর লুপের অন্তঃকথন

JavaScript Iterator

জাভাস্ক্রিপ্টের for…of লুপের সাথে পরিচয় আছে নিশ্চয়ই। একটা অ্যারে এর সব ইলিমেন্টে সিকোয়েন্সিয়ালি অ্যাকসেস করতে বা ইটারেট করতে আমরা for…of লুপ ব্যাবহার করে থাকি। কখনও কি জানতে ইচ্ছে করেছে এই লুপটা কীভাবে কাজ করে? একটা অ্যারেতে তো অ্যারের ভ্যালুগুলো ছাড়াও length, concat, fill ইত্যাদি key থাকে? তাহলে for…of লুপ কীভাবে জানে ঠিক কোন কোন ভ্যালুতে অ্যাকসেস করতে হবে? ঠিক কোথায় গিয়ে থামতে হবে? এই প্রশ্নগুলোর উত্তর আমাদের দেবে ইটারেটর?

ইটারেটর কী? সহজ বাংলায় ইটারেটর হচ্ছে এমন একটা অবজেক্ট যেটা ফর লুপকে বলতে পারে — প্রথমে এই ভ্যালুতে অ্যাকসেস করো। তারপরে আছে এই ভ্যালু। তারপরে আছে এই ভ্যালু। আচ্ছা, সব ভ্যালুতে অ্যাকসেস করা শেষ। এবার থামো। এই কথাবার্তা চালানোর জন্য প্রতিটা ইটারেটরের next() নামে একটা ফাংশন থাকে। for…of লুপ বারবার এই ফাংশনে কল করে জিজ্ঞেস করতে থাকে — ভাই আরও কি ভ্যালু আছে? না’কি আমি থামব?

প্রতিটা অ্যারের সাথেই একটা করে ইটারেটর অ্যাসোসিয়েটেড থাকে। for…of লুপ সেই ইটারেটর অবজেক্টের সান্নিধ্য লাভ করে Symbol.iterator নামের একটা স্পেশাল ফাংশন দিয়ে। যাদের ES6 এর Symbol টাইপ নিয়ে ধারণা নেই, তাদের কাছে একটা ফাংশনের নাম হিসেবে Symbol.iterator কে খানিকটা বদখত লাগতে পারে। তবে, ইটারেটর বুঝতে Symbolকে বোঝার দরকার নেই। শুধু ইটারেটরে অ্যাকসেস করার ফাংশনের নাম Symbol.iterator — এটা জানলেই চলবে।

অনেক বকবক হল। এবার একটু কোড দেখা যাক।

একটা for…of লুপ যখন যখন একটা অ্যারেকে ইটারেট করে, তখন ঠিক এই কাজগুলোই করে। প্রথমে অ্যারের Symbol.iterator ফাংশনটা কল করে সেই অ্যারের ইটারেটরটায় অ্যাকসেস করে। এখানে লক্ষ্য করার মত ব্যাপার হচ্ছে, আমরা কিন্তু a.Symbol.iterator লিখিনি বরং a[Symbol.iterator] লিখেছি। এর কারণ হচ্ছে, Symbol.iterator একটা গ্লোবাল ভ্যারিয়েবল যার ভ্যালু হচ্ছে ফাংশনটার নাম। জাভাস্ক্রিপ্টে এভাবে ভ্যারিয়েবলের মাধ্যমে কোন প্রোপার্টিতে অ্যাকসেস করা যায়। যেমন, আমরা জানি a.length লিখলে আমরা অ্যারেটার লেন্থ পাবো। তার বদলে আমরা লিখতে পারি const l = 'length'; a[l]; তাহলেও আমরা অ্যারের লেন্থ পেয়ে যাব।

যাই হোক! ইটারেটরটা পাবার পরে for…of লুপ বারবার সেই ইটারেটরের next() ফাংশন কল করতে থাকে। next() ফাংশন প্রতিবার একটা করে অবজেক্ট রিটার্ন করে। সেই অবজেক্টে দু’টো প্রোপার্টি থাকে — value আর done. নাম শুনেই বোঝা যাচ্ছে কার কী কাজ। value প্রোপার্টি দিয়ে ফর লুপ পরবর্তী ভ্যালুতে অ্যাকসেস করে। আর done প্রোপার্টি দিয়ে চেক করে এর পরেও আর কোন ভ্যালু আছে কি’না। যখন done এর ভ্যালু হিসেবে true পাওয়া যাবে, তার মানে লুপিং শেষ।

এখানে একটা জিনিস মনে রাখতে হবে — যখন done এর ভ্যালু true পাওয়া যাবে, সেবার থেকেই value প্রোপার্টিটা আর ইউজ করা হয় না। তাই, শেষ ভ্যালুটা পাঠানোর সময়েও done এর ভ্যালু হবে false. পরবর্তী অবজেক্টে doneএর ভ্যালু হবে true. এর পরে যতবারই next() কল করা হোক না কেন, done এর ভ্যালু trueই থাকবে।

আচ্ছা, ইটারেটর কী জিনিস সেটা তো জানা হল। কিন্তু, এটা জেনে আমাদের কী লাভ? যখন আমরা জেনে গিয়েছি, ইটারেটর কীভাবে কাজ করে, তখন আমরা নিজেরাই নিজেদের মত ইটারেটর তৈরি করতে পারি, যার ওপর for…of লুপ চালানো যাবে।

ইটারেটরের Hello World হচ্ছে একটা range ফাংশন। অর্থাৎ, ইটারেটর নিয়ে সবাই প্রথমেই যে কোডটা করে, সেটা হচ্ছে range নামে একটা ফাংশন ডিফাইন করে। এই ফাংশনের দু’টো প্যারামিটার থাকে — from আর to. আর range ফাংশন রিটার্ন করে একটা ইটারেটর যেটার মাধ্যমে from থেকে to এর আগ পর্যন্ত ইন্টিজারগুলোতে ইটারেট করা যায়। দেখা যাক, ইটারেটর দিয়ে আমরা কাজটা কীভাবে করতে পারি।

আমরা জেনেছি, for…of লুপ শুরুতেই যেটা করে সেটা হচ্ছে, Symbol.iterator নামের ফাংশনটাকে কল করে। আর তাই, range ফাংশন যেই অবজেক্টটা রিটার্ন করে, তাতে একটাই ফাংশন আছে Symbol.iterator নামে। সেই ফাংশনটা রিটার্ন করে একটা ইটারেটর। ইটারেটরটা নিজেও একটা অবজেক্ট। তার একটাই ফাংশন আছে next() নামে। সেই next() ফাংশন কীভাবে কাজ করছে, সেটা তো জলবৎ তরলং। তো for…of লুপ বারবার সেই next() ফাংশন কল করতে থাকে যতক্ষণ না next() ফাংশন এমন একটা অবজেক্ট রিটার্ন করে যেখানে done এর ভ্যালু true। সেটা পেয়ে গেলেই for…of লুপ থেমে যায়।

প্রশ্ন হতেই পারে ,আমরা তো চাইলেই for(let i = 5; i<10; i++) লিখে ঠিক একই কাজ করতে পারতাম। তো সোজাসুজি খাওয়ার বদলে আমরা কেন হাতটা মাথার পেছন দিয়ে ঘুরিয়ে এনে খেলাম?

একটা কারণ হচ্ছে, যখন আমরা একটা ইটারেটর তৈরি করি তার সাথেই for…of লুপ চালানো ছাড়াও আরও বেশ কিছু সুবিধা পেয়ে যাই। যদি জাভাস্ক্রিপ্টের স্প্রেড অপারেটর ( অপারেটর) সম্পর্কে ধারণা থেকে থাকে, তাহলেই একটা জোশ কাজ করা যাবে। প্রতিটা ইটারেটরের সাথেই স্প্রেড অপারেটর ব্যবহার করা যায়।

আমরা চাইলে খুব সহজেই একটা ইটারেটরকে অ্যারে বানিয়ে ফেলতে পারি। আমরা যদি লিখি const a = […range(3, 7)] তাহলে a এর ভ্যালু হবে [3, 4, 5, 6] অর্থাৎ range() ফাংশন যেই ইটারেটরটা রিটার্ন করে, সেটা একটা অ্যারেতে রূপান্তরিত হয়ে যাবে।

তাছাড়া, আমরা যদি যেকোন ধরণের ডেটা স্ট্রাকচার (যেমন লিংকড লিস্ট বা বাইনারি ট্রি) ডিজাইন করি, একটা কমন এক্সপেক্টেশন হচ্ছে আমরা তার ওপর লুপ চালাতে পারব। সেটা পারতে হলে অবশ্যই আমাদের সেই ডেটা স্ট্রাকচারকে ইটারেবল হতে হবে।

প্রাকটিক্যাল কেসে আমি ইটারেটরের সবচেয়ে বেশি ব্যাবহার দেখেছি মেমরি ইফিশিয়েন্ট ইটারেশনের জন্য। ধরা যাক, আমাদের দশটা ফাইল আছে। প্রতিটা ফাইলের সাইজ 100MB। আমাদের সবগুলো ফাইল প্রসেস করতে হবে। এখন আমরা দু’টো কাজ করতে পারি। দশটা ফাইলই রিড করে তার ডেটা একটা অ্যারেতে রাখতে পারি। তারপর সেই অ্যারের ওপর একটা লুপ চালাতে পারি। আর আমাদের সাধের প্রোগ্রামটা র‌্যামের এক জিবি জায়গা দখল করে রাখবে পুরোটা প্রসেসিংয়ের সময়, যেখানে সাধারণ ইউজারদের র‌্যামের আকারই হয় দুই কি চার জিবি। অথবা আমরা একটা ইটারেটর ডিজাইন করতে পারি, যে ইটারেটরে next() ফাংশন কল করলে একটা করে ফাইল রিড করে রিটার্ন করে দেবে। সেটা প্রসেস শেষ করার পরে next() ফাংশন পরবর্তী ফাইলটা রিড করবে। অন্য ভাবে বলতে গেলে লুপের প্রতি ইটারেশনে একটা করে ফাইল রিড করা হবে। তাহলে র‌্যামে জায়গা দখল করা হবে মাত্র 100MB। ডিসিশন আপনার।

ইটারেটরের আরেকটা ইউজ কেস হচ্ছে ইনফাইনাইট ইটারেটর। হতে পারে আমাদের একটা র‌্যান্ডম নাম্বার জেনারেটর লাগবে, যেটা দিয়ে আমরা যতক্ষণ খুশি র‌্যান্ডম নাম্বার নিতে থাকব, যতক্ষণ আমাদের ক্রাইটেরিয়া না মেলে।

আপাতত একটা সহজ উদাহরণ দেখা যাক। ধরা যাক, আমাদের একটা অ্যারে আছে স্বেচ্ছাসেবীর নামের। এখন আমাদের সবাইক সিরিয়ালি লাল, নীল, সবুজ এই তিনটার রং দিয়ে মার্ক করতে হবে। অর্থাৎ, প্রথম জনকে লাল, দ্বিতীয় জনকে নীল, তৃতীয় জনকে সবুজ, চতুর্থ জনকে লাল, পঞ্চম জনকে নীল এভাবে চলতে থাকবে।

দেখা যাক ইটারেটর ব্যবহার করে আমরা কীভাবে সেটা করতে পারি।

এভাবে আজীবন চাইলে আমরা কালার পেতে থাকব।

এখন যদি আমি প্রশ্ন করি, আপনার পরবর্তী প্রোজেক্টে কি আপনি ইটারেটর ব্যবহার করবেন? আমার ধারণা, এখনও বেশিরভাগের উত্তর হবে — না ভাই, দরকার নেই। কারণটা সম্ভবত ইটারেটরে অল্প একটু কাজ করার জন্যেও অনেকখানি কোড লিখতে হয়। ব্যাপারটা সত্যি! আর তাই, ইটারেটর লেখার জন্যে জাভাস্ক্রিপ্টে আরেকটা সহজ সিনট্যাক্স আছে। সেটার নাম জেনারেটর। আমরা চাইলে জেনারেটর দিয়েই শুরু করতে পারতাম। কিন্তু, তাহলে আমাদের for…of লুপকে হয়তো চেনা হত কিন্তু, ইটারেটরকেই চেনা হত না।

সোজা বাংলায় জেনারেটর হচ্ছে ইটারেটর লেখার সহজ সিনট্যাক্স। তো সেটাতেও একবার চোখ বুলিয়ে নেয়া যাক।

আমরা আগের color() ফাংশনটাকে এই নতুন কালার ফাংশন দিয়ে রিপ্লেস করে দিতে পারি। দুটো ফাংশন ঠিক একই কাজ করে। কিন্তু পরেরটায় শুধুমাত্র দরকারী লজিকটুকুই লেখা। শুধু শুধু পিচ্চি একটু কাজকে next() আর [Symbol.iterator]() ফাংশন দিয়ে র‍্যাপ করার কোন দরকার নেই। ভ্যালুগুলোকে {value: someValue, done: false} আকারের অবজেক্টে র‍্যাপ করারও দরকার নেই। শুধু একটা/কয়েকটা yield আর function কিওয়ার্ডের পরে একটা *। ব্যাস! জাভাস্ক্রিপ্ট নিজেই পুরোটা করে নেবে। এবার আশা করি অনেকেরই ইন্টারেস্ট জন্মাবে ইটারেটর নিয়ে। আর এর পরেও যদি ইটারেটরের ব্যবহার নিয়ে কনফিউশন থাকে, তাহলে ইটারেটরের প্রাকটিক্যাল ইউজ নিয়ে এই লেখাটা পড়তে পারেন।

জাভাস্ক্রিপ্টে Array, String, Map, Set ইত্যাদিকে ইটারেবল হিসেবে ইম্প্লিমেন্ট করা হয়েছে। সুতরাং, যেকোন Array, String, Map বা Set এ আমরা ইটারেটরের ফাংশনালিটিগুলো(লুপ চালানো, স্প্রেড করা) ব্যবহার করতে পারি।

হ্যাপি জাভাস্ক্রিপ্টিং!

--

--