مقدمه
در دو درس گذشته با کلوژرها و تکرارگرها به عنوان دو ویژگی قدرتمند برنامهنویسی تابعی در Rust آشنا
شدیم. اکنون زمان آن است که این مفاهیم را در پروژه minigrep که در فصل قبل ساختیم، به کار
بگیریم. ما با بازسازی کد فعلی، نشان خواهیم داد که چگونه استفاده از کلوژرها و تکرارگرها میتواند
کد ما را نه تنها مختصرتر و گویاتر، بلکه اصولیتر و نزدیکتر به سبک ایدهآل برنامهنویسی در Rust
کند.
جداسازی منطق از main
در حال حاضر، منطق اصلی برنامه ما بین دو تابع main و run تقسیم شده است. تابع
main مسئولیت مدیریت پیکربندی و خطاها را بر عهده دارد و تابع run منطق اصلی را اجرا
میکند. این یک جداسازی خوب است. بیایید این ساختار را به یک کتابخانه و یک فایل اجرایی مجزا منتقل
کنیم تا ماژولاریتی کد را افزایش دهیم.
تمام منطق برنامه را (شامل ساختار Config و تابع run) از src/main.rs به یک
فایل جدید به نام src/lib.rs منتقل میکنیم. سپس تابع main را به شکل زیر سادهسازی
میکنیم:
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])، خود تکرارگر
را بپذیرد. این کار نه تنها بهینهتر است (چون از ساخت یک وکتور میانی جلوگیری میکند) بلکه به ما
اجازه میدهد تا منطق پردازش آرگومانها را با استفاده از متدهای تکرارگر بنویسیم.
src/lib.rs
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 })
}
}
در این نسخه، ما به جای ایندکسگذاری روی یک وکتور، مستقیماً از متد next روی تکرارگر
آرگومانها
استفاده میکنیم. این روش بسیار نزدیکتر به سبک اصولی Rust است. ما از impl
Iterator<Item=String> به عنوان نوع پارامتر استفاده کردهایم که یک استفاده زیبا
از Traitها و جنریکها برای
پذیرش هر نوعی است که Iterator را برای Stringها پیادهسازی کرده باشد.
بازسازی منطق جستجو با تکرارگرها
در نهایت، میتوانیم توابع search خود را نیز بازسازی کنیم تا به جای حلقههای for
دستی،
از زنجیرهی متدهای تکرارگر استفاده کنند.
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 و
تکرارگرها خواهیم پرداخت و خواهیم دید که آیا این انتزاع سطح بالا هزینهی عملکردی دارد یا خیر.