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 Types)

রাস্ট টাইপ সিস্টেমের কিছু ফিচার আছে যা আমরা এখন পর্যন্ত উল্লেখ করেছি কিন্তু আলোচনা করিনি। আমরা প্রথমে নিউটাইপ (newtype) নিয়ে সাধারণভাবে আলোচনা করে শুরু করব এবং দেখব কেন নিউটাইপ টাইপ হিসেবে উপযোগী। এরপর আমরা টাইপ অ্যালিয়াস (type alias) নিয়ে আলোচনা করব, যা নিউটাইপের মতোই একটি ফিচার কিন্তু এর শব্দার্থ কিছুটা ভিন্ন। আমরা ! টাইপ এবং ডায়নামিক্যালি সাইজড টাইপ (dynamically sized types) নিয়েও আলোচনা করব।

টাইপ সেফটি এবং অ্যাবস্ট্র্যাকশনের জন্য নিউটাইপ প্যাটার্ন ব্যবহার করা (Using the Newtype Pattern for Type Safety and Abstraction)

এই বিভাগটি পড়ার আগে ধরে নেওয়া হচ্ছে যে আপনি পূর্ববর্তী "Using the Newtype Pattern to Implement External Traits" বিভাগটি পড়েছেন। নিউটাইপ প্যাটার্নটি আমরা এখন পর্যন্ত যা আলোচনা করেছি তার বাইরেও অন্যান্য কাজের জন্য উপযোগী, যার মধ্যে রয়েছে স্ট্যাটিক্যালি নিশ্চিত করা যে মানগুলো কখনো বিভ্রান্ত হবে না এবং একটি মানের একক (unit) নির্দেশ করা। আপনি লিস্টিং ২০-১৬-তে একক নির্দেশ করার জন্য নিউটাইপ ব্যবহারের একটি উদাহরণ দেখেছেন: মনে করুন Millimeters এবং Meters struct দুটি u32 মানকে একটি নিউটাইপে র‍্যাপ (wrap) করেছিল। যদি আমরা Millimeters টাইপের একটি প্যারামিটারসহ একটি ফাংশন লিখতাম, তাহলে আমরা এমন কোনো প্রোগ্রাম কম্পাইল করতে পারতাম না যা ভুলবশত Meters টাইপের একটি মান বা একটি সাধারণ u32 দিয়ে সেই ফাংশনটি কল করার চেষ্টা করত।

আমরা একটি টাইপের কিছু ইমপ্লিমেন্টেশন ডিটেইলস অ্যাবস্ট্রাক্ট করার জন্যও নিউটাইপ প্যাটার্ন ব্যবহার করতে পারি: নতুন টাইপটি একটি পাবলিক API প্রকাশ করতে পারে যা প্রাইভেট ইনার টাইপের API থেকে ভিন্ন।

নিউটাইপ অভ্যন্তরীণ ইমপ্লিমেন্টেশন লুকাতেও পারে। উদাহরণস্বরূপ, আমরা একটি People টাইপ সরবরাহ করতে পারি যা একটি HashMap<i32, String>-কে র‍্যাপ করে, যা একজন ব্যক্তির নামের সাথে সম্পর্কিত তার আইডি সংরক্ষণ করে। People ব্যবহারকারী কোড শুধুমাত্র আমাদের সরবরাহ করা পাবলিক API-এর সাথে ইন্টারঅ্যাক্ট করবে, যেমন People কালেকশনে একটি নাম স্ট্রিং যোগ করার একটি মেথড; সেই কোডকে জানতে হবে না যে আমরা অভ্যন্তরীণভাবে নামগুলিতে একটি i32 আইডি বরাদ্দ করি। নিউটাইপ প্যাটার্নটি এনক্যাপসুলেশন (encapsulation) অর্জনের একটি হালকা উপায়, যা আমরা চ্যাপ্টার ১৮-এর "Encapsulation that Hides Implementation Details"-এ আলোচনা করেছি।

টাইপ অ্যালিয়াস দিয়ে টাইপের সমার্থক নাম তৈরি করা (Creating Type Synonyms with Type Aliases)

রাস্ট একটি বিদ্যমান টাইপকে অন্য নাম দেওয়ার জন্য একটি টাইপ অ্যালিয়াস (type alias) ঘোষণা করার সুবিধা প্রদান করে। এর জন্য আমরা type কীওয়ার্ড ব্যবহার করি। উদাহরণস্বরূপ, আমরা i32-এর জন্য Kilometers অ্যালিয়াসটি এভাবে তৈরি করতে পারি:

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

