Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ক্লোজার (Closures): অ্যানোনিমাস ফাংশন যা তার এনভায়রনমেন্ট ক্যাপচার করতে পারে

রাস্টের ক্লোজার হলো অ্যানোনিমাস ফাংশন (anonymous functions) যা আপনি একটি ভ্যারিয়েবলে সংরক্ষণ করতে পারেন বা অন্য ফাংশনে আর্গুমেন্ট হিসেবে পাস করতে পারেন। আপনি এক জায়গায় ক্লোজার তৈরি করে পরে অন্য কোনো কনটেক্সটে (context) কল করে তাকে এক্সিকিউট করতে পারেন। সাধারণ ফাংশনের মতো নয়, ক্লোজারগুলো যে স্কোপে (scope) তৈরি হয়, সেই স্কোপের ভ্যালু ক্যাপচার করতে পারে। আমরা দেখাব কীভাবে ক্লোজারের এই বৈশিষ্ট্যগুলো কোড পুনঃব্যবহার (reuse) এবং আচরণ কাস্টমাইজ (behavior customization) করার সুযোগ করে দেয়।

ক্লোজার দিয়ে এনভায়রনমেন্ট ক্যাপচার করা

আমরা প্রথমে দেখব কীভাবে ক্লোজার ব্যবহার করে তাদের 정의কৃত এনভায়রনমেন্ট থেকে মান ক্যাপচার করা যায় এবং পরে ব্যবহার করা যায়। দৃশ্যপটটি এরকম: আমাদের টি-শার্ট কোম্পানি মাঝে মাঝে প্রচারের অংশ হিসেবে আমাদের মেইলিং লিস্টের কাউকে একটি এক্সক্লুসিভ, লিমিটেড-এডিশন শার্ট উপহার দেয়। মেইলিং লিস্টের সদস্যরা চাইলে তাদের প্রোফাইলে তাদের প্রিয় রঙ যোগ করতে পারেন। যদি বিনামূল্যে শার্টের জন্য নির্বাচিত ব্যক্তির পছন্দের রঙ সেট করা থাকে, তবে তিনি সেই রঙের শার্ট পাবেন। আর যদি তিনি পছন্দের রঙ উল্লেখ না করে থাকেন, তবে কোম্পানি যে রঙের শার্ট সবচেয়ে বেশি স্টক করেছে, সেটি পাবেন।

এটি বিভিন্ন উপায়ে প্রয়োগ করা যেতে পারে। এই উদাহরণের জন্য, আমরা ShirtColor নামে একটি enum ব্যবহার করব, যার দুটি ভ্যারিয়েন্ট থাকবে: Red এবং Blue (সহজবোধ্যতার জন্য রঙের সংখ্যা সীমিত রাখা হয়েছে)। কোম্পানির ইনভেন্টরিকে আমরা একটি Inventory struct দিয়ে প্রকাশ করছি, যার shirts নামে একটি ফিল্ড আছে। এই ফিল্ডটিতে Vec<ShirtColor> রয়েছে, যা বর্তমানে স্টকে থাকা শার্টের রঙগুলোকে উপস্থাপন করে। Inventory struct-এর উপর giveaway নামে একটি মেথড ডিফাইন করা হয়েছে, যা বিনামূল্যে শার্ট বিজয়ীর পছন্দের রঙের (যদি থাকে) অপশনাল মানটি নেয় এবং ব্যবহারকারী কোন রঙের শার্টটি পাবেন তা রিটার্ন করে। এই সেটআপটি Listing 13-1-এ দেখানো হয়েছে।

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}

main ফাংশনে ডিফাইন করা store-এ এই লিমিটেড-এডিশন প্রোমোশনের জন্য দুটি নীল এবং একটি লাল শার্ট অবশিষ্ট আছে। আমরা giveaway মেথডটি একজন ব্যবহারকারীর জন্য কল করি যার পছন্দের রঙ লাল এবং আরেকজন ব্যবহারকারীর জন্য যার কোনো পছন্দের রঙ নেই।

