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

অ্যাডভান্সড ফাংশন এবং ক্লোজার (Advanced Functions and Closures)

এই বিভাগে ফাংশন এবং ক্লোজার সম্পর্কিত কিছু অ্যাডভান্সড ফিচার নিয়ে আলোচনা করা হয়েছে, যার মধ্যে রয়েছে ফাংশন পয়েন্টার এবং ক্লোজার রিটার্ন করা।

ফাংশন পয়েন্টার (Function Pointers)

আমরা ফাংশনে ক্লোজার কীভাবে পাস করতে হয় তা নিয়ে কথা বলেছি; আপনি ফাংশনে সাধারণ ফাংশনও পাস করতে পারেন! এই কৌশলটি তখন কার্যকর যখন আপনি একটি নতুন ক্লোজার সংজ্ঞায়িত না করে আপনার ইতিমধ্যে সংজ্ঞায়িত একটি ফাংশন পাস করতে চান। ফাংশনগুলো fn (ছোট হাতের f দিয়ে) টাইপে পরিণত হয়, এটিকে Fn ক্লোজার trait-এর সাথে গুলিয়ে ফেলবেন না। fn টাইপটিকে ফাংশন পয়েন্টার (function pointer) বলা হয়। ফাংশন পয়েন্টার দিয়ে ফাংশন পাস করা আপনাকে অন্য ফাংশনের আর্গুমেন্ট হিসেবে ফাংশন ব্যবহার করার সুযোগ দেবে।

একটি প্যারামিটার যে একটি ফাংশন পয়েন্টার, তা নির্দিষ্ট করার সিনট্যাক্সটি ক্লোজারের মতোই, যেমনটি লিস্টিং ২০-২৮-এ দেখানো হয়েছে। এখানে আমরা add_one নামে একটি ফাংশন সংজ্ঞায়িত করেছি যা তার প্যারামিটারের সাথে ১ যোগ করে। do_twice ফাংশনটি দুটি প্যারামিটার নেয়: যেকোনো ফাংশনের জন্য একটি ফাংশন পয়েন্টার যা একটি i32 প্যারামিটার নেয় এবং একটি i32 রিটার্ন করে, এবং একটি i32 মান। do_twice ফাংশনটি f ফাংশনকে দুবার কল করে, arg মানটি পাস করে, তারপর দুটি ফাংশন কলের ফলাফল একসাথে যোগ করে। main ফাংশনটি add_one এবং 5 আর্গুমেন্ট দিয়ে do_twice-কে কল করে।

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

এই কোডটি The answer is: 12 প্রিন্ট করবে। আমরা নির্দিষ্ট করেছি যে do_twice-এর f প্যারামিটারটি একটি fn যা একটি i32 টাইপের প্যারামিটার নেয় এবং একটি i32 রিটার্ন করে। এরপর আমরা do_twice-এর বডিতে f-কে কল করতে পারি। main-এ, আমরা add_one ফাংশনের নামটি do_twice-এর প্রথম আর্গুমেন্ট হিসেবে পাস করতে পারি।

ক্লোজারের মতো নয়, fn একটি টাইপ, trait নয়, তাই আমরা সরাসরি fn-কে প্যারামিটার টাইপ হিসেবে নির্দিষ্ট করি, Fn trait-গুলোর একটিকে trait bound হিসেবে ব্যবহার করে একটি জেনেরিক টাইপ প্যারামিটার ঘোষণা না করে।

ফাংশন পয়েন্টারগুলো তিনটি ক্লোজার trait (Fn, FnMut, এবং FnOnce) সবগুলোই ইমপ্লিমেন্ট করে, যার মানে হলো আপনি সবসময় একটি ফাংশন পয়েন্টারকে এমন একটি ফাংশনের আর্গুমেন্ট হিসেবে পাস করতে পারেন যা একটি ক্লোজার আশা করে। ফাংশন লেখার সময় একটি জেনেরিক টাইপ এবং ক্লোজার trait-গুলোর একটি ব্যবহার করা সবচেয়ে ভালো, যাতে আপনার ফাংশনগুলো ফাংশন বা ক্লোজার উভয়ই গ্রহণ করতে পারে।