এখন Kilometers অ্যালিয়াসটি i32-এর একটি সমার্থক নাম (synonym); লিস্টিং ২০-১৬-তে তৈরি করা Millimeters এবং Meters টাইপের মতো নয়, Kilometers একটি পৃথক, নতুন টাইপ নয়। Kilometers টাইপের মানগুলোকে i32 টাইপের মানের মতোই ব্যবহার করা হবে:

fn main() {
    type Kilometers = i32;

    let x: i32 = 5;
    let y: Kilometers = 5;

    println!("x + y = {}", x + y);
}

যেহেতু Kilometers এবং i32 একই টাইপ, আমরা উভয় টাইপের মান যোগ করতে পারি এবং আমরা i32 প্যারামিটার গ্রহণকারী ফাংশনগুলিতে Kilometers মান পাস করতে পারি। তবে, এই পদ্ধতি ব্যবহার করে, আমরা আগে আলোচনা করা নিউটাইপ প্যাটার্ন থেকে প্রাপ্ত টাইপ-চেকিং সুবিধাগুলো পাই না। অন্য কথায়, যদি আমরা কোথাও Kilometers এবং i32 মান মিশিয়ে ফেলি, কম্পাইলার আমাদের কোনো এরর দেবে না।

টাইপ সিনোনিমের প্রধান ব্যবহার হলো পুনরাবৃত্তি কমানো। উদাহরণস্বরূপ, আমাদের এরকম একটি দীর্ঘ টাইপ থাকতে পারে:

Box<dyn Fn() + Send + 'static>

ফাংশন সিগনেচারে এবং কোডের সর্বত্র টাইপ অ্যানোটেশন হিসেবে এই দীর্ঘ টাইপটি লেখা ক্লান্তিকর এবং ভুলপ্রবণ হতে পারে। লিস্টিং ২০-২৫-এর মতো কোডে পূর্ণ একটি প্রজেক্ট কল্পনা করুন।

fn main() {
    let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));

    fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
        // --snip--
    }

    fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
        // --snip--
        Box::new(|| ())
    }
}

একটি টাইপ অ্যালিয়াস পুনরাবৃত্তি কমিয়ে এই কোডটিকে আরও পরিচালনাযোগ্য করে তোলে। লিস্টিং ২০-২৬-এ, আমরা দীর্ঘ টাইপের জন্য Thunk নামে একটি অ্যালিয়াস চালু করেছি এবং টাইপের সমস্ত ব্যবহারকে সংক্ষিপ্ত অ্যালিয়াস Thunk দিয়ে প্রতিস্থাপন করতে পারি।

fn main() {
    type Thunk = Box<dyn Fn() + Send + 'static>;

    let f: Thunk = Box::new(|| println!("hi"));

    fn takes_long_type(f: Thunk) {
        // --snip--
    }

    fn returns_long_type() -> Thunk {
        // --snip--
        Box::new(|| ())
    }
}

এই কোডটি পড়া এবং লেখা অনেক সহজ! একটি টাইপ অ্যালিয়াসের জন্য একটি অর্থপূর্ণ নাম নির্বাচন করা আপনার উদ্দেশ্য প্রকাশ করতেও সাহায্য করতে পারে (thunk শব্দটি এমন কোডের জন্য ব্যবহৃত হয় যা পরে মূল্যায়ন করা হবে, তাই এটি একটি ক্লোজারের জন্য একটি উপযুক্ত নাম যা সংরক্ষণ করা হয়)।

টাইপ অ্যালিয়াসগুলো পুনরাবৃত্তি কমানোর জন্য Result<T, E> টাইপের সাথেও সাধারণভাবে ব্যবহৃত হয়। স্ট্যান্ডার্ড লাইব্রেরির std::io মডিউলটি বিবেচনা করুন। I/O অপারেশনগুলো প্রায়শই একটি Result<T, E> রিটার্ন করে যখন অপারেশনগুলো কাজ করতে ব্যর্থ হয় তখন তা পরিচালনা করার জন্য। এই লাইব্রেরিতে একটি std::io::Error struct আছে যা সমস্ত সম্ভাব্য I/O ত্রুটির প্রতিনিধিত্ব করে। std::io-এর অনেক ফাংশন Result<T, E> রিটার্ন করবে যেখানে E হলো std::io::Error, যেমন Write trait-এর এই ফাংশনগুলো:

use std::fmt;
use std::io::Error;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
    fn flush(&mut self) -> Result<(), Error>;

    fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

Result<..., Error> অনেকবার পুনরাবৃত্তি হয়। তাই, std::io-তে এই টাইপ অ্যালিয়াস ঘোষণাটি রয়েছে:

use std::fmt;

type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

যেহেতু এই ঘোষণাটি std::io মডিউলে রয়েছে, আমরা সম্পূর্ণ কোয়ালিফাইড অ্যালিয়াস std::io::Result<T> ব্যবহার করতে পারি; অর্থাৎ, একটি Result<T, E> যেখানে E-কে std::io::Error হিসেবে পূরণ করা হয়েছে। Write trait ফাংশন সিগনেচারগুলো শেষ পর্যন্ত এরকম দেখায়:

use std::fmt;

type Result<T> = std::result::Result<T, std::io::Error>;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

টাইপ অ্যালিয়াস দুটি উপায়ে সাহায্য করে: এটি কোড লেখা সহজ করে এবং এটি আমাদের std::io জুড়ে একটি সামঞ্জস্যপূর্ণ ইন্টারফেস দেয়। যেহেতু এটি একটি অ্যালিয়াস, এটি কেবল আরেকটি Result<T, E>, যার মানে আমরা Result<T, E>-তে কাজ করে এমন যেকোনো মেথড এর সাথে ব্যবহার করতে পারি, সেইসাথে ? অপারেটরের মতো বিশেষ সিনট্যাক্সও।

The Never Type যা কখনো রিটার্ন করে না

রাস্টের ! নামে একটি বিশেষ টাইপ রয়েছে যা টাইপ থিওরির ভাষায় এম্পটি টাইপ (empty type) নামে পরিচিত কারণ এর কোনো মান নেই। আমরা এটিকে নেভার টাইপ (never type) বলতে পছন্দ করি কারণ এটি সেই রিটার্ন টাইপের জায়গায় বসে যখন একটি ফাংশন কখনো রিটার্ন করবে না। এখানে একটি উদাহরণ:

fn bar() -> ! {
    // --snip--
    panic!();
}

এই কোডটি এভাবে পড়া হয়: "ফাংশন bar কখনো রিটার্ন করে না।" যে ফাংশনগুলো কখনো রিটার্ন করে না তাদের ডাইভারজিং ফাংশন (diverging functions) বলা হয়। আমরা ! টাইপের মান তৈরি করতে পারি না, তাই bar কখনো রিটার্ন করতে পারে না।

কিন্তু এমন একটি টাইপের কী ব্যবহার যার জন্য আপনি কখনো মান তৈরি করতে পারবেন না? লিস্টিং ২-৫ থেকে সংখ্যা-অনুমান খেলার কোডটি মনে করুন; আমরা এর কিছুটা এখানে লিস্টিং ২০-২৭-এ পুনরুৎপাদন করেছি।

use std::cmp::Ordering;
use std::io;

use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

তখন আমরা এই কোডের কিছু বিবরণ এড়িয়ে গিয়েছিলাম। চ্যাপ্টার ৬-এর "The match Control Flow Construct"-এ আমরা আলোচনা করেছি যে match arm-গুলোকে অবশ্যই একই টাইপ রিটার্ন করতে হবে। তাই, উদাহরণস্বরূপ, নিম্নলিখিত কোডটি কাজ করে না:

fn main() {
    let guess = "3";
    let guess = match guess.trim().parse() {
        Ok(_) => 5,
        Err(_) => "hello",
    };
}

এই কোডে guess-এর টাইপ একটি ইন্টিজার এবং একটি স্ট্রিং হতে হতো, এবং রাস্টের প্রয়োজন যে guess-এর কেবল একটি টাইপ থাকবে। তাহলে continue কী রিটার্ন করে? লিস্টিং ২০-২৭-এ আমরা কীভাবে একটি arm থেকে একটি u32 রিটার্ন করার অনুমতি পেয়েছিলাম এবং অন্য একটি arm continue দিয়ে শেষ হয়েছিল?

যেমন আপনি অনুমান করতে পারেন, continue-এর একটি ! মান রয়েছে। অর্থাৎ, যখন রাস্ট guess-এর টাইপ গণনা করে, তখন এটি উভয় ম্যাচ arm দেখে, আগেরটি u32 মান সহ এবং পরেরটি ! মান সহ। যেহেতু !-এর কখনো কোনো মান থাকতে পারে না, রাস্ট সিদ্ধান্ত নেয় যে guess-এর টাইপ হলো u32

