আমাদের I/O প্রজেক্টের উন্নতি সাধন
ইটারেটর সম্পর্কে আমাদের এই নতুন জ্ঞানের মাধ্যমে, আমরা Chapter 12-এর I/O প্রজেক্টকে উন্নত করতে পারি। ইটারেটর ব্যবহার করে কোডের কিছু অংশ আরও স্পষ্ট এবং সংক্ষিপ্ত করা সম্ভব। আসুন দেখি কীভাবে ইটারেটর আমাদের Config::build
ফাংশন এবং search
ফাংশনের ইমপ্লিমেন্টেশনকে উন্নত করতে পারে।
ইটারেটর ব্যবহার করে clone
সরানো
Listing 12-6-এ, আমরা এমন কোড যোগ করেছিলাম যা String
ভ্যালুর একটি স্লাইস (slice) নিত এবং স্লাইসে ইনডেক্সিং করে ও ভ্যালুগুলো ক্লোন (cloning) করে Config
struct-এর একটি ইনস্ট্যান্স তৈরি করত, যার ফলে Config
struct সেই ভ্যালুগুলোর মালিকানা (own) পেত। Listing 13-17-এ, আমরা Config::build
ফাংশনের ইমপ্লিমেন্টেশনটি পুনরায় তুলে ধরেছি, যা Listing 12-23-এ ছিল।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
সেই সময়ে, আমরা বলেছিলাম অদক্ষ clone
কলগুলো নিয়ে চিন্তা না করতে, কারণ আমরা ভবিষ্যতে সেগুলো সরিয়ে ফেলব। এখন সেই সময় এসেছে!
এখানে আমাদের clone
প্রয়োজন হয়েছিল কারণ args
প্যারামিটারে আমাদের String
এলিমেন্টসহ একটি স্লাইস ছিল, কিন্তু build
ফাংশন args
-এর মালিক ছিল না। Config
ইনস্ট্যান্সের মালিকানা রিটার্ন করার জন্য, আমাদের Config
-এর query
এবং file_path
ফিল্ডের ভ্যালুগুলো ক্লোন করতে হয়েছিল যাতে Config
ইনস্ট্যান্স তার ভ্যালুগুলোর মালিকানা পেতে পারে।
ইটারেটর সম্পর্কে আমাদের নতুন জ্ঞানের মাধ্যমে, আমরা build
ফাংশনটি পরিবর্তন করে স্লাইস ধার (borrow) করার পরিবর্তে আর্গুমেন্ট হিসেবে একটি ইটারেটরের মালিকানা নিতে পারি। আমরা স্লাইসের দৈর্ঘ্য পরীক্ষা করা এবং নির্দিষ্ট লোকেশনে ইনডেক্স করার কোডের পরিবর্তে ইটারেটরের কার্যকারিতা ব্যবহার করব। এটি Config::build
ফাংশনটি কী করছে তা আরও স্পষ্ট করবে কারণ ইটারেটর ভ্যালুগুলো অ্যাক্সেস করবে।
যখন Config::build
ইটারেটরের মালিকানা নেবে এবং ধার করা ইনডেক্সিং অপারেশন ব্যবহার করা বন্ধ করবে, তখন আমরা clone
কল করে নতুন মেমোরি অ্যালোকেশন করার পরিবর্তে ইটারেটর থেকে String
ভ্যালুগুলো Config
-এ মুভ (move) করতে পারব।
সরাসরি রিটার্ন করা ইটারেটর ব্যবহার করা
আপনার I/O প্রজেক্টের src/main.rs ফাইলটি খুলুন, যা দেখতে এমন হওয়া উচিত:
ফাইলের নাম: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
আমরা প্রথমে main
ফাংশনের শুরুটি পরিবর্তন করব, যা Listing 12-24-এ ছিল। এবার আমরা Listing 13-18-এর কোডটি ব্যবহার করব, যা একটি ইটারেটর ব্যবহার করে। এটি ততক্ষণ পর্যন্ত কম্পাইল হবে না যতক্ষণ না আমরা Config::build
আপডেট করছি।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args
ফাংশনটি একটি ইটারেটর রিটার্ন করে! ইটারেটরের ভ্যালুগুলোকে একটি ভেক্টরে সংগ্রহ করে তারপর Config::build
-এ একটি স্লাইস পাস করার পরিবর্তে, এখন আমরা env::args
থেকে রিটার্ন করা ইটারেটরের মালিকানা সরাসরি Config::build
-কে পাস করছি।
এরপর, আমাদের Config::build
-এর ডেফিনিশন আপডেট করতে হবে। আসুন Config::build
-এর সিগনেচার (signature) পরিবর্তন করে Listing 13-19-এর মতো করি। এটি এখনও কম্পাইল হবে না, কারণ আমাদের ফাংশনের বডি আপডেট করতে হবে।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args
ফাংশনের জন্য স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশন দেখায় যে এটি যে ইটারেটর রিটার্ন করে তার টাইপ হলো std::env::Args
, এবং সেই টাইপটি Iterator
ট্রেইট ইমপ্লিমেন্ট করে এবং String
ভ্যালু রিটার্ন করে।
আমরা Config::build
ফাংশনের সিগনেচার আপডেট করেছি যাতে args
প্যারামিটারটির &[String]
-এর পরিবর্তে impl Iterator<Item = String>
ট্রেইট বাউন্ডসহ একটি জেনেরিক টাইপ থাকে। Chapter 10-এর "Traits as Parameters" বিভাগে আলোচনা করা impl Trait
সিনট্যাক্সের এই ব্যবহারটির অর্থ হলো args
যেকোনো টাইপের হতে পারে যা Iterator
ট্রেইট ইমপ্লিমেন্ট করে এবং String
আইটেম রিটার্ন করে।
যেহেতু আমরা args
-এর মালিকানা নিচ্ছি এবং এর উপর ইটারেট করে args
-কে পরিবর্তন (mutate) করব, তাই আমরা args
প্যারামিটারের স্পেসিফিকেশনে mut
কীওয়ার্ড যোগ করে এটিকে মিউটেবল করতে পারি।
ইনডেক্সিং এর পরিবর্তে Iterator
ট্রেইট মেথড ব্যবহার করা
এরপর, আমরা Config::build
-এর বডি ঠিক করব। যেহেতু args
, Iterator
ট্রেইট ইমপ্লিমেন্ট করে, আমরা জানি যে আমরা এর উপর next
মেথড কল করতে পারি! Listing 13-20, Listing 12-23-এর কোডটি next
মেথড ব্যবহার করার জন্য আপডেট করে।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
মনে রাখবেন env::args
-এর রিটার্ন ভ্যালুর প্রথম মানটি হলো প্রোগ্রামের নাম। আমরা সেটি উপেক্ষা করে পরবর্তী ভ্যালুটি পেতে চাই, তাই প্রথমে আমরা next
কল করি এবং রিটার্ন ভ্যালু নিয়ে কিছুই করি না। তারপর আমরা Config
-এর query
ফিল্ডে যে ভ্যালু রাখতে চাই তা পেতে আবার next
কল করি। যদি next
একটি Some
রিটার্ন করে, আমরা ভ্যালুটি এক্সট্র্যাক্ট করতে একটি match
ব্যবহার করি। যদি এটি None
রিটার্ন করে, তার মানে যথেষ্ট আর্গুমেন্ট দেওয়া হয়নি এবং আমরা একটি Err
ভ্যালু দিয়ে আগেভাগেই রিটার্ন করি। আমরা file_path
ভ্যালুর জন্যও একই কাজ করি।
ইটারেটর অ্যাডাপ্টার দিয়ে কোড আরও স্পষ্ট করা
আমরা আমাদের I/O প্রজেক্টের search
ফাংশনেও ইটারেটরের সুবিধা নিতে পারি, যা এখানে Listing 13-21-এ পুনঃপ্রস্তুত করা হয়েছে যেমনটি Listing 12-19-এ ছিল।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
আমরা এই কোডটি ইটারেটর অ্যাডাপ্টার মেথড ব্যবহার করে আরও সংক্ষিপ্তভাবে লিখতে পারি। এটি করলে আমরা একটি মিউটেবল অন্তর্বর্তী results
ভেক্টর এড়াতে পারি। ফাংশনাল প্রোগ্রামিং স্টাইল কোডকে আরও স্পষ্ট করার জন্য মিউটেবল স্টেট (mutable state) কমানো পছন্দ করে। মিউটেবল স্টেট অপসারণ ভবিষ্যতে সমান্তরালভাবে সার্চিং করার জন্য একটি enhancement সক্ষম করতে পারে কারণ আমাদের results
ভেক্টরের কনকারেন্ট অ্যাক্সেস পরিচালনা করতে হবে না। Listing 13-22 এই পরিবর্তনটি দেখায়।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
স্মরণ করুন যে search
ফাংশনের উদ্দেশ্য হলো contents
-এর মধ্যে query
ধারণকারী সমস্ত লাইন রিটার্ন করা। Listing 13-16-এর filter
উদাহরণের মতো, এই কোডটি filter
অ্যাডাপ্টার ব্যবহার করে শুধুমাত্র সেই লাইনগুলো রাখে যার জন্য line.contains(query)
true
রিটার্ন করে। তারপর আমরা collect
দিয়ে মিলে যাওয়া লাইনগুলোকে অন্য একটি ভেক্টরে সংগ্রহ করি। অনেক সহজ! search_case_insensitive
ফাংশনেও ইটারেটর মেথড ব্যবহার করার জন্য নির্দ্বিধায় একই পরিবর্তন করুন।
আরও উন্নতির জন্য, collect
কলটি সরিয়ে দিয়ে এবং রিটার্ন টাইপ পরিবর্তন করে impl Iterator<Item = &'a str>
করে search
ফাংশন থেকে একটি ইটারেটর রিটার্ন করুন যাতে ফাংশনটি একটি ইটারেটর অ্যাডাপ্টার হয়ে যায়। লক্ষ্য করুন যে আপনাকে টেস্টগুলোও আপডেট করতে হবে! এই পরিবর্তন করার আগে এবং পরে আপনার minigrep
টুল ব্যবহার করে একটি বড় ফাইল সার্চ করে আচরণের পার্থক্য পর্যবেক্ষণ করুন। এই পরিবর্তনের আগে, প্রোগ্রামটি সমস্ত ফলাফল সংগ্রহ না করা পর্যন্ত কোনো ফলাফল প্রিন্ট করবে না, কিন্তু পরিবর্তনের পরে, প্রতিটি মিলে যাওয়া লাইন খুঁজে পাওয়ার সাথে সাথে ফলাফলগুলো প্রিন্ট হবে কারণ run
ফাংশনের for
লুপ ইটারেটরের অলসতার (laziness) সুবিধা নিতে সক্ষম।
লুপ এবং ইটারেটরের মধ্যে একটি বেছে নেওয়া
পরবর্তী যৌক্তিক প্রশ্ন হলো আপনার নিজের কোডে কোন স্টাইলটি বেছে নেওয়া উচিত এবং কেন: Listing 13-21-এর মূল ইমপ্লিমেন্টেশন নাকি Listing 13-22-এর ইটারেটর ব্যবহার করা সংস্করণটি (ধরে নিচ্ছি আমরা ইটারেটর রিটার্ন না করে সমস্ত ফলাফল সংগ্রহ করছি)। বেশিরভাগ রাস্ট প্রোগ্রামার ইটারেটর স্টাইল ব্যবহার করতে পছন্দ করেন। এটি প্রথমে আয়ত্ত করা কিছুটা কঠিন, কিন্তু একবার আপনি বিভিন্ন ইটারেটর অ্যাডাপ্টার এবং সেগুলো কী করে সে সম্পর্কে ধারণা পেয়ে গেলে, ইটারেটর বোঝা সহজ হতে পারে। লুপের বিভিন্ন অংশ এবং নতুন ভেক্টর তৈরি করার ঝামেলার পরিবর্তে, কোডটি লুপের উচ্চ-স্তরের উদ্দেশ্যের উপর মনোযোগ দেয়। এটি কিছু সাধারণ কোডকে অ্যাবস্ট্রাক্ট করে দেয় যাতে এই কোডের জন্য অনন্য ধারণাগুলো, যেমন ইটারেটরের প্রতিটি এলিমেন্টকে যে ফিল্টারিং শর্তটি পাস করতে হবে, তা দেখা সহজ হয়।
কিন্তু দুটি ইমপ্লিমেন্টেশন কি সত্যিই সমতুল্য? স্বতঃস্ফূর্ত ধারণা হতে পারে যে নিম্ন-স্তরের (lower-level) লুপটি দ্রুততর হবে। আসুন পারফরম্যান্স নিয়ে কথা বলি।