আবারও বলি, এই কোডটি অনেক উপায়ে প্রয়োগ করা যেতে পারে। এখানে, ক্লোজারের উপর মনোযোগ কেন্দ্রীভূত করার জন্য, আমরা কেবল সেই ধারণাগুলো ব্যবহার করেছি যা আপনি ইতিমধ্যে শিখেছেন, শুধুমাত্র giveaway মেথডের বডি ছাড়া, যেখানে একটি ক্লোজার ব্যবহৃত হয়েছে। giveaway মেথডে, আমরা ব্যবহারকারীর পছন্দকে Option<ShirtColor> টাইপের একটি প্যারামিটার হিসাবে গ্রহণ করি এবং user_preference-এর উপর unwrap_or_else মেথডটি কল করি। Option<T>-এর unwrap_or_else মেথডটি স্ট্যান্ডার্ড লাইব্রেরি দ্বারা ডিফাইন করা। এটি একটি আর্গুমেন্ট নেয়: একটি ক্লোজার যার কোনো আর্গুমেন্ট নেই এবং এটি T টাইপের একটি ভ্যালু রিটার্ন করে (এই ক্ষেত্রে ShirtColor, যা Option<T>-এর Some ভ্যারিয়েন্টে সংরক্ষিত টাইপের সমান)। যদি Option<T>-টি Some ভ্যারিয়েন্ট হয়, unwrap_or_else সেই Some-এর ভেতরের ভ্যালুটি রিটার্ন করে। যদি Option<T>-টি None ভ্যারিয়েন্ট হয়, unwrap_or_else ক্লোজারটিকে কল করে এবং ক্লোজারের রিটার্ন করা ভ্যালুটি রিটার্ন করে।

আমরা unwrap_or_else-এর আর্গুমেন্ট হিসেবে || self.most_stocked() ক্লোজার এক্সপ্রেশনটি উল্লেখ করেছি। এটি এমন একটি ক্লোজার যা নিজে কোনো প্যারামিটার নেয় না (যদি ক্লোজারের প্যারামিটার থাকত, তবে সেগুলি দুটি ভার্টিকেল পাইপের মধ্যে থাকত)। ক্লোজারের বডি self.most_stocked()-কে কল করে। আমরা এখানে ক্লোজারটি ডিফাইন করছি, এবং unwrap_or_else-এর ইমপ্লিমেন্টেশন পরে প্রয়োজন হলে ক্লোজারটিকে মূল্যায়ন করবে।

এই কোডটি রান করলে নিম্নলিখিত আউটপুট প্রিন্ট হবে:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

এখানে একটি আকর্ষণীয় দিক হলো, আমরা এমন একটি ক্লোজার পাস করেছি যা বর্তমান Inventory ইনস্ট্যান্সের উপর self.most_stocked() মেথডটিকে কল করে। স্ট্যান্ডার্ড লাইব্রেরির আমাদের ডিফাইন করা Inventory বা ShirtColor টাইপ সম্পর্কে বা এই পরিস্থিতিতে আমরা যে লজিক ব্যবহার করতে চাই সে সম্পর্কে কিছুই জানার প্রয়োজন ছিল না। ক্লোজারটি self Inventory ইনস্ট্যান্সের একটি ইমিউটেবল রেফারেন্স (immutable reference) ক্যাপচার করে এবং আমাদের নির্দিষ্ট করা কোডের সাথে unwrap_or_else মেথডে পাস করে। অন্যদিকে, ফাংশনগুলো এইভাবে তাদের এনভায়রনমেন্ট ক্যাপচার করতে পারে না।

ক্লোজারের টাইপ ইনফারেন্স এবং অ্যানোটেশন

ফাংশন এবং ক্লোজারের মধ্যে আরও কিছু পার্থক্য রয়েছে। ক্লোজারের ক্ষেত্রে সাধারণত আপনাকে fn ফাংশনের মতো প্যারামিটারের টাইপ বা রিটার্ন ভ্যালুর টাইপ অ্যানোটেট (annotate) করতে হয় না। ফাংশনের উপর টাইপ অ্যানোটেশন প্রয়োজন কারণ টাইপগুলো আপনার ব্যবহারকারীদের কাছে প্রকাশিত একটি সুস্পষ্ট ইন্টারফেসের (explicit interface) অংশ। একটি ফাংশন কী ধরনের ভ্যালু ব্যবহার করে এবং রিটার্ন করে সে বিষয়ে সবাই যাতে একমত হয়, তা নিশ্চিত করার জন্য এই ইন্টারফেসটিকে কঠোরভাবে ডিফাইন করা গুরুত্বপূর্ণ। অন্যদিকে, ক্লোজারগুলো এমন কোনো প্রকাশিত ইন্টারফেসে ব্যবহৃত হয় না: এগুলি ভ্যারিয়েবলে সংরক্ষণ করা হয় এবং নাম না দিয়ে এবং আমাদের লাইব্রেরির ব্যবহারকারীদের কাছে প্রকাশ না করেই ব্যবহার করা হয়।