এই আচরণের আনুষ্ঠানিক বর্ণনা হলো যে ! টাইপের এক্সপ্রেশনগুলোকে অন্য যেকোনো টাইপে coerce করা যেতে পারে। আমরা এই match arm-টি continue দিয়ে শেষ করার অনুমতি পেয়েছি কারণ continue একটি মান রিটার্ন করে না; পরিবর্তে, এটি নিয়ন্ত্রণকে লুপের শীর্ষে ফিরিয়ে নিয়ে যায়, তাই Err ক্ষেত্রে, আমরা কখনো guess-এ একটি মান অ্যাসাইন করি না।

নেভার টাইপটি panic! ম্যাক্রোর সাথেও উপযোগী। Option<T> মানের উপর আমরা unwrap ফাংশনটি কল করি, যা একটি মান তৈরি করে বা এই সংজ্ঞা সহ প্যানিক করে:

enum Option<T> {
    Some(T),
    None,
}

use crate::Option::*;

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

এই কোডে, লিস্টিং ২০-২৭-এর match-এর মতোই একই জিনিস ঘটে: রাস্ট দেখে যে val-এর টাইপ T এবং panic!-এর টাইপ !, তাই সামগ্রিক match এক্সপ্রেশনের ফলাফল T। এই কোডটি কাজ করে কারণ panic! একটি মান তৈরি করে না; এটি প্রোগ্রামটি শেষ করে দেয়। None ক্ষেত্রে, আমরা unwrap থেকে একটি মান রিটার্ন করব না, তাই এই কোডটি বৈধ।

শেষ একটি এক্সপ্রেশন যার টাইপ ! হলো একটি loop:

fn main() {
    print!("forever ");

    loop {
        print!("and ever ");
    }
}

এখানে, লুপটি কখনো শেষ হয় না, তাই ! হলো এক্সপ্রেশনের মান। তবে, যদি আমরা একটি break অন্তর্ভুক্ত করতাম তবে এটি সত্য হতো না, কারণ লুপটি break-এ পৌঁছলে শেষ হয়ে যেত।

ডায়নামিক্যালি সাইজড টাইপ এবং Sized Trait

রাস্টকে তার টাইপ সম্পর্কে নির্দিষ্ট কিছু বিবরণ জানতে হয়, যেমন একটি নির্দিষ্ট টাইপের মানের জন্য কতটা জায়গা বরাদ্দ করতে হবে। এটি তার টাইপ সিস্টেমের একটি কোণকে প্রথমে কিছুটা বিভ্রান্তিকর করে তোলে: ডায়নামিক্যালি সাইজড টাইপ (dynamically sized types) এর ধারণা। কখনও কখনও DSTs বা আনসাইজড টাইপ (unsized types) হিসাবে উল্লেখ করা হয়, এই টাইপগুলো আমাদের এমন মান ব্যবহার করে কোড লিখতে দেয় যার আকার আমরা কেবল রানটাইমে জানতে পারি।

আসুন str নামক একটি ডায়নামিক্যালি সাইজড টাইপের বিবরণে প্রবেশ করি, যা আমরা বই জুড়ে ব্যবহার করে আসছি। হ্যাঁ, ঠিকই, &str নয়, বরং str নিজেই একটি DST। অনেক ক্ষেত্রে, যেমন ব্যবহারকারীর দ্বারা প্রবেশ করা টেক্সট সংরক্ষণ করার সময়, আমরা স্ট্রিংটি কত দীর্ঘ তা রানটাইম পর্যন্ত জানতে পারি না। এর মানে হলো আমরা str টাইপের একটি ভেরিয়েবল তৈরি করতে পারি না, বা str টাইপের একটি আর্গুমেন্ট নিতে পারি না। নিম্নলিখিত কোডটি বিবেচনা করুন, যা কাজ করে না:

fn main() {
    let s1: str = "Hello there!";
    let s2: str = "How's it going?";
}

রাস্টকে জানতে হয় যে একটি নির্দিষ্ট টাইপের যেকোনো মানের জন্য কতটা মেমরি বরাদ্দ করতে হবে, এবং একটি টাইপের সমস্ত মান অবশ্যই একই পরিমাণ মেমরি ব্যবহার করবে। যদি রাস্ট আমাদের এই কোডটি লিখতে দিত, তাহলে এই দুটি str মানকে একই পরিমাণ জায়গা নিতে হতো। কিন্তু তাদের দৈর্ঘ্য ভিন্ন: s1-এর জন্য ১২ বাইট স্টোরেজ প্রয়োজন এবং s2-এর জন্য ১৫ বাইট। এই কারণেই একটি ডায়নামিক্যালি সাইজড টাইপ ধারণকারী একটি ভেরিয়েবল তৈরি করা সম্ভব নয়।