তবে, একটি উদাহরণ যেখানে আপনি শুধুমাত্র fn গ্রহণ করতে চাইবেন এবং ক্লোজার নয়, তা হলো যখন এক্সটার্নাল কোডের সাথে ইন্টারফেস করছেন যার ক্লোজার নেই: সি (C) ফাংশনগুলো আর্গুমেন্ট হিসেবে ফাংশন গ্রহণ করতে পারে, কিন্তু সি-তে ক্লোজার নেই।

আপনি কোথায় একটি ইনলাইন সংজ্ঞায়িত ক্লোজার বা একটি নামযুক্ত ফাংশন ব্যবহার করতে পারেন তার একটি উদাহরণ হিসেবে, আসুন স্ট্যান্ডার্ড লাইব্রেরির Iterator trait দ্বারা প্রদত্ত map মেথডের একটি ব্যবহার দেখি। সংখ্যার একটি ভেক্টরকে স্ট্রিংয়ের একটি ভেক্টরে পরিণত করার জন্য map মেথড ব্যবহার করতে, আমরা একটি ক্লোজার ব্যবহার করতে পারি, যেমনটি লিস্টিং ২০-২৯-এ দেখানো হয়েছে।

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

লক্ষ্য করুন যে আমাদের অবশ্যই সম্পূর্ণ কোয়ালিফাইড সিনট্যাক্স (fully qualified syntax) ব্যবহার করতে হবে যা আমরা "Advanced Traits" বিভাগে আলোচনা করেছি কারণ to_string নামে একাধিক ফাংশন উপলব্ধ রয়েছে।

এখানে, আমরা ToString trait-এ সংজ্ঞায়িত to_string ফাংশনটি ব্যবহার করছি, যা স্ট্যান্ডার্ড লাইব্রেরি Display ইমপ্লিমেন্ট করে এমন যেকোনো টাইপের জন্য ইমপ্লিমেন্ট করেছে।

চ্যাপ্টার ৬-এর "Enum Values" থেকে মনে করুন যে আমরা যে প্রতিটি enum variant সংজ্ঞায়িত করি তার নামও একটি ইনিশিয়ালাইজার ফাংশন হয়ে যায়। আমরা এই ইনিশিয়ালাইজার ফাংশনগুলোকে ফাংশন পয়েন্টার হিসেবে ব্যবহার করতে পারি যা ক্লোজার trait-গুলো ইমপ্লিমেন্ট করে, যার মানে হলো আমরা ক্লোজার গ্রহণকারী মেথডগুলোর আর্গুমেন্ট হিসেবে ইনিশিয়ালাইজার ফাংশনগুলো নির্দিষ্ট করতে পারি, যেমনটি লিস্টিং ২০-৩১-এ দেখা যায়।

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

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

এখানে, আমরা map কল করা রেঞ্জের প্রতিটি u32 মান ব্যবহার করে Status::Value-এর ইনিশিয়ালাইজার ফাংশন ব্যবহার করে Status::Value ইনস্ট্যান্স তৈরি করছি। কিছু লোক এই স্টাইল পছন্দ করে এবং কিছু লোক ক্লোজার ব্যবহার করতে পছন্দ করে। এগুলো একই কোডে কম্পাইল হয়, তাই আপনার কাছে যেটি বেশি স্পষ্ট মনে হয় সেটিই ব্যবহার করুন।

ক্লোজার রিটার্ন করা (Returning Closures)

ক্লোজারগুলো trait দ্বারা প্রতিনিধিত্ব করা হয়, যার মানে হলো আপনি সরাসরি ক্লোজার রিটার্ন করতে পারবেন না। বেশিরভাগ ক্ষেত্রে যেখানে আপনি একটি trait রিটার্ন করতে চাইতে পারেন, সেখানে আপনি ফাংশনের রিটার্ন ভ্যালু হিসেবে trait ইমপ্লিমেন্ট করে এমন সুনির্দিষ্ট (concrete) টাইপ ব্যবহার করতে পারেন। তবে, আপনি সাধারণত ক্লোজারের সাথে তা করতে পারবেন না কারণ তাদের কোনো রিটার্নযোগ্য সুনির্দিষ্ট টাইপ নেই; উদাহরণস্বরূপ, যদি ক্লোজারটি তার স্কোপ থেকে কোনো মান ক্যাপচার করে তবে আপনাকে ফাংশন পয়েন্টার fn-কে রিটার্ন টাইপ হিসেবে ব্যবহার করার অনুমতি নেই।