ক্লোজারগুলো সাধারণত সংক্ষিপ্ত হয় এবং যেকোনো নির্বিচার পরিস্থিতির পরিবর্তে শুধুমাত্র একটি সংকীর্ণ কনটেক্সটের মধ্যে প্রাসঙ্গিক হয়। এই সীমিত কনটেক্সটগুলোর মধ্যে, কম্পাইলার প্যারামিটারের টাইপ এবং রিটার্ন টাইপ অনুমান (infer) করতে পারে, ঠিক যেমন এটি বেশিরভাগ ভ্যারিয়েবলের টাইপ অনুমান করতে সক্ষম (বিরল ক্ষেত্রে কম্পাইলারেরও ক্লোজার টাইপ অ্যানোটেশন প্রয়োজন হয়)।

ভ্যারিয়েবলের মতোই, আমরা চাইলে টাইপ অ্যানোটেশন যোগ করতে পারি যাতে কোডটি আরও সুস্পষ্ট এবং পরিষ্কার হয়, যদিও এর জন্য কোডটি প্রয়োজনের চেয়ে বেশি ভার্বোস (verbose) হয়ে যায়। একটি ক্লোজারের জন্য টাইপ অ্যানোটেট করা Listing 13-2-এ দেখানো সংজ্ঞার মতো দেখাবে। এই উদাহরণে, আমরা একটি ক্লোজার ডিফাইন করে সেটিকে একটি ভ্যারিয়েবলে সংরক্ষণ করছি, Listing 13-1-এর মতো আর্গুমেন্ট হিসাবে পাস করার সময় ডিফাইন করার পরিবর্তে।

use std::thread;
use std::time::Duration;

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

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}

টাইপ অ্যানোটেশন যোগ করার সাথে সাথে ক্লোজারের সিনট্যাক্স ফাংশনের সিনট্যাক্সের সাথে আরও সাদৃশ্যপূর্ণ দেখায়। এখানে, তুলনার জন্য আমরা একটি ফাংশন ডিফাইন করছি যা তার প্যারামিটারে 1 যোগ করে এবং একই আচরণের একটি ক্লোজার ডিফাইন করছি। প্রাসঙ্গিক অংশগুলো মেলানোর জন্য আমরা কিছু স্পেস যোগ করেছি। এটি দেখায় যে ক্লোজারের সিনট্যাক্স ফাংশনের সিনট্যাক্সের মতোই, শুধু পাইপ-এর ব্যবহার এবং সিনট্যাক্সের ঐচ্ছিক অংশগুলোর পরিমাণ ছাড়া:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

প্রথম লাইনে একটি ফাংশন ডেফিনিশন এবং দ্বিতীয় লাইনে একটি সম্পূর্ণ অ্যানোটেটেড ক্লোজার ডেফিনিশন দেখানো হয়েছে। তৃতীয় লাইনে, আমরা ক্লোজার ডেফিনিশন থেকে টাইপ অ্যানোটেশনগুলো সরিয়ে দিয়েছি। চতুর্থ লাইনে, আমরা ব্র্যাকেটগুলো সরিয়ে দিয়েছি, যা ঐচ্ছিক কারণ ক্লোজারের বডিতে শুধুমাত্র একটি এক্সপ্রেশন আছে। এগুলি সবই বৈধ ডেফিনিশন যা কল করা হলে একই আচরণ তৈরি করবে। add_one_v3 এবং add_one_v4 লাইনগুলোর জন্য ক্লোজারগুলোকে মূল্যায়ন করা প্রয়োজন যাতে কম্পাইল করা যায়, কারণ টাইপগুলো তাদের ব্যবহার থেকে অনুমান করা হবে। এটি let v = Vec::new();-এর মতো, যেখানে রাস্টের টাইপ অনুমান করতে পারার জন্য হয় টাইপ অ্যানোটেশন প্রয়োজন অথবা Vec-এর মধ্যে কোনো টাইপের ভ্যালু ঢোকানো প্রয়োজন।

