অল্প সল্প ইটারেটর: ফর লুপের অন্তঃকথন
জাভাস্ক্রিপ্টের 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
এ আমরা ইটারেটরের ফাংশনালিটিগুলো(লুপ চালানো, স্প্রেড করা) ব্যবহার করতে পারি।