مقدمه

در دو درس گذشته با کلوژرها و تکرارگرها به عنوان دو ویژگی قدرتمند برنامه‌نویسی تابعی در Rust آشنا شدیم. اکنون زمان آن است که این مفاهیم را در پروژه minigrep که در فصل قبل ساختیم، به کار بگیریم. ما با بازسازی کد فعلی، نشان خواهیم داد که چگونه استفاده از کلوژرها و تکرارگرها می‌تواند کد ما را نه تنها مختصرتر و گویاتر، بلکه اصولی‌تر و نزدیک‌تر به سبک ایده‌آل برنامه‌نویسی در Rust کند.

جداسازی منطق از main

در حال حاضر، منطق اصلی برنامه ما بین دو تابع main و run تقسیم شده است. تابع main مسئولیت مدیریت پیکربندی و خطاها را بر عهده دارد و تابع run منطق اصلی را اجرا می‌کند. این یک جداسازی خوب است. بیایید این ساختار را به یک کتابخانه و یک فایل اجرایی مجزا منتقل کنیم تا ماژولاریتی کد را افزایش دهیم.

تمام منطق برنامه را (شامل ساختار Config و تابع run) از src/main.rs به یک فایل جدید به نام src/lib.rs منتقل می‌کنیم. سپس تابع main را به شکل زیر ساده‌سازی می‌کنیم:

Copy Icon src/main.rs
use std::{env, process};
use minigrep::Config;

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = minigrep::run(config) {
        eprintln!("Application error: {e}");
        process::exit(1);
    }
}

توجه کنید که به جای دریافت آرگومان‌ها و تبدیل آنها به وکتور، ما مستقیماً تکرارگر بازگشتی از env::args() را به تابع Config::build پاس می‌دهیم. این اولین قدم ما برای استفاده بهتر از تکرارگرهاست.

بهبود Config::build با تکرارگرها

حالا بیایید تابع Config::build را بازسازی کنیم تا به جای یک اسلایس (&[String])، خود تکرارگر را بپذیرد. این کار نه تنها بهینه‌تر است (چون از ساخت یک وکتور میانی جلوگیری می‌کند) بلکه به ما اجازه می‌دهد تا منطق پردازش آرگومان‌ها را با استفاده از متدهای تکرارگر بنویسیم.

Copy Icon src/lib.rs
impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next(); // Skip the program name

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

در این نسخه، ما به جای ایندکس‌گذاری روی یک وکتور، مستقیماً از متد next روی تکرارگر آرگومان‌ها استفاده می‌کنیم. این روش بسیار نزدیک‌تر به سبک اصولی Rust است. ما از impl Iterator<Item=String> به عنوان نوع پارامتر استفاده کرده‌ایم که یک استفاده زیبا از Traitها و جنریک‌ها برای پذیرش هر نوعی است که Iterator را برای Stringها پیاده‌سازی کرده باشد.

بازسازی منطق جستجو با تکرارگرها

در نهایت، می‌توانیم توابع search خود را نیز بازسازی کنیم تا به جای حلقه‌های for دستی، از زنجیره‌ی متدهای تکرارگر استفاده کنند.

Copy Icon src/lib.rs
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();
    contents
        .lines()
        .filter(|line| line.to_lowercase().contains(&query))
        .collect()
}

این نسخه جدید بسیار مختصرتر و گویاتر است. ما به صورت زنجیره‌ای ابتدا با lines یک تکرارگر روی خطوط متن ایجاد می‌کنیم، سپس با filter و یک کلوژر، خطوط مورد نظر را انتخاب کرده و در نهایت با collect، نتایج را در یک وکتور جدید جمع‌آوری می‌کنیم. این کد به وضوح بیان می‌کند که ما چه کاری می‌خواهیم انجام دهیم، به جای اینکه چگونه باید آن را انجام دهیم.

در این درس، با به کارگیری کلوژرها و تکرارگرها، کد پروژه minigrep خود را به شکل قابل توجهی بهبود دادیم. این بازسازی نه تنها کد را کوتاه‌تر و خواناتر کرد، بلکه آن را به سبک اصولی و تابعی Rust نزدیک‌تر کرد. در درس پایانی این فصل، به یک مقایسه جالب بین عملکرد حلقه‌های for و تکرارگرها خواهیم پرداخت و خواهیم دید که آیا این انتزاع سطح بالا هزینه‌ی عملکردی دارد یا خیر.