توابع پیشرفته و Closureها

این بخش به بررسی برخی از ویژگی‌های پیشرفته مربوط به توابع و Closureها می‌پردازد، از جمله Pointerهای تابع و بازگرداندن Closureها.

Pointerهای تابع

قبلاً در مورد چگونگی ارسال Closureها به توابع صحبت کردیم؛ شما همچنین می‌توانید توابع معمولی را به توابع دیگر ارسال کنید! این تکنیک زمانی مفید است که بخواهید تابعی که قبلاً تعریف کرده‌اید را ارسال کنید به جای اینکه یک Closureها جدید تعریف کنید. توابع به نوع fn (با f کوچک) تبدیل می‌شوند، که نباید با ویژگی Closureها Fn اشتباه گرفته شود. نوع fn به عنوان یک اشاره‌گر (Pointer) تابع شناخته می‌شود. ارسال توابع با استفاده از Pointerهای تابع به شما این امکان را می‌دهد که از توابع به عنوان آرگومان برای توابع دیگر استفاده کنید.

سینتکس مشخص کردن اینکه یک پارامتر یک اشاره‌گر (Pointer) تابع است، مشابه Closureها است، همان‌طور که در لیست ۲۰-۲۸ نشان داده شده است. در این مثال، تابعی به نام add_one تعریف کرده‌ایم که یک واحد به پارامتر خود اضافه می‌کند. تابع do_twice دو پارامتر می‌گیرد: یک اشاره‌گر (Pointer) تابع به هر تابعی که یک پارامتر i32 بگیرد و یک مقدار i32 برگرداند، و یک مقدار i32. تابع do_twice تابع f را دو بار فراخوانی می‌کند، مقدار arg را به آن می‌فرستد و سپس نتایج دو فراخوانی را با هم جمع می‌کند. تابع main تابع do_twice را با آرگومان‌های add_one و 5 فراخوانی می‌کند.

Filename: src/main.rs
fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}
Listing 20-28: استفاده از نوع fn برای پذیرش یک اشاره‌گر (Pointer) تابع به عنوان آرگومان

این کد مقدار The answer is: 12 را چاپ می‌کند. ما مشخص کرده‌ایم که پارامتر f در do_twice یک fn است که یک پارامتر از نوع i32 می‌گیرد و یک i32 باز می‌گرداند. سپس می‌توانیم f را در بدنه تابع do_twice فراخوانی کنیم. در main، می‌توانیم نام تابع add_one را به عنوان آرگومان اول به do_twice ارسال کنیم.

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

Pointerهای تابع تمام سه ویژگی Closureها (Fn، FnMut، و FnOnce) را پیاده‌سازی می‌کنند، به این معنی که شما همیشه می‌توانید یک اشاره‌گر (Pointer) تابع را به عنوان آرگومان برای یک تابع که انتظار یک Closureها را دارد ارسال کنید. بهتر است توابع را با استفاده از یک نوع جنریک و یکی از ویژگی‌های Closureها بنویسید تا توابع شما بتوانند هم توابع و هم Closureها را بپذیرند.

با این حال، یک مثال از جایی که ممکن است بخواهید فقط fn را بپذیرید و نه Closureها زمانی است که با کد خارجی که Closureها ندارد تعامل می‌کنید: توابع C می‌توانند توابع را به عنوان آرگومان بپذیرند، اما C Closureها ندارد.

به عنوان مثالی از جایی که می‌توانید از یک Closureها تعریف‌شده درون‌خطی یا یک تابع نام‌گذاری‌شده استفاده کنید، بیایید به استفاده از متد map که توسط ویژگی Iterator در کتابخانه استاندارد ارائه شده است نگاهی بیندازیم. برای استفاده از تابع map برای تبدیل یک بردار اعداد به یک بردار رشته‌ها، می‌توانیم از یک Closureها به این صورت استفاده کنیم:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}

