ইটারেটর ব্যবহার করে আইটেমের সিরিজ প্রসেস করা
ইটারেটর প্যাটার্ন (iterator pattern) আপনাকে একটি সিকোয়েন্সের (sequence) প্রতিটি আইটেমের উপর পর্যায়ক্রমে কোনো কাজ করার সুযোগ দেয়। একটি ইটারেটর প্রতিটি আইটেমের উপর পুনরাবৃত্তি (iterating) করার এবং সিকোয়েন্সটি কখন শেষ হয়েছে তা নির্ধারণ করার লজিকের জন্য দায়ী থাকে। আপনি যখন ইটারেটর ব্যবহার করেন, তখন আপনাকে সেই লজিকটি নিজে থেকে পুনরায় ইমপ্লিমেন্ট (reimplement) করতে হয় না।
রাস্টে, ইটারেটরগুলো lazy (অলস), যার মানে হলো যতক্ষণ না আপনি ইটারেটরটিকে ব্যবহার করার জন্য কোনো মেথড কল করছেন, ততক্ষণ পর্যন্ত এর কোনো প্রভাব থাকে না। উদাহরণস্বরূপ, Listing 13-10-এর কোড v1
ভেক্টরের আইটেমগুলোর উপর একটি ইটারেটর তৈরি করে, যা Vec<T>
-তে ডিফাইন করা iter
মেথড কল করার মাধ্যমে করা হয়। এই কোডটি নিজে থেকে কোনো দরকারী কাজ করে না।
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }``` </Listing> ইটারেটরটি `v1_iter` ভ্যারিয়েবলে সংরক্ষণ করা হয়েছে। একবার আমরা একটি ইটারেটর তৈরি করলে, আমরা এটিকে বিভিন্ন উপায়ে ব্যবহার করতে পারি। Listing 3-5-এ, আমরা একটি `for` লুপ ব্যবহার করে একটি অ্যারের উপর ইটারেট করেছিলাম এবং প্রতিটি আইটেমের উপর কিছু কোড এক্সিকিউট করেছিলাম। পর্দার আড়ালে, এটি একটি ইটারেটর তৈরি করে এবং তারপর তা ব্যবহার করে, কিন্তু এখন পর্যন্ত আমরা এটি ঠিক কীভাবে কাজ করে তা বিস্তারিত আলোচনা করিনি। Listing 13-11-এর উদাহরণে, আমরা ইটারেটর তৈরি করা এবং `for` লুপে ইটারেটর ব্যবহার করাকে আলাদা করেছি। যখন `v1_iter`-এর ইটারেটর ব্যবহার করে `for` লুপ কল করা হয়, তখন ইটারেটরের প্রতিটি এলিমেন্ট লুপের একটি ইটারেশনে ব্যবহৃত হয়, যা প্রতিটি মান প্রিন্ট করে। <Listing number="13-11" file-name="src/main.rs" caption="`for` লুপে একটি ইটারেটর ব্যবহার করা"> ```rust fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {val}"); } }
যেসব ভাষায় তাদের স্ট্যান্ডার্ড লাইব্রেরিতে ইটারেটর সরবরাহ করা হয় না, সেখানে আপনাকে সম্ভবত একই কার্যকারিতা লিখতে হতো একটি ভ্যারিয়েবলকে ইনডেক্স ০ থেকে শুরু করে, সেই ভ্যারিয়েবলটি ব্যবহার করে ভেক্টর থেকে একটি মান পেতে, এবং লুপের মধ্যে ভ্যারিয়েবলের মান বাড়িয়ে যতক্ষণ না এটি ভেক্টরের মোট আইটেমের সংখ্যায় পৌঁছায়।
ইটারেটর আপনার জন্য এই সমস্ত লজিক পরিচালনা করে, যা পুনরাবৃত্তিমূলক কোড কমিয়ে দেয় এবং সম্ভাব্য ভুল এড়াতে সাহায্য করে। ইটারেটর আপনাকে অনেক বিভিন্ন ধরণের সিকোয়েন্সের সাথে একই লজিক ব্যবহার করার জন্য আরও বেশি ফ্লেক্সিবিলিটি দেয়, শুধু ভেক্টরের মতো ডেটা স্ট্রাকচার নয় যা আপনি ইনডেক্স করতে পারেন। আসুন দেখি ইটারেটর কীভাবে তা করে।
Iterator
ট্রেইট এবং next
মেথড
সমস্ত ইটারেটর Iterator
নামের একটি ট্রেইট (trait) ইমপ্লিমেন্ট করে যা স্ট্যান্ডার্ড লাইব্রেরিতে ডিফাইন করা আছে। ট্রেইটের ডেফিনিশনটি দেখতে এইরকম:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided } }
লক্ষ্য করুন যে এই ডেফিনিশনে কিছু নতুন সিনট্যাক্স ব্যবহার করা হয়েছে: type Item
এবং Self::Item
, যা এই ট্রেইটের সাথে একটি associated type ডিফাইন করছে। আমরা Chapter 20-এ associated type নিয়ে গভীরভাবে আলোচনা করব। আপাতত, আপনার শুধু এটুকু জানলেই চলবে যে এই কোডটি বলছে Iterator
ট্রেইট ইমপ্লিমেন্ট করার জন্য আপনাকে একটি Item
টাইপও ডিফাইন করতে হবে, এবং এই Item
টাইপটি next
মেথডের রিটার্ন টাইপে ব্যবহৃত হয়। অন্য কথায়, Item
টাইপটি হবে ইটারেটর থেকে রিটার্ন করা টাইপ।
Iterator
ট্রেইট ইমপ্লিমেন্ট করার জন্য শুধুমাত্র একটি মেথড ডিফাইন করতে হয়: next
মেথড, যা ইটারেটরের একটি করে আইটেম Some
-এ মুড়িয়ে রিটার্ন করে, এবং যখন ইটারেশন শেষ হয়ে যায়, তখন None
রিটার্ন করে।
আমরা ইটারেটরের উপর সরাসরি next
মেথড কল করতে পারি; Listing 13-12 দেখায় যে ভেক্টর থেকে তৈরি করা ইটারেটরের উপর বারবার next
কল করলে কী মান রিটার্ন হয়।
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
লক্ষ্য করুন যে আমাদের v1_iter
-কে মিউটেবল (mutable) করতে হয়েছিল: একটি ইটারেটরের উপর next
মেথড কল করলে এর অভ্যন্তরীণ অবস্থা পরিবর্তিত হয়, যা ইটারেটর সিকোয়েন্সে তার অবস্থান ট্র্যাক করতে ব্যবহার করে। অন্য কথায়, এই কোডটি ইটারেটরটিকে কনজিউম (consumes) বা ব্যবহার করে ফেলে। প্রতিটি next
কল ইটারেটর থেকে একটি আইটেম গ্রহণ করে। for
লুপ ব্যবহার করার সময় আমাদের v1_iter
-কে মিউটেবল করতে হয়নি কারণ লুপটি v1_iter
-এর মালিকানা নিয়ে পর্দার আড়ালে এটিকে মিউটেবল করে দিয়েছিল।
আরও লক্ষ্য করুন যে next
কল থেকে আমরা যে মানগুলো পাই তা ভেক্টরের মানগুলোর ইমিউটেবল রেফারেন্স (immutable references)। iter
মেথড ইমিউটেবল রেফারেন্সের উপর একটি ইটারেটর তৈরি করে। যদি আমরা এমন একটি ইটারেটর তৈরি করতে চাই যা v1
-এর মালিকানা নেয় এবং ওউনড ভ্যালু (owned values) রিটার্ন করে, তাহলে আমরা iter
-এর পরিবর্তে into_iter
কল করতে পারি। একইভাবে, যদি আমরা মিউটেবল রেফারেন্সের উপর ইটারেট করতে চাই, তাহলে আমরা iter
-এর পরিবর্তে iter_mut
কল করতে পারি।
যে মেথডগুলো ইটারেটরকে ব্যবহার করে ফেলে (consume)
Iterator
ট্রেইটে স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা ডিফল্ট ইমপ্লিমেন্টেশনসহ বেশ কয়েকটি ভিন্ন মেথড রয়েছে; আপনি Iterator
ট্রেইটের জন্য স্ট্যান্ডার্ড লাইব্রেরি API ডকুমেন্টেশনে এই মেথডগুলো সম্পর্কে জানতে পারবেন। এই মেথডগুলোর মধ্যে কিছু তাদের ডেফিনিশনে next
মেথডকে কল করে, যে কারণে Iterator
ট্রেইট ইমপ্লিমেন্ট করার সময় আপনাকে next
মেথড ইমপ্লিমেন্ট করতে হয়।
যে মেথডগুলো next
কল করে, সেগুলোকে কনজিউমিং অ্যাডাপ্টার (consuming adapters) বলা হয়, কারণ এগুলো কল করলে ইটারেটরটি ব্যবহৃত হয়ে যায়। একটি উদাহরণ হলো sum
মেথড, যা ইটারেটরের মালিকানা নেয় এবং বারবার next
কল করে আইটেমগুলোর মধ্য দিয়ে ইটারেট করে, ফলে ইটারেটরটি ব্যবহৃত হয়। এটি ইটারেট করার সময় প্রতিটি আইটেমকে একটি চলমান মোটের সাথে যোগ করে এবং ইটারেশন সম্পূর্ণ হলে মোটটি রিটার্ন করে। Listing 13-13-এ sum
মেথডের ব্যবহার দেখানো একটি টেস্ট রয়েছে।
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
sum
কল করার পরে আমরা v1_iter
ব্যবহার করতে পারব না কারণ sum
যে ইটারেটরের উপর কল করা হয় তার মালিকানা নিয়ে নেয়।
যে মেথডগুলো অন্য ইটারেটর তৈরি করে
ইটারেটর অ্যাডাপ্টার (Iterator adapters) হলো Iterator
ট্রেইটে ডিফাইন করা এমন মেথড যা ইটারেটরকে ব্যবহার করে না। বরং, এগুলো মূল ইটারেটরের কিছু দিক পরিবর্তন করে ভিন্ন ইটারেটর তৈরি করে।
Listing 13-14 ইটারেটর অ্যাডাপ্টার মেথড map
কল করার একটি উদাহরণ দেখায়, যা একটি ক্লোজার নেয় এবং আইটেমগুলোর উপর ইটারেট করার সময় প্রতিটি আইটেমের উপর সেই ক্লোজারকে কল করে। map
মেথড একটি নতুন ইটারেটর রিটার্ন করে যা পরিবর্তিত আইটেমগুলো তৈরি করে। এখানকার ক্লোজারটি একটি নতুন ইটারেটর তৈরি করে যেখানে ভেক্টরের প্রতিটি আইটেমের মান ১ করে বাড়ানো হবে।
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
তবে, এই কোডটি একটি সতর্কবার্তা (warning) তৈরি করে:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
Listing 13-14-এর কোডটি কিছুই করে না; আমরা যে ক্লোজারটি নির্দিষ্ট করেছি তা কখনই কল করা হয় না। সতর্কবার্তাটি আমাদের মনে করিয়ে দেয় কেন: ইটারেটর অ্যাডাপ্টারগুলো lazy, এবং আমাদের এখানে ইটারেটরটি ব্যবহার করতে হবে।
এই সতর্কবার্তাটি ঠিক করতে এবং ইটারেটরটি ব্যবহার করতে, আমরা collect
মেথড ব্যবহার করব, যা আমরা Listing 12-1-এ env::args
-এর সাথে ব্যবহার করেছিলাম। এই মেথডটি ইটারেটরকে ব্যবহার করে এবং ফলস্বরূপ মানগুলোকে একটি কালেকশন ডেটা টাইপে সংগ্রহ করে।
Listing 13-15-এ, আমরা map
কল থেকে রিটার্ন করা ইটারেটরের উপর ইটারেট করার ফলাফল একটি ভেক্টরে সংগ্রহ করি। এই ভেক্টরটিতে মূল ভেক্টরের প্রতিটি আইটেম থাকবে, যার মান ১ করে বাড়ানো হয়েছে।
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
যেহেতু map
একটি ক্লোজার নেয়, তাই আমরা প্রতিটি আইটেমের উপর যেকোনো অপারেশন নির্দিষ্ট করতে পারি। এটি একটি চমৎকার উদাহরণ যে কীভাবে ক্লোজার আপনাকে কিছু আচরণ কাস্টমাইজ করতে দেয় এবং একই সাথে Iterator
ট্রেইট দ্বারা প্রদত্ত ইটারেশন আচরণটি পুনরায় ব্যবহার করতে দেয়।
আপনি একাধিক ইটারেটর অ্যাডাপ্টার কল চেইন করে জটিল কাজগুলো একটি পঠনযোগ্য উপায়ে সম্পাদন করতে পারেন। কিন্তু যেহেতু সমস্ত ইটারেটর lazy, তাই ইটারেটর অ্যাডাপ্টার কল থেকে ফলাফল পেতে আপনাকে কনজিউমিং অ্যাডাপ্টার মেথডগুলোর একটি কল করতে হবে।
এনভায়রনমেন্ট ক্যাপচার করে এমন ক্লোজার ব্যবহার করা
অনেক ইটারেটর অ্যাডাপ্টার আর্গুমেন্ট হিসেবে ক্লোজার নেয়, এবং সাধারণত আমরা ইটারেটর অ্যাডাপ্টারের আর্গুমেন্ট হিসেবে যে ক্লোজারগুলো নির্দিষ্ট করব তা তাদের এনভায়রনমেন্ট ক্যাপচার করে।
এই উদাহরণের জন্য, আমরা filter
মেথড ব্যবহার করব যা একটি ক্লোজার নেয়। ক্লোজারটি ইটারেটর থেকে একটি আইটেম পায় এবং একটি bool
রিটার্ন করে। যদি ক্লোজারটি true
রিটার্ন করে, তবে মানটি filter
দ্বারা উৎপাদিত ইটারেশনে অন্তর্ভুক্ত হবে। যদি ক্লোজারটি false
রিটার্ন করে, তবে মানটি অন্তর্ভুক্ত হবে না।
Listing 13-16-এ, আমরা filter
ব্যবহার করি একটি ক্লোজারের সাথে যা তার এনভায়রনমেন্ট থেকে shoe_size
ভ্যারিয়েবলটি ক্যাপচার করে Shoe
struct ইনস্ট্যান্সের একটি কালেকশনের উপর ইটারেট করার জন্য। এটি শুধুমাত্র নির্দিষ্ট আকারের জুতা রিটার্ন করবে।
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
shoes_in_size
ফাংশনটি প্যারামিটার হিসেবে একটি জুতার ভেক্টরের মালিকানা এবং একটি জুতার সাইজ নেয়। এটি শুধুমাত্র নির্দিষ্ট আকারের জুতা ধারণকারী একটি ভেক্টর রিটার্ন করে।
shoes_in_size
-এর বডিতে, আমরা into_iter
কল করে একটি ইটারেটর তৈরি করি যা ভেক্টরের মালিকানা নেয়। তারপর আমরা filter
কল করে সেই ইটারেটরটিকে একটি নতুন ইটারেটরে অ্যাডাপ্ট করি যা শুধুমাত্র সেই এলিমেন্টগুলো ধারণ করে যার জন্য ক্লোজারটি true
রিটার্ন করে।
ক্লোজারটি এনভায়রনমেন্ট থেকে shoe_size
প্যারামিটারটি ক্যাপচার করে এবং প্রতিটি জুতার আকারের সাথে মানটি তুলনা করে, শুধুমাত্র নির্দিষ্ট আকারের জুতাগুলো রাখে। অবশেষে, collect
কল করা অ্যাডাপ্টেড ইটারেটর দ্বারা রিটার্ন করা মানগুলোকে একটি ভেক্টরে সংগ্রহ করে যা ফাংশন দ্বারা রিটার্ন করা হয়।
টেস্টটি দেখায় যে যখন আমরা shoes_in_size
কল করি, তখন আমরা শুধুমাত্র সেই জুতাগুলো ফেরত পাই যেগুলোর আকার আমাদের নির্দিষ্ট করা মানের সমান।