ক্লোজার ডেফিনিশনের জন্য, কম্পাইলার প্রতিটি প্যারামিটার এবং তাদের রিটার্ন ভ্যালুর জন্য একটি করে কনক্রিট টাইপ (concrete type) অনুমান করবে। উদাহরণস্বরূপ, Listing 13-3 একটি সংক্ষিপ্ত ক্লোজারের ডেফিনিশন দেখায় যা কেবল তার প্যারামিটার হিসাবে প্রাপ্ত ভ্যালুটি রিটার্ন করে। এই ক্লোজারটি এই উদাহরণের উদ্দেশ্য ছাড়া তেমন কার্যকর নয়। লক্ষ্য করুন যে আমরা ডেফিনিশনে কোনো টাইপ অ্যানোটেশন যোগ করিনি। যেহেতু কোনো টাইপ অ্যানোটেশন নেই, তাই আমরা যেকোনো টাইপ দিয়ে ক্লোজারটিকে কল করতে পারি, যা আমরা এখানে প্রথমবার String দিয়ে করেছি। যদি আমরা এরপর example_closure-কে একটি ইন্টিজার (integer) দিয়ে কল করার চেষ্টা করি, তাহলে আমরা একটি এরর পাব।

fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}

কম্পাইলার আমাদের এই এররটি দেয়:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected `String`, found integer
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

প্রথমবার যখন আমরা example_closure-কে String ভ্যালু দিয়ে কল করি, কম্পাইলার x-এর টাইপ এবং ক্লোজারের রিটার্ন টাইপ String হিসাবে অনুমান করে। সেই টাইপগুলো তখন example_closure-এর ক্লোজারে লক হয়ে যায়, এবং আমরা যখন পরবর্তী সময়ে একই ক্লোজারের সাথে একটি ভিন্ন টাইপ ব্যবহার করার চেষ্টা করি তখন একটি টাইপ এরর পাই।

রেফারেন্স ক্যাপচার করা বা মালিকানা মুভ করা

ক্লোজার তাদের এনভায়রনমেন্ট থেকে তিনটি উপায়ে ভ্যালু ক্যাপচার করতে পারে, যা সরাসরি একটি ফাংশনের প্যারামিটার নেওয়ার তিনটি উপায়ের সাথে মিলে যায়: ইমিউটেবলভাবে ধার করা (borrowing immutably), মিউটেবলভাবে ধার করা (borrowing mutably), এবং মালিকানা নেওয়া (taking ownership)। ক্লোজারের ফাংশন বডি ক্যাপচার করা ভ্যালুগুলো দিয়ে কী করে তার উপর ভিত্তি করে ক্লোজার সিদ্ধান্ত নেবে কোনটি ব্যবহার করতে হবে।

Listing 13-4-এ, আমরা একটি ক্লোজার ডিফাইন করেছি যা list নামের ভেক্টরের একটি ইমিউটেবল রেফারেন্স ক্যাপচার করে কারণ এটির কেবল ভ্যালু প্রিন্ট করার জন্য একটি ইমিউটেবল রেফারেন্স প্রয়োজন।

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}

এই উদাহরণটি আরও দেখায় যে একটি ভ্যারিয়েবল একটি ক্লোজার ডেফিনিশনে বাইন্ড হতে পারে, এবং আমরা পরে ভ্যারিয়েবলের নাম এবং প্যারেনথেসিস ব্যবহার করে ক্লোজারটিকে কল করতে পারি যেন ভ্যারিয়েবলের নামটি একটি ফাংশনের নাম।

যেহেতু আমরা একই সময়ে list-এর একাধিক ইমিউটেবল রেফারেন্স রাখতে পারি, তাই list ক্লোজার ডেফিনিশনের আগে, ক্লোজার ডেফিনিশনের পরে কিন্তু কল করার আগে, এবং ক্লোজার কল করার পরেও অ্যাক্সেসযোগ্য। এই কোডটি কম্পাইল হয়, রান করে এবং প্রিন্ট করে:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

