মেথড সিনট্যাক্স (Method Syntax)

মেথডগুলো (Methods) ফাংশনের মতোই: আমরা সেগুলোকে fn কীওয়ার্ড এবং একটি নাম দিয়ে ঘোষণা করি, সেগুলোর প্যারামিটার এবং একটি রিটার্ন মান থাকতে পারে এবং সেগুলোর মধ্যে কিছু কোড থাকে যা অন্য কোথাও থেকে মেথড কল করা হলে চালানো হয়। ফাংশনগুলোর বিপরীতে, মেথডগুলো একটি স্ট্রাকটের (অথবা একটি এনাম বা একটি ট্রেইট অবজেক্ট, যা আমরা যথাক্রমে চ্যাপ্টার ৬ এবং চ্যাপ্টার 18-এ কভার করব) প্রেক্ষাপটে সংজ্ঞায়িত করা হয় এবং তাদের প্রথম প্যারামিটার সর্বদাই self হয়, যা স্ট্রাকটের ইন্সট্যান্সটিকে উপস্থাপন করে যেটিতে মেথড কল করা হচ্ছে।

মেথড সংজ্ঞায়িত করা (Defining Methods)

চলুন, area ফাংশনটিকে পরিবর্তন করি, যেখানে একটি Rectangle ইন্সট্যান্স প্যারামিটার হিসেবে আছে। এর পরিবর্তে, Rectangle স্ট্রাকটে একটি area মেথড সংজ্ঞায়িত করি, যেমনটি Listing 5-13-তে দেখানো হয়েছে।

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

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Rectangle-এর পরিপ্রেক্ষিতে ফাংশনটি সংজ্ঞায়িত করতে, আমরা Rectangle-এর জন্য একটি impl (ইমপ্লিমেন্টেশন) ব্লক শুরু করি। এই impl ব্লকের ভেতরের সবকিছু Rectangle টাইপের সাথে সম্পর্কিত হবে। তারপর আমরা area ফাংশনটিকে impl-এর কার্লি ব্র্যাকেটের মধ্যে সরিয়ে নিই এবং সিগনেচারে ও বডির সর্বত্র প্রথম (এবং এই ক্ষেত্রে, একমাত্র) প্যারামিটারটিকে self করি। main-এ, যেখানে আমরা area ফাংশনটিকে কল করেছি এবং rect1-কে আর্গুমেন্ট হিসাবে পাস করেছি, সেখানে আমরা পরিবর্তে আমাদের Rectangle ইন্সট্যান্সে area মেথড কল করার জন্য মেথড সিনট্যাক্স ব্যবহার করতে পারি। মেথড সিনট্যাক্স একটি ইন্সট্যান্সের পরে বসে: আমরা একটি ডট এবং তারপর মেথডের নাম, প্যারেন্থেসিস এবং যেকোনো আর্গুমেন্ট যোগ করি।

area-এর সিগনেচারে, আমরা rectangle: &Rectangle-এর পরিবর্তে &self ব্যবহার করি। &self আসলে self: &Self-এর সংক্ষিপ্ত রূপ। একটি impl ব্লকের মধ্যে, Self টাইপটি হল সেই টাইপের উপনাম (alias) যার জন্য impl ব্লকটি রয়েছে। মেথডগুলোর প্রথম প্যারামিটার হিসেবে Self টাইপের self নামের একটি প্যারামিটার থাকা আবশ্যক, তাই Rust আপনাকে প্রথম প্যারামিটারের জায়গায় শুধুমাত্র self নামটি দিয়ে এটিকে সংক্ষিপ্ত করতে দেয়। মনে রাখবেন যে, rectangle: &Rectangle-এ আমরা যেভাবে করেছিলাম, ঠিক সেভাবেই এই মেথডটি যে Self ইন্সট্যান্স ধার করে তা নির্দেশ করার জন্য আমাদের এখনও self শর্টহ্যান্ডের সামনে & ব্যবহার করতে হবে। মেথডগুলো self-এর ওনারশিপ নিতে পারে, self ইমিউটেবলভাবে ধার করতে পারে, যেমনটি আমরা এখানে করেছি, অথবা self মিউটেবলভাবে ধার করতে পারে, ঠিক যেমন তারা অন্য কোনো প্যারামিটার নিতে পারে।