পরিবর্তে, আপনি সাধারণত impl Trait সিনট্যাক্স ব্যবহার করবেন যা আমরা চ্যাপ্টার ১০-এ শিখেছি। আপনি Fn, FnOnce এবং FnMut ব্যবহার করে যেকোনো ফাংশন টাইপ রিটার্ন করতে পারেন। উদাহরণস্বরূপ, লিস্টিং ২০-৩২-এর কোডটি ঠিকঠাক কম্পাইল হবে।

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}

তবে, যেমনটি আমরা চ্যাপ্টার ১৩-এর "Closure Type Inference and Annotation"-এ উল্লেখ করেছি, প্রতিটি ক্লোজারও তার নিজস্ব স্বতন্ত্র টাইপ। যদি আপনার একই সিগনেচার কিন্তু ভিন্ন ইমপ্লিমেন্টেশন সহ একাধিক ফাংশনের সাথে কাজ করার প্রয়োজন হয়, তবে আপনাকে তাদের জন্য একটি 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() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
    move |x| x + init
}

এখানে আমাদের দুটি ফাংশন আছে, returns_closure এবং returns_initialized_closure, উভয়ই impl Fn(i32) -> i32 রিটার্ন করে। লক্ষ্য করুন যে তারা যে ক্লোজারগুলো রিটার্ন করে তা ভিন্ন, যদিও তারা একই টাইপ ইমপ্লিমেন্ট করে। যদি আমরা এটি কম্পাইল করার চেষ্টা করি, রাস্ট আমাদের জানায় যে এটি কাজ করবে না:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
  --> src/main.rs:2:44
   |
2  |     let handlers = vec![returns_closure(), returns_initialized_closure(123)];
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9  | fn returns_closure() -> impl Fn(i32) -> i32 {
   |                         ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
   |                                              ------------------- the found opaque type
   |
   = note: expected opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:9:25>)
              found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
   = note: distinct uses of `impl Trait` result in different opaque types

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

এরর মেসেজটি আমাদের বলে যে যখনই আমরা একটি impl Trait রিটার্ন করি, রাস্ট একটি অনন্য অস্বচ্ছ টাইপ (opaque type) তৈরি করে, এমন একটি টাইপ যেখানে আমরা রাস্ট আমাদের জন্য যা তৈরি করে তার বিবরণ দেখতে পারি না, বা আমরা রাস্ট যে টাইপ তৈরি করবে তা অনুমান করে নিজে লিখতে পারি না। তাই যদিও এই ফাংশনগুলো একই trait, Fn(i32) -> i32, ইমপ্লিমেন্ট করে এমন ক্লোজার রিটার্ন করে, রাস্ট প্রতিটির জন্য যে অস্বচ্ছ টাইপ তৈরি করে তা স্বতন্ত্র। (এটি যেমন রাস্ট ভিন্ন ভিন্ন async ব্লকের জন্য ভিন্ন ভিন্ন সুনির্দিষ্ট টাইপ তৈরি করে যদিও তাদের আউটপুট টাইপ একই হয়, যেমনটি আমরা চ্যাপ্টার ১৭-এর "Working with Any Number of Futures"-এ দেখেছি।) আমরা এখন কয়েকবার এই সমস্যার একটি সমাধান দেখেছি: আমরা একটি 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)
}

এই কোডটি ঠিকঠাক কম্পাইল হবে। trait object সম্পর্কে আরও জানতে, চ্যাপ্টার ১৮-এর "Using Trait Objects That Allow for Values of Different Types" বিভাগটি দেখুন।

এর পরে, চলুন ম্যাক্রো দেখি!