এরপরে, Listing 13-5-এ, আমরা ক্লোজারের বডি পরিবর্তন করে list ভেক্টরে একটি এলিমেন্ট যোগ করি। ক্লোজারটি এখন একটি মিউটেবল রেফারেন্স ক্যাপচার করে।

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}

এই কোডটি কম্পাইল হয়, রান করে এবং প্রিন্ট করে:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

লক্ষ্য করুন যে borrows_mutably ক্লোজারের ডেফিনিশন এবং কলের মধ্যে আর কোনো println! নেই: যখন borrows_mutably ডিফাইন করা হয়, তখন এটি list-এর একটি মিউটেবল রেফারেন্স ক্যাপচার করে। ক্লোজারটি কল করার পরে আমরা আর ক্লোজারটি ব্যবহার করি না, তাই মিউটেবল বরো (mutable borrow) শেষ হয়ে যায়। ক্লোজার ডেফিনিশন এবং ক্লোজার কলের মধ্যে, প্রিন্ট করার জন্য একটি ইমিউটেবল বরো অনুমোদিত নয় কারণ যখন একটি মিউটেবল বরো থাকে তখন অন্য কোনো বরো অনুমোদিত নয়। আপনি কী এরর মেসেজ পান তা দেখতে সেখানে একটি println! যোগ করার চেষ্টা করুন!

আপনি যদি ক্লোজারকে তার এনভায়রনমেন্টে ব্যবহৃত ভ্যালুগুলোর মালিকানা নিতে বাধ্য করতে চান, যদিও ক্লোজারের বডির কঠোরভাবে মালিকানার প্রয়োজন নেই, আপনি প্যারামিটার তালিকার আগে move কীওয়ার্ড ব্যবহার করতে পারেন।

এই কৌশলটি বেশিরভাগ সময় একটি নতুন থ্রেডে (thread) ক্লোজার পাস করার সময় উপযোগী হয়, যাতে ডেটা মুভ করে নতুন থ্রেডের মালিকানাধীন করা যায়। আমরা Chapter 16-এ যখন কনকারেন্সি (concurrency) নিয়ে আলোচনা করব তখন থ্রেড এবং কেন আপনি সেগুলি ব্যবহার করতে চাইবেন সে সম্পর্কে বিস্তারিত আলোচনা করব, কিন্তু আপাতত, move কীওয়ার্ড প্রয়োজন এমন একটি ক্লোজার ব্যবহার করে একটি নতুন থ্রেড স্পন (spawn) করার বিষয়টি সংক্ষেপে অন্বেষণ করা যাক। Listing 13-6, Listing 13-4-কে পরিবর্তন করে দেখায় কীভাবে মেইন থ্রেডের পরিবর্তে একটি নতুন থ্রেডে ভেক্টর প্রিন্ট করা যায়।

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}

আমরা একটি নতুন থ্রেড স্পন করি, এবং থ্রেডটিকে চালানোর জন্য একটি ক্লোজার আর্গুমেন্ট হিসাবে দিই। ক্লোজারের বডি লিস্টটি প্রিন্ট করে। Listing 13-4-এ, ক্লোজারটি শুধুমাত্র একটি ইমিউটেবল রেফারেন্স ব্যবহার করে list ক্যাপচার করেছিল কারণ list প্রিন্ট করার জন্য এটিই সর্বনিম্ন অ্যাক্সেস যা প্রয়োজন ছিল। এই উদাহরণে, যদিও ক্লোজারের বডিকে এখনও শুধুমাত্র একটি ইমিউটেবল রেফারেন্স প্রয়োজন, আমাদের নির্দিষ্ট করতে হবে যে list-কে ক্লোজারের মধ্যে মুভ করা উচিত এবং এর জন্য ক্লোজার ডেফিনিশনের শুরুতে move কীওয়ার্ডটি বসাতে হবে। যদি মেইন থ্রেড নতুন থ্রেডে join কল করার আগে আরও কাজ সম্পাদন করত, তাহলে নতুন থ্রেডটি মেইন থ্রেডের বাকি অংশ শেষ হওয়ার আগে শেষ হতে পারত, অথবা মেইন থ্রেডটি আগে শেষ হতে পারত। যদি মেইন থ্রেড list-এর মালিকানা বজায় রাখত কিন্তু নতুন থ্রেড শেষ হওয়ার আগেই শেষ হয়ে যেত এবং list-কে ড্রপ করে দিত, তাহলে থ্রেডের ইমিউটেবল রেফারেন্সটি অবৈধ হয়ে যেত। অতএব, কম্পাইলারের প্রয়োজন হয় যে list-কে নতুন থ্রেডে দেওয়া ক্লোজারের মধ্যে মুভ করা হোক যাতে রেফারেন্সটি বৈধ থাকে। আপনি কী কম্পাইলার এরর পান তা দেখতে move কীওয়ার্ডটি সরিয়ে দেওয়ার বা ক্লোজার ডিফাইন করার পরে মেইন থ্রেডে list ব্যবহার করার চেষ্টা করুন!