আমরা এখানে &self বেছে নিয়েছি সেই একই কারণে যে কারণে আমরা ফাংশন ভার্সনে &Rectangle ব্যবহার করেছি: আমরা ওনারশিপ নিতে চাই না এবং আমরা কেবল স্ট্রাকটের ডেটা পড়তে চাই, লিখতে নয়। যদি আমরা ইন্সট্যান্সটিকে পরিবর্তন করতে চাইতাম যেটিতে আমরা মেথড কল করেছি, তাহলে আমরা প্রথম প্যারামিটার হিসাবে &mut self ব্যবহার করতাম। শুধুমাত্র self-কে প্রথম প্যারামিটার হিসাবে ব্যবহার করে ইন্সট্যান্সের ওনারশিপ নেয় এমন মেথড থাকা বিরল; এই কৌশলটি সাধারণত তখনই ব্যবহার করা হয় যখন মেথডটি self-কে অন্য কিছুতে রূপান্তরিত করে এবং আপনি রূপান্তরের পরে কলারকে মূল ইন্সট্যান্সটি ব্যবহার করতে বাধা দিতে চান।

মেথড সিনট্যাক্স সরবরাহ করা এবং প্রতিটি মেথডের সিগনেচারে self-এর টাইপ পুনরাবৃত্তি না করা ছাড়াও, ফাংশনের পরিবর্তে মেথড ব্যবহার করার প্রধান কারণ হল সংগঠন। আমরা একটি টাইপের ইন্সট্যান্সের সাথে যা করতে পারি তার সমস্ত কিছু একটি impl ব্লকে রেখেছি, যাতে আমাদের কোডের ভবিষ্যত ব্যবহারকারীদেরকে আমাদের দেওয়া লাইব্রেরির বিভিন্ন জায়গায় Rectangle-এর ক্ষমতাগুলো খুঁজতে না হয়।

লক্ষ্য করুন যে আমরা একটি মেথডকে স্ট্রাকটের একটি ফিল্ডের মতোই একই নাম দিতে পারি। উদাহরণস্বরূপ, আমরা Rectangle-এ একটি মেথড সংজ্ঞায়িত করতে পারি যার নামও width:

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

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("The rectangle has a nonzero width; it is {}", rect1.width);
    }
}

এখানে, আমরা width মেথডটিকে true রিটার্ন করার জন্য বেছে নিচ্ছি যদি ইন্সট্যান্সের width ফিল্ডের মান 0-এর চেয়ে বড় হয় এবং false রিটার্ন করার জন্য যদি মান 0 হয়: আমরা একই নামের একটি মেথডের মধ্যে একটি ফিল্ড যেকোনো উদ্দেশ্যে ব্যবহার করতে পারি। main-এ, যখন আমরা rect1.width-এর পরে প্যারেন্থেসিস দিই, তখন Rust বোঝে যে আমরা width মেথডকে বোঝাতে চেয়েছি। যখন আমরা প্যারেন্থেসিস ব্যবহার করি না, তখন Rust বোঝে যে আমরা width ফিল্ডকে বোঝাতে চেয়েছি।

প্রায়শই, সব সময় নয়, যখন আমরা একটি ফিল্ডের মতো একই নাম একটি মেথডকে দিই তখন আমরা চাই যে এটি শুধুমাত্র ফিল্ডের মান রিটার্ন করুক এবং অন্য কিছু না করুক। এই ধরনের মেথডগুলোকে গেটার (getters) বলা হয় এবং Rust অন্য কিছু ভাষার মতো স্ট্রাকট ফিল্ডের জন্য এগুলো স্বয়ংক্রিয়ভাবে প্রয়োগ করে না। গেটারগুলো দরকারী কারণ আপনি ফিল্ডটিকে প্রাইভেট কিন্তু মেথডটিকে পাবলিক করতে পারেন এবং এইভাবে টাইপের পাবলিক API-এর অংশ হিসাবে সেই ফিল্ডে রিড-অনলি অ্যাক্সেস সক্রিয় করতে পারেন। পাবলিক এবং প্রাইভেট কী এবং কীভাবে একটি ফিল্ড বা মেথডকে পাবলিক বা প্রাইভেট হিসাবে মনোনীত করতে হয় তা নিয়ে আমরা চ্যাপ্টার 7-এ আলোচনা করব।

-> অপারেটরটি কোথায়?

C এবং C++-এ, মেথড কল করার জন্য দুটি ভিন্ন অপারেটর ব্যবহার করা হয়: আপনি যদি সরাসরি অবজেক্টে একটি মেথড কল করেন তবে আপনি . ব্যবহার করেন এবং যদি আপনি অবজেক্টের পয়েন্টারে মেথড কল করেন এবং প্রথমে পয়েন্টারটিকে ডিরেফারেন্স করতে হয় তবে আপনি -> ব্যবহার করেন। অন্য কথায়, যদি object একটি পয়েন্টার হয়, তাহলে object->something() হল (*object).something()-এর অনুরূপ।

Rust-এর -> অপারেটরের সমতুল্য নেই; পরিবর্তে, Rust-এর অটোমেটিক রেফারেন্সিং এবং ডিরেফারেন্সিং (automatic referencing and dereferencing) নামক একটি ফিচার রয়েছে। মেথড কল করা Rust-এর কয়েকটি জায়গার মধ্যে একটি যেখানে এই আচরণ রয়েছে।