یا می‌توانیم به جای کلوزر، نام یک تابع را به عنوان آرگومان به map ارسال کنیم، به این صورت:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}

توجه داشته باشید که باید از سینتکس کاملاً مشخصی که قبلاً در بخش “ویژگی‌های پیشرفته” توضیح داده شد استفاده کنیم، زیرا چندین تابع با نام to_string در دسترس هستند. در اینجا، ما از تابع to_string که در ویژگی ToString تعریف شده است استفاده می‌کنیم، که کتابخانه استاندارد برای هر نوعی که ویژگی Display را پیاده‌سازی کند، آن را پیاده‌سازی کرده است.

به یاد بیاورید که در بخش “مقادیر Enum” از فصل ۶ گفته شد که نام هر واریانت enum که تعریف می‌کنیم، همچنین به یک تابع مقداردهی اولیه تبدیل می‌شود. می‌توانیم از این توابع مقداردهی اولیه به عنوان اشاره‌گر (Pointer)های تابع که ویژگی‌های کلوزر را پیاده‌سازی می‌کنند استفاده کنیم، به این معنی که می‌توانیم توابع مقداردهی اولیه را به عنوان آرگومان برای متدهایی که کلوزرها (closures) را می‌پذیرند مشخص کنیم، به این صورت:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}

در اینجا با استفاده از تابع مقداردهی اولیه Status::Value، نمونه‌هایی از Status::Value ایجاد می‌کنیم که از هر مقدار u32 در محدوده‌ای که map روی آن فراخوانی می‌شود استفاده می‌کند. برخی افراد این سبک را ترجیح می‌دهند و برخی دیگر ترجیح می‌دهند از کلوزرها (closures) استفاده کنند. این‌ها به کدی یکسان کامپایل می‌شوند، بنابراین هر سبکی که برای شما واضح‌تر است را انتخاب کنید.

بازگرداندن کلوزرها (closures) (Returning Closures)

کلوزرها (closures) با ویژگی‌ها نمایش داده می‌شوند، به این معنی که نمی‌توانید مستقیماً کلوزرها (closures) را بازگردانید. در بیشتر مواردی که ممکن است بخواهید یک ویژگی را بازگردانید، می‌توانید به جای آن از نوع مشخصی که ویژگی را پیاده‌سازی می‌کند به عنوان مقدار بازگشتی تابع استفاده کنید. با این حال، نمی‌توانید این کار را با کلوزرها (closures) انجام دهید زیرا آن‌ها نوع مشخصی که قابل بازگشت باشد ندارند؛ به عنوان مثال، نمی‌توانید از اشاره‌گر (Pointer) تابع fn به عنوان نوع بازگشتی استفاده کنید.

در عوض، معمولاً از سینتکس impl Trait که در فصل ۱۰ یاد گرفتیم استفاده می‌کنید. می‌توانید هر نوع تابعی را با استفاده از Fn، FnOnce و FnMut بازگردانید. برای مثال، این کد به خوبی کار می‌کند:

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

با این حال، همان‌طور که در بخش “استنتاج نوع کلوزر و حاشیه‌نویسی” از فصل ۱۳ اشاره کردیم، هر کلوزر نوع مشخص خود را دارد. اگر نیاز داشته باشید با چندین تابع که امضای یکسانی دارند اما پیاده‌سازی‌های متفاوتی دارند کار کنید، باید از یک شیء ویژگی (trait object) برای آن‌ها استفاده کنید:

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + init)
}

این کد به خوبی کامپایل می‌شود—اما اگر تلاش می‌کردیم از impl Fn(i32) -> i32 استفاده کنیم، کامپایل نمی‌شد. برای اطلاعات بیشتر در مورد اشیاء ویژگی، به بخش “استفاده از اشیاء ویژگی که امکان مقادیر با تایپ‌های مختلف را فراهم می‌کنند” در فصل ۱۸ مراجعه کنید.

در ادامه، بیایید نگاهی به ماکروها بیندازیم!