ক্যাপচার করা মান ক্লোজারের বাইরে মুভ করা এবং Fn ট্রেইট

যখন একটি ক্লোজার তার এনভায়রনমেন্ট থেকে একটি রেফারেন্স বা একটি মানের মালিকানা ক্যাপচার করে (যা ক্লোজারের ভিতরে কী মুভ হবে তা প্রভাবিত করে), তখন ক্লোজারের বডির কোড নির্ধারণ করে যে ক্লোজারটি পরে মূল্যায়ন করা হলে সেই রেফারেন্স বা মানগুলোর কী হবে (যা ক্লোজারের বাইরে কী মুভ হবে তা প্রভাবিত করে)।

একটি ক্লোজারের বডি নিম্নলিখিত যেকোনোটি করতে পারে: একটি ক্যাপচার করা মান ক্লোজারের বাইরে মুভ করা, ক্যাপচার করা মান পরিবর্তন করা, মানটি মুভ বা পরিবর্তন না করা, অথবা শুরু থেকেই এনভায়রনমেন্ট থেকে কিছুই ক্যাপচার না করা।

একটি ক্লোজার যেভাবে এনভায়রনমেন্ট থেকে মান ক্যাপচার এবং হ্যান্ডেল করে তা প্রভাবিত করে যে ক্লোজারটি কোন ট্রেইট (trait) ইমপ্লিমেন্ট করবে, এবং ফাংশন ও স্ট্রাকটগুলো ট্রেইটের মাধ্যমেই নির্দিষ্ট করতে পারে যে তারা কোন ধরনের ক্লোজার ব্যবহার করতে পারবে। ক্লোজারগুলো তাদের বডি কীভাবে মান হ্যান্ডেল করে তার উপর নির্ভর করে, এই তিনটি Fn ট্রেইটের মধ্যে একটি, দুটি, বা তিনটিই স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করবে:

  • FnOnce সেইসব ক্লোজারের ক্ষেত্রে প্রযোজ্য যা একবার কল করা যেতে পারে। সমস্ত ক্লোজার অন্তত এই ট্রেইটটি ইমপ্লিমেন্ট করে কারণ সমস্ত ক্লোজার কল করা যায়। একটি ক্লোজার যা তার বডি থেকে ক্যাপচার করা মান মুভ করে ফেলে, সেটি শুধুমাত্র FnOnce ইমপ্লিমেন্ট করবে এবং অন্য কোনো Fn ট্রেইট করবে না, কারণ এটি শুধুমাত্র একবারই কল করা যেতে পারে।
  • FnMut সেইসব ক্লোজারের ক্ষেত্রে প্রযোজ্য যা তাদের বডি থেকে ক্যাপচার করা মান মুভ করে না, কিন্তু ক্যাপচার করা মান পরিবর্তন (mutate) করতে পারে। এই ক্লোজারগুলো একাধিকবার কল করা যেতে পারে।
  • Fn সেইসব ক্লোজারের ক্ষেত্রে প্রযোজ্য যা তাদের বডি থেকে ক্যাপচার করা মান মুভ করে না এবং ক্যাপচার করা মান পরিবর্তনও করে না, সেইসাথে সেই ক্লোজারগুলো যা এনভায়রনমেন্ট থেকে কিছুই ক্যাপচার করে না। এই ক্লোজারগুলো তাদের এনভায়রনমেন্ট পরিবর্তন না করে একাধিকবার কল করা যেতে পারে, যা কনকারেন্টভাবে একটি ক্লোজারকে একাধিকবার কল করার মতো ক্ষেত্রে গুরুত্বপূর্ণ।

