مقدمه

به فصل «ویژگی‌های Functional در Rust» خوش آمدید. Rust یک زبان چند-پارادایمی است و علاوه بر مفاهیم قدرتمندی که از برنامه‌نویسی سیستمی به ارث برده، به شدت تحت تأثیر پارادایم برنامه‌نویسی تابعی (Functional Programming) نیز قرار دارد. ویژگی‌هایی مانند توابع درجه یک (first-class functions)، کلوژرها و تکرارگرها به ما اجازه می‌دهند تا کدهایی گویاتر، مختصرتر و با انتزاع بالاتری بنویسیم. در این فصل، به بررسی این ویژگی‌ها و نحوه استفاده از آنها برای نوشتن کدهای اصولی‌تر در Rust می‌پردازیم.

کلوژرها: توابع ناشناس با قابلیت به ارث بردن محیط

«کلوژر» یا Closure یک تابع ناشناس (anonymous function) است که می‌توانید آن را در یک متغیر ذخیره کرده، به عنوان آرگومان به توابع دیگر پاس دهید، و مهم‌تر از همه، می‌تواند متغیرهایی را از محیطی که در آن تعریف شده، به ارث برده و استفاده کند (capture its environment).

سینتکس تعریف کلوژر

سینتکس تعریف یک کلوژر کمی با توابع عادی متفاوت است. پارامترها در داخل دو خط عمودی (| |) قرار گرفته و بدنه نیز در داخل آکولاد {} (یا به صورت یک عبارت تنها) می‌آید.

Copy Icon 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) {
    // Define a closure and store it in a variable
    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ها باید پیاده‌سازی شود.

Copy Icon src/main.rs
fn main() {
    let x = vec![1, 2, 3];

    // This closure immutably borrows `x`
    let equal_to_x = |z| z == x;

    // We can still use `x` here because it was only borrowed.
    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 همگی یک کلوژر را به عنوان آرگومان می‌گیرند تا منطق سفارشی را روی هر عنصر از یک کالکشن اعمال کنند.

Copy Icon 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» خواهیم رفت و خواهیم دید که چگونه ترکیب آنها با کلوژرها، الگوهای پردازش داده بسیار قدرتمند و گویایی را ایجاد می‌کند.