এটি এইভাবে কাজ করে: যখন আপনি object.something() দিয়ে একটি মেথড কল করেন, তখন Rust স্বয়ংক্রিয়ভাবে &, &mut, বা * যোগ করে যাতে object মেথডের সিগনেচারের সাথে মেলে। অন্য কথায়, নিম্নলিখিতগুলো একই:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

প্রথমটি অনেক বেশি পরিষ্কার দেখায়। এই স্বয়ংক্রিয় রেফারেন্সিং আচরণটি কাজ করে কারণ মেথডগুলোর একটি পরিষ্কার রিসিভার রয়েছে—self-এর টাইপ। একটি মেথডের রিসিভার এবং নাম দেওয়া থাকলে, Rust নিশ্চিতভাবে বের করতে পারে যে মেথডটি পড়ছে (&self), পরিবর্তন করছে (&mut self), নাকি কনজিউম করছে (self। মেথড রিসিভারদের জন্য Rust-এর অন্তর্নিহিতভাবে ধার করা (borrowing) ওনারশিপকে বাস্তবে ব্যবহারের উপযোগী করে তোলার একটি বড় অংশ।

আরও প্যারামিটার সহ মেথড (Methods with More Parameters)

আসুন Rectangle স্ট্রাকটে একটি দ্বিতীয় মেথড প্রয়োগ করে মেথডগুলো ব্যবহার করার অনুশীলন করি। এবার আমরা চাই যে একটি Rectangle-এর ইন্সট্যান্স Rectangle-এর আরেকটি ইন্সট্যান্স নেবে এবং true রিটার্ন করবে যদি দ্বিতীয় Rectangle সম্পূর্ণরূপে self-এর (প্রথম Rectangle) মধ্যে ফিট করে; অন্যথায়, এটি false রিটার্ন করবে। অর্থাৎ, আমরা can_hold মেথড সংজ্ঞায়িত করার পরে, আমরা Listing 5-14-তে দেখানো প্রোগ্রামটি লিখতে সক্ষম হতে চাই।

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

প্রত্যাশিত আউটপুটটি দেখতে নিচের মতো হবে কারণ rect2-এর উভয় ডাইমেনশন rect1-এর ডাইমেনশনের চেয়ে ছোট, কিন্তু rect3 rect1-এর চেয়ে প্রশস্ত:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

আমরা জানি যে আমরা একটি মেথড সংজ্ঞায়িত করতে চাই, তাই এটি impl Rectangle ব্লকের মধ্যে থাকবে। মেথডটির নাম হবে can_hold এবং এটি প্যারামিটার হিসাবে অন্য একটি Rectangle-এর ইমিউটেবল বোরো নেবে। আমরা মেথডটি যে কোড থেকে কল করা হবে সেটি দেখে প্যারামিটারের টাইপ কী হবে তা বলতে পারি: rect1.can_hold(&rect2) &rect2 পাস করে, যেটি হল rect2-এর একটি ইমিউটেবল বোরো, Rectangle-এর একটি ইন্সট্যান্স। এটি বোধগম্য কারণ আমাদের কেবল rect2 পড়তে হবে (লেখার পরিবর্তে, যার অর্থ আমাদের একটি মিউটেবল বোরো প্রয়োজন হবে) এবং আমরা চাই main rect2-এর ওনারশিপ বজায় রাখুক যাতে আমরা can_hold মেথড কল করার পরেও এটি ব্যবহার করতে পারি। can_hold-এর রিটার্ন মান হবে একটি বুলিয়ান এবং ইমপ্লিমেন্টেশনটি পরীক্ষা করবে যে self-এর প্রস্থ এবং উচ্চতা যথাক্রমে অন্য Rectangle-এর প্রস্থ এবং উচ্চতার চেয়ে বেশি কিনা। চলুন Listing 5-13 থেকে impl ব্লকে নতুন can_hold মেথড যোগ করি, যা Listing 5-15-এ দেখানো হয়েছে।

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

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

আমরা যখন Listing 5-14-এর main ফাংশন দিয়ে এই কোডটি চালাব, তখন আমরা আমাদের কাঙ্ক্ষিত আউটপুট পাব। মেথডগুলো একাধিক প্যারামিটার নিতে পারে যা আমরা self প্যারামিটারের পরে সিগনেচারে যোগ করি এবং সেই প্যারামিটারগুলো ফাংশনের প্যারামিটারগুলোর মতোই কাজ করে।

অ্যাসোসিয়েটেড ফাংশন (Associated Functions)

impl ব্লকের মধ্যে সংজ্ঞায়িত সমস্ত ফাংশনকে অ্যাসোসিয়েটেড ফাংশন (associated functions) বলা হয় কারণ সেগুলো impl-এর পরে থাকা টাইপের সাথে সম্পর্কিত। আমরা অ্যাসোসিয়েটেড ফাংশন সংজ্ঞায়িত করতে পারি যেগুলোর প্রথম প্যারামিটার হিসাবে self নেই (এবং এইভাবে সেগুলো মেথড নয়) কারণ তাদের কাজ করার জন্য টাইপের কোনো ইন্সট্যান্সের প্রয়োজন নেই। আমরা ইতিমধ্যেই এইরকম একটি ফাংশন ব্যবহার করেছি: String::from ফাংশন যা String টাইপে সংজ্ঞায়িত।

যে অ্যাসোসিয়েটেড ফাংশনগুলো মেথড নয় সেগুলো প্রায়শই কনস্ট্রাক্টরগুলোর জন্য ব্যবহৃত হয় যা স্ট্রাকটের একটি নতুন ইন্সট্যান্স রিটার্ন করবে। এগুলোকে প্রায়শই new বলা হয়, কিন্তু new কোনো বিশেষ নাম নয় এবং এটি ভাষার মধ্যে তৈরি করা নয়। উদাহরণস্বরূপ, আমরা square নামে একটি অ্যাসোসিয়েটেড ফাংশন সরবরাহ করতে পারি যার একটি ডাইমেনশন প্যারামিটার থাকবে এবং এটিকে প্রস্থ এবং উচ্চতা উভয় হিসাবে ব্যবহার করবে, এইভাবে একটি বর্গক্ষেত্র Rectangle তৈরি করা সহজ করে তুলবে, যেখানে একই মান দুবার উল্লেখ করার প্রয়োজন হবে না:

Filename: src/main.rs

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

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

রিটার্ন টাইপ এবং ফাংশনের বডিতে Self কীওয়ার্ডগুলো হল সেই টাইপের উপনাম যা impl কীওয়ার্ডের পরে প্রদর্শিত হয়, যা এই ক্ষেত্রে Rectangle

এই অ্যাসোসিয়েটেড ফাংশনটিকে কল করতে, আমরা স্ট্রাকটের নাম সহ :: সিনট্যাক্স ব্যবহার করি; let sq = Rectangle::square(3); হল একটি উদাহরণ। এই ফাংশনটি স্ট্রাকট দ্বারা নেমস্পেস করা হয়: :: সিনট্যাক্সটি অ্যাসোসিয়েটেড ফাংশন এবং মডিউল দ্বারা তৈরি নেমস্পেস উভয়ের জন্য ব্যবহৃত হয়। আমরা চ্যাপ্টার 7-এ মডিউল নিয়ে আলোচনা করব।

একাধিক impl ব্লক (Multiple impl Blocks)

প্রতিটি স্ট্রাকটের একাধিক impl ব্লক থাকতে পারে। উদাহরণস্বরূপ, Listing 5-15 Listing 5-16-তে দেখানো কোডের সমতুল্য, যেখানে প্রতিটি মেথড তার নিজস্ব impl ব্লকে রয়েছে।

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

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

এখানে এই মেথডগুলোকে একাধিক impl ব্লকে আলাদা করার কোনো কারণ নেই, তবে এটি বৈধ সিনট্যাক্স। আমরা চ্যাপ্টার ১০-এ একাধিক impl ব্লক দরকারী এমন একটি ক্ষেত্র দেখব, যেখানে আমরা জেনেরিক টাইপ এবং ট্রেইট নিয়ে আলোচনা করব।

সারসংক্ষেপ (Summary)

স্ট্রাকটগুলো আপনাকে কাস্টম টাইপ তৈরি করতে দেয় যা আপনার ডোমেনের জন্য অর্থপূর্ণ। স্ট্রাকট ব্যবহার করে, আপনি সম্পর্কিত ডেটার অংশগুলোকে একে অপরের সাথে সংযুক্ত রাখতে পারেন এবং আপনার কোডকে স্পষ্ট করতে প্রতিটি অংশের নাম দিতে পারেন। impl ব্লকগুলোতে, আপনি আপনার টাইপের সাথে সম্পর্কিত ফাংশনগুলো সংজ্ঞায়িত করতে পারেন এবং মেথডগুলো হল এক ধরনের অ্যাসোসিয়েটেড ফাংশন যা আপনাকে আপনার স্ট্রাকটগুলোর ইন্সট্যান্সের আচরণ নির্দিষ্ট করতে দেয়।

কিন্তু স্ট্রাকটগুলোই কাস্টম টাইপ তৈরি করার একমাত্র উপায় নয়: আসুন Rust-এর এনাম (enum) ফিচারে যাই যাতে আপনার টুলবক্সে আরেকটি টুল যুক্ত করা যায়।