আসুন Option<T>-এর unwrap_or_else মেথডের ডেফিনিশন দেখি যা আমরা Listing 13-1-এ ব্যবহার করেছি:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

স্মরণ করুন যে T হলো জেনেরিক টাইপ যা Option-এর Some ভ্যারিয়েন্টের মানের টাইপকে প্রতিনিধিত্ব করে। সেই T টাইপটি unwrap_or_else ফাংশনের রিটার্ন টাইপও: উদাহরণস্বরূপ, Option<String>-এর উপর unwrap_or_else কল করা কোড একটি String পাবে।

এরপর লক্ষ্য করুন যে unwrap_or_else ফাংশনের একটি অতিরিক্ত জেনেরিক টাইপ প্যারামিটার F আছে। F টাইপটি f নামের প্যারামিটারের টাইপ, যা হলো সেই ক্লোজার যা আমরা unwrap_or_else কল করার সময় সরবরাহ করি।

জেনেরিক টাইপ F-এর উপর নির্দিষ্ট করা ট্রেইট বাউন্ড (trait bound) হলো FnOnce() -> T, যার মানে F-কে অবশ্যই একবার কল করা যেতে হবে, কোনো আর্গুমেন্ট না নিতে হবে, এবং একটি T রিটার্ন করতে হবে। ট্রেইট বাউন্ডে FnOnce ব্যবহার করার মাধ্যমে এই সীমাবদ্ধতা প্রকাশ করা হয় যে unwrap_or_else f-কে সর্বোচ্চ একবার কল করবে। unwrap_or_else-এর বডিতে আমরা দেখতে পাই যে যদি Option হয় Some, তাহলে f কল করা হবে না। যদি Option হয় None, তাহলে f একবার কল করা হবে। যেহেতু সমস্ত ক্লোজার FnOnce ইমপ্লিমেন্ট করে, তাই unwrap_or_else সব তিন ধরনের ক্লোজার গ্রহণ করে এবং যতটা সম্ভব ফ্লেক্সিবল।

দ্রষ্টব্য: যদি আমাদের যা করতে হবে তার জন্য এনভায়রনমেন্ট থেকে কোনো মান ক্যাপচার করার প্রয়োজন না হয়, তাহলে যেখানে আমাদের Fn ট্রেইটগুলোর একটি ইমপ্লিমেন্ট করে এমন কিছু প্রয়োজন সেখানে আমরা একটি ক্লোজারের পরিবর্তে একটি ফাংশনের নাম ব্যবহার করতে পারি। উদাহরণস্বরূপ, একটি Option<Vec<T>> মানের উপর, আমরা unwrap_or_else(Vec::new) কল করতে পারি যাতে মানটি None হলে একটি নতুন, খালি ভেক্টর পাওয়া যায়। কম্পাইলার স্বয়ংক্রিয়ভাবে একটি ফাংশন ডেফিনিশনের জন্য প্রযোজ্য Fn ট্রেইটগুলোর যেকোনোটি ইমপ্লিমেন্ট করে।

এখন আসুন স্ট্যান্ডার্ড লাইব্রেরি মেথড sort_by_key, যা স্লাইসের উপর ডিফাইন করা আছে, দেখি এটি unwrap_or_else-এর থেকে কীভাবে আলাদা এবং কেন sort_by_key ট্রেইট বাউন্ডের জন্য FnOnce-এর পরিবর্তে FnMut ব্যবহার করে। ক্লোজারটি স্লাইসের বর্তমান আইটেমের একটি রেফারেন্স আকারে একটি আর্গুমেন্ট পায় এবং K টাইপের একটি মান রিটার্ন করে যা অর্ডার করা যায়। এই ফাংশনটি উপযোগী যখন আপনি প্রতিটি আইটেমের একটি নির্দিষ্ট অ্যাট্রিবিউট দ্বারা একটি স্লাইস সর্ট করতে চান। Listing 13-7-এ, আমাদের কাছে Rectangle ইনস্ট্যান্সের একটি তালিকা আছে এবং আমরা সেগুলোকে তাদের width অ্যাট্রিবিউট দ্বারা ছোট থেকে বড় ক্রমে সাজানোর জন্য sort_by_key ব্যবহার করি।

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}