তাহলে আমরা কী করব? এই ক্ষেত্রে, আপনি ইতিমধ্যে উত্তরটি জানেন: আমরা s1 এবং s2-এর টাইপকে str-এর পরিবর্তে &str করি। চ্যাপ্টার ৪-এর "String Slices" থেকে মনে করুন যে স্লাইস ডেটা স্ট্রাকচারটি কেবল স্লাইসের শুরুর অবস্থান এবং দৈর্ঘ্য সংরক্ষণ করে। তাই, যদিও একটি &T একটি একক মান যা T কোথায় অবস্থিত তার মেমরি ঠিকানা সংরক্ষণ করে, একটি &str হলো দুটি মান: str-এর ঠিকানা এবং তার দৈর্ঘ্য। এভাবে, আমরা কম্পাইল টাইমে একটি &str মানের আকার জানতে পারি: এটি একটি usize-এর দৈর্ঘ্যের দ্বিগুণ। অর্থাৎ, আমরা সর্বদা একটি &str-এর আকার জানি, এটি যে স্ট্রিংটিকে নির্দেশ করে তা যত দীর্ঘই হোক না কেন। সাধারণভাবে, রাস্টে ডায়নামিক্যালি সাইজড টাইপগুলো এভাবেই ব্যবহৃত হয়: তাদের একটি অতিরিক্ত মেটাডেটা থাকে যা ডায়নামিক তথ্যের আকার সংরক্ষণ করে। ডায়নামিক্যালি সাইজড টাইপের গোল্ডেন রুল হলো যে আমাদের সর্বদা ডায়নামিক্যালি সাইজড টাইপের মানগুলোকে কোনো না কোনো পয়েন্টারের পিছনে রাখতে হবে।

আমরা str-কে সব ধরনের পয়েন্টারের সাথে একত্রিত করতে পারি: উদাহরণস্বরূপ, Box<str> বা Rc<str>। আসলে, আপনি এটি আগে একটি ভিন্ন ডায়নামিক্যালি সাইজড টাইপের সাথে দেখেছেন: traits। প্রতিটি trait একটি ডায়নামিক্যালি সাইজড টাইপ যা আমরা trait-এর নাম ব্যবহার করে উল্লেখ করতে পারি। চ্যাপ্টার ১৮-এর "Using Trait Objects to Abstract over Shared Behavior"-এ আমরা উল্লেখ করেছি যে trait-গুলোকে trait object হিসেবে ব্যবহার করতে হলে, আমাদের সেগুলোকে একটি পয়েন্টারের পিছনে রাখতে হবে, যেমন &dyn Trait বা Box<dyn Trait> (Rc<dyn Trait>-ও কাজ করবে)।

DST-এর সাথে কাজ করার জন্য, রাস্ট Sized trait প্রদান করে যা নির্ধারণ করে যে কোনো টাইপের আকার কম্পাইল টাইমে জানা যায় কি না। এই trait-টি স্বয়ংক্রিয়ভাবে সেই সবকিছুর জন্য ইমপ্লিমেন্ট করা হয় যার আকার কম্পাইল টাইমে জানা যায়। উপরন্তু, রাস্ট প্রতিটি জেনেরিক ফাংশনে Sized-এর উপর একটি বাউন্ড (bound) অন্তর্নিহিতভাবে যোগ করে। অর্থাৎ, একটি জেনেরিক ফাংশন সংজ্ঞা যেমন এটি:

fn generic<T>(t: T) {
    // --snip--
}

আসলে এমনভাবে ব্যবহার করা হয় যেন আমরা এটি লিখেছি:

fn generic<T: Sized>(t: T) {
    // --snip--
}

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

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

?Sized-এর উপর একটি trait bound-এর অর্থ হলো "T Sized হতেও পারে বা নাও হতে পারে" এবং এই নোটেশনটি ডিফল্টকে ওভাররাইড করে যে জেনেরিক টাইপগুলোর কম্পাইল টাইমে একটি পরিচিত আকার থাকতে হবে। ?Trait সিনট্যাক্সটি এই অর্থে শুধুমাত্র Sized-এর জন্য উপলব্ধ, অন্য কোনো trait-এর জন্য নয়।

আরও লক্ষ্য করুন যে আমরা t প্যারামিটারের টাইপ T থেকে &T-তে পরিবর্তন করেছি। যেহেতু টাইপটি Sized নাও হতে পারে, আমাদের এটিকে কোনো না কোনো পয়েন্টারের পিছনে ব্যবহার করতে হবে। এই ক্ষেত্রে, আমরা একটি রেফারেন্স বেছে নিয়েছি।

পরবর্তীতে, আমরা ফাংশন এবং ক্লোজার নিয়ে কথা বলব!