مقدمه
به فصل «ویژگیهای Functional در Rust» خوش آمدید. Rust یک زبان چند-پارادایمی است و علاوه بر
مفاهیم قدرتمندی که از برنامهنویسی سیستمی به ارث برده، به شدت تحت تأثیر پارادایم برنامهنویسی
تابعی (Functional Programming) نیز قرار دارد. ویژگیهایی مانند توابع درجه یک (first-class
functions)، کلوژرها و تکرارگرها به ما اجازه میدهند تا کدهایی گویاتر، مختصرتر و با انتزاع
بالاتری بنویسیم. در این فصل، به بررسی این ویژگیها و نحوه استفاده از آنها برای نوشتن کدهای
اصولیتر در Rust میپردازیم.
کلوژرها: توابع ناشناس با قابلیت به ارث بردن محیط
«کلوژر» یا Closure یک تابع ناشناس (anonymous function) است که میتوانید آن را در یک متغیر
ذخیره کرده، به عنوان آرگومان به توابع دیگر پاس دهید، و مهمتر از همه، میتواند متغیرهایی را از
محیطی که در آن تعریف شده، به ارث برده و استفاده کند (capture its environment).
سینتکس تعریف کلوژر
سینتکس تعریف یک کلوژر کمی با توابع عادی متفاوت است. پارامترها در داخل دو خط عمودی (| |) قرار
گرفته و بدنه نیز در داخل آکولاد {} (یا به صورت یک عبارت تنها) میآید.
src/main.rs
use std::thread;
use std::time::Duration;
fn main() {
let intensity = 10;
let random_number = 7;
generate_workout(intensity, random_number);
}
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!("Today, run for {} minutes!", expensive_closure(intensity));
}
}
}
در این مثال، ما یک کلوژر به نام expensive_closure تعریف کردهایم که یک محاسبه شبیهسازی
شده و زمانبر را انجام میدهد. برخلاف توابع، کامپایلر Rust معمولاً نیازی به تعریف صریح نوع
پارامترها و مقدار بازگشتی برای کلوژرها ندارد و میتواند آنها را از روی اولین فراخوانی استنتاج
کند. با این حال، افزودن آنها برای خوانایی بیشتر توصیه میشود.
به ارث بردن مقادیر از محیط
ویژگی اصلی کلوژرها، توانایی آنها در به ارث بردن مقادیر از حوزهای است که در آن تعریف شدهاند.
این کار میتواند به سه روش انجام شود که با Traitهای Fn، FnMut، و i
FnOnce مشخص میشوند.
- FnOnce: مالکیت متغیرهای به ارث برده شده را به خود منتقل میکند (move). این کلوژر فقط
یک بار قابل فراخوانی است.
- FnMut: متغیرها را به صورت تغییرپذیر قرض میگیرد (mutable borrow).
- Fn: متغیرها را به صورت تغییرناپذیر قرض میگیرد (immutable borrow).
وقتی یک کلوژر تعریف میکنیم، کامپایلر به صورت خودکار تشخیص میدهد که کدام یک از این Traitها
باید پیادهسازی شود.
src/main.rs
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = |z| z == x;
println!("can still use x: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
در این مثال، کلوژر equal_to_x یک رفرنس تغییرناپذیر به وکتور x را به ارث میبرد. به
همین دلیل، ما همچنان میتوانیم بعد از تعریف کلوژر، از متغیر x استفاده کنیم.
اگر بخواهیم کلوژر مالکیت را به خود منتقل کند، میتوانیم از کلمه کلیدی move قبل از لیست
پارامترها استفاده کنیم. این کار زمانی مفید است که بخواهیم یک کلوژر را به یک ریسمان (thread) دیگر
منتقل کنیم.
کلوژرها در عمل
کلوژرها به خصوص در ترکیب با متدهای کتابخانه استاندارد که تکرارگرها (iterators) را پردازش
میکنند، بسیار قدرتمند هستند. متدهایی مانند map، filter و find همگی یک کلوژر را به عنوان
آرگومان میگیرند تا منطق سفارشی را روی هر عنصر از یک کالکشن اعمال کنند.
src/main.rs
fn main() {
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
}
در اینجا، متد map یک کلوژر دریافت میکند که روی هر عنصر از وکتور اعمال شده و آن را به
اضافه یک میکند. این سبک از برنامهنویسی بسیار گویاتر و مختصرتر از نوشتن یک حلقه for به
صورت دستی است.
در این درس با کلوژرها به عنوان توابع ناشناسی که میتوانند محیط اطراف خود را به ارث ببرند، آشنا
شدیم. دیدیم که چگونه میتوان آنها را تعریف، در متغیرها ذخیره، و به عنوان آرگومان به توابع دیگر
پاس داد. کلوژرها یکی از ارکان اصلی برنامهنویسی تابعی در Rust هستند. در درس بعدی، به سراغ
«کاربرد تکرارگرها یا Iterators» خواهیم رفت و خواهیم دید که چگونه ترکیب آنها با کلوژرها، الگوهای
پردازش داده بسیار قدرتمند و گویایی را ایجاد میکند.