এই কোডটি প্রিন্ট করে:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

sort_by_key একটি FnMut ক্লোজার নেওয়ার জন্য ডিফাইন করা হয়েছে কারণ এটি ক্লোজারটিকে একাধিকবার কল করে: স্লাইসের প্রতিটি আইটেমের জন্য একবার। |r| r.width ক্লোজারটি তার এনভায়রনমেন্ট থেকে কিছু ক্যাপচার, পরিবর্তন বা মুভ করে না, তাই এটি ট্রেইট বাউন্ডের প্রয়োজনীয়তা পূরণ করে।

বিপরীতে, Listing 13-8 একটি ক্লোজারের উদাহরণ দেখায় যা শুধুমাত্র FnOnce ট্রেইট ইমপ্লিমেন্ট করে, কারণ এটি এনভায়রনমেন্ট থেকে একটি মান মুভ করে। কম্পাইলার আমাদের এই ক্লোজারটি sort_by_key-এর সাথে ব্যবহার করতে দেবে না।

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}

এটি list সর্ট করার সময় sort_by_key কতবার ক্লোজার কল করে তা গণনা করার একটি কৃত্রিম, জটিল উপায় (যা কাজ করে না)। এই কোডটি sort_operations ভেক্টরে value—ক্লোজারের এনভায়রনমেন্ট থেকে একটি String—পুশ করে এই গণনা করার চেষ্টা করে। ক্লোজারটি value ক্যাপচার করে এবং তারপর value-এর মালিকানা sort_operations ভেক্টরে স্থানান্তর করে ক্লোজারের বাইরে মুভ করে দেয়। এই ক্লোজারটি একবার কল করা যেতে পারে; দ্বিতীয়বার কল করার চেষ্টা করলে কাজ করবে না কারণ value আর এনভায়রনমেন্টে থাকবে না যে আবার sort_operations-এ পুশ করা যায়! তাই, এই ক্লোজারটি শুধুমাত্র FnOnce ইমপ্লিমেন্ট করে। যখন আমরা এই কোডটি কম্পাইল করার চেষ্টা করি, তখন আমরা এই এরর পাই যে value-কে ক্লোজারের বাইরে মুভ করা যাবে না কারণ ক্লোজারটিকে অবশ্যই FnMut ইমপ্লিমেন্ট করতে হবে:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         ----- captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

এররটি ক্লোজারের বডির সেই লাইনের দিকে নির্দেশ করে যা value-কে এনভায়রনমেন্টের বাইরে মুভ করে। এটি ঠিক করতে, আমাদের ক্লোজারের বডি পরিবর্তন করতে হবে যাতে এটি এনভায়রনমেন্ট থেকে মান মুভ না করে। এনভায়রনমেন্টে একটি কাউন্টার রাখা এবং ক্লোজারের বডিতে এর মান বাড়ানো হলো ক্লোজারটি কতবার কল করা হয়েছে তা গণনা করার একটি সহজ উপায়। Listing 13-9-এর ক্লোজারটি sort_by_key-এর সাথে কাজ করে কারণ এটি শুধুমাত্র num_sort_operations কাউন্টারের একটি মিউটেবল রেফারেন্স ক্যাপচার করছে এবং তাই একাধিকবার কল করা যেতে পারে:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}

ক্লোজার ব্যবহার করে এমন ফাংশন বা টাইপ ডিফাইন বা ব্যবহার করার সময় Fn ট্রেইটগুলো গুরুত্বপূর্ণ। পরবর্তী বিভাগে, আমরা ইটারেটর নিয়ে আলোচনা করব। অনেক ইটারেটর মেথড ক্লোজার আর্গুমেন্ট নেয়, তাই আমরা যখন এগোব তখন এই ক্লোজারের বিস্তারিত মনে রাখবেন!