আমাদের I/O প্রোজেক্টকে উন্নত করা

Iterator সম্পর্কে এই নতুন জ্ঞান দিয়ে, আমরা Chapter 12-এর I/O প্রোজেক্টকে improve করতে পারি iterator ব্যবহার করে code-এর জায়গাগুলোকে আরও clear এবং concise করতে। আসুন দেখি কীভাবে iterator গুলো Config::build ফাংশন এবং search ফাংশনের আমাদের implementation-কে improve করতে পারে।

একটি clone সরানো Iterator ব্যবহার করে

Listing 12-6-এ, আমরা code যোগ করেছিলাম যেটি String value-গুলোর একটি slice নিত এবং slice-এ index করে এবং value গুলোকে clone করে Config struct-এর একটি instance create করত, Config struct-কে সেই value-গুলোর owner হওয়ার অনুমতি দিত। Listing 13-17-এ, আমরা Config::build ফাংশনের implementation-টিকে পুনরায় লিখেছি যেমনটি Listing 12-23-এ ছিল:

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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
}

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)
        );
    }
}

তখন, আমরা বলেছিলাম inefficient clone কলগুলো নিয়ে চিন্তা না করতে কারণ আমরা ভবিষ্যতে সেগুলোকে সরিয়ে দেব। এখন সেই সময়!

আমাদের এখানে clone-এর প্রয়োজন ছিল কারণ parameter args-এ String element-সহ একটি slice রয়েছে, কিন্তু build ফাংশনটি args-এর owner নয়। একটি Config instance-এর ownership return করার জন্য, আমাদের Config-এর query এবং file_path field থেকে value গুলোকে clone করতে হয়েছিল যাতে Config instance টি তার value-গুলোর owner হতে পারে।

Iterator সম্পর্কে আমাদের নতুন জ্ঞান দিয়ে, আমরা build ফাংশনটিকে পরিবর্তন করতে পারি একটি slice borrow করার পরিবর্তে argument হিসেবে একটি iterator-এর ownership নেওয়ার জন্য। আমরা slice-এর length check করা এবং specific location-গুলোতে index করার code-এর পরিবর্তে iterator functionality ব্যবহার করব। এটি Config::build ফাংশনটি কী করছে তা স্পষ্ট করবে কারণ iterator টি value গুলো access করবে।

একবার Config::build iterator-এর ownership নেওয়ার পরে এবং borrow করা indexing operation গুলো ব্যবহার করা বন্ধ করে দিলে, আমরা clone কল না করে এবং একটি new allocation তৈরি না করে iterator থেকে String value গুলোকে Config-এ move করতে পারি।

Returned Iterator সরাসরি ব্যবহার করা

আপনার I/O প্রোজেক্টের src/main.rs ফাইলটি খুলুন, যেটি এইরকম হওয়া উচিত:

Filename: src/main.rs

use std::env;
use std::process;

use minigrep::Config;

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) = minigrep::run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

আমরা প্রথমে main ফাংশনের শুরু পরিবর্তন করব যা Listing 12-24-এ ছিল Listing 13-18-এর code-এ, যেটি এবার একটি iterator ব্যবহার করে। আমরা যতক্ষণ Config::build update না করি ততক্ষণ এটি compile হবে না।

use std::env;
use std::process;

use minigrep::Config;

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) = minigrep::run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

Env::args ফাংশন একটি iterator return করে! Iterator value গুলোকে একটি vector-এ collect করে তারপর Config::build-এ একটি slice pass করার পরিবর্তে, এখন আমরা env::args থেকে returned iterator-এর ownership সরাসরি Config::build-এ pass করছি।

এরপরে, আমাদের Config::build-এর definition update করতে হবে। আপনার I/O প্রোজেক্টের src/lib.rs ফাইলে, আসুন Config::build-এর signature পরিবর্তন করে Listing 13-19-এর মতো করি। এটি এখনও compile হবে না কারণ আমাদের function body update করতে হবে।

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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
}

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)
        );
    }
}

Env::args ফাংশনের জন্য standard library documentation দেখায় যে এটি যে iterator return করে তার type হল std::env::Args, এবং সেই type টি Iterator trait implement করে এবং String value return করে।

আমরা Config::build ফাংশনের signature আপডেট করেছি যাতে parameter args-এর একটি generic type থাকে trait bound impl Iterator<Item = String> সহ, &[String]-এর পরিবর্তে। Chapter 10-এর “প্যারামিটার হিসেবে Traits” বিভাগে আলোচনা করা impl Trait syntax-এর এই ব্যবহারটির অর্থ হল args যেকোনো type হতে পারে যেটি Iterator trait implement করে এবং String item return করে।

যেহেতু আমরা args-এর ownership নিচ্ছি এবং এটিকে iterate করে args কে mutate করব, তাই আমরা এটিকে mutable করতে args parameter-এর specification-এ mut keyword যোগ করতে পারি।

Indexing-এর পরিবর্তে Iterator Trait Method ব্যবহার করা

এরপরে, আমরা Config::build-এর body ঠিক করব। যেহেতু args, Iterator trait implement করে, তাই আমরা জানি যে আমরা এটিতে next method কল করতে পারি! Listing 13-20, Listing 12-23 থেকে code update করে next method ব্যবহার করার জন্য:

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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
}

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)
        );
    }
}

মনে রাখবেন যে env::args-এর return value-এর প্রথম value টি হল প্রোগ্রামের নাম। আমরা সেটিকে ignore করতে চাই এবং পরবর্তী value-তে যেতে চাই, তাই প্রথমে আমরা next কল করি এবং return value-এর সাথে কিছুই করি না। দ্বিতীয়ত, আমরা Config-এর query field-এ যে value টি রাখতে চাই সেটি পেতে next কল করি। যদি next একটি Some return করে, তাহলে আমরা value টি extract করতে একটি match ব্যবহার করি। যদি এটি None return করে, তাহলে এর অর্থ হল পর্যাপ্ত argument দেওয়া হয়নি এবং আমরা একটি Err value দিয়ে তাড়াতাড়ি return করি। আমরা file_path value-এর জন্য একই কাজ করি।

Iterator Adapter দিয়ে Code আরও Clear করা

আমরা আমাদের I/O প্রোজেক্টের search ফাংশনেও iterator-গুলোর সুবিধা নিতে পারি, যেটি Listing 13-21-এ পুনরায় লেখা হয়েছে যেমনটি Listing 12-19-এ ছিল:

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub 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();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    Ok(())
}

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));
    }
}

আমরা iterator adapter method ব্যবহার করে এই code-টিকে আরও concise উপায়ে লিখতে পারি। এটি আমাদের একটি mutable intermediate results vector থাকাও এড়াতে দেয়। Functional programming style code-কে আরও clear করতে mutable state-এর পরিমাণ minimize করা prefer করে। Mutable state সরিয়ে দেওয়া future-এ searching-কে parallel-এ ঘটানোর enhancement enable করতে পারে, কারণ আমাদের results vector-এ concurrent access manage করতে হবে না। Listing 13-22 এই পরিবর্তনটি দেখায়:

use std::env;
use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub 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,
        })
    }
}

pub 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(())
}

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-এর সমস্ত line return করা যেগুলোতে query রয়েছে। Listing 13-16-এর filter উদাহরণের মতোই, এই code টি শুধুমাত্র সেই line গুলো রাখতে filter adapter ব্যবহার করে যেগুলোর জন্য line.contains(query) true return করে। তারপর আমরা matching line গুলোকে collect-এর সাহায্যে অন্য একটি vector-এ collect করি। অনেক সহজ! Search_case_insensitive ফাংশনেও iterator method ব্যবহার করতে একই পরিবর্তন করতে পারেন।

Loop বা Iterator-এর মধ্যে বেছে নেওয়া

পরবর্তী logical প্রশ্ন হল আপনার নিজের code-এ আপনার কোন style বেছে নেওয়া উচিত এবং কেন: Listing 13-21-এর original implementation নাকি Listing 13-22-এ iterator ব্যবহার করা version। বেশিরভাগ Rust programmer iterator style ব্যবহার করা prefer করেন। এটি প্রথমে আয়ত্ত করা একটু কঠিন, কিন্তু একবার আপনি বিভিন্ন iterator adapter এবং সেগুলো কী করে সে সম্পর্কে ধারণা পেলে, iterator গুলো বুঝতে সহজ হতে পারে। Looping-এর বিভিন্ন অংশ নিয়ে ঘাঁটাঘাঁটি করার এবং new vector তৈরি করার পরিবর্তে, code loop-এর high-level objective-এর উপর focus করে। এটি কিছু commonplace code-কে abstract করে যাতে এই code-এর unique concept গুলো দেখা সহজ হয়, যেমন iterator-এর প্রতিটি element-কে যে filtering condition টি pass করতে হবে।

কিন্তু দুটি implementation কি truly equivalent? স্বজ্ঞাত অনুমান হতে পারে যে আরও low-level loop টি দ্রুততর হবে। আসুন performance নিয়ে কথা বলি।