Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

আমাদের 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) লুপটি দ্রুততর হবে। আসুন পারফরম্যান্স নিয়ে কথা বলি।