অ্যাডভান্সড টাইপস (Advanced Types)
Rust-এর টাইপ সিস্টেমে কিছু বৈশিষ্ট্য রয়েছে যা আমরা ఇప్పటి পর্যন্ত উল্লেখ করেছি কিন্তু এখনও আলোচনা করিনি। টাইপ হিসাবে নিউটাইপগুলি কেন দরকারী তা পরীক্ষা করার সময় আমরা সাধারণভাবে নিউটাইপ নিয়ে আলোচনা শুরু করব। তারপর আমরা টাইপ অ্যালিয়াস-এ যাব, যা নিউটাইপের মতোই একটি বৈশিষ্ট্য কিন্তু সামান্য ভিন্ন শব্দার্থ সহ। আমরা !
টাইপ এবং ডায়নামিকভাবে সাইজড টাইপ নিয়েও আলোচনা করব।
টাইপ নিরাপত্তা এবং অ্যাবস্ট্রাকশনের জন্য নিউটাইপ প্যাটার্ন ব্যবহার করা (Using the Newtype Pattern for Type Safety and Abstraction)
এই অংশটি ধরে নিচ্ছে যে আপনি আগের অংশটি পড়েছেন “Using the Newtype Pattern to Implement External Traits on External Types.” নিউটাইপ প্যাটার্নটি আমরা இதுவரை যে কাজগুলি নিয়ে আলোচনা করেছি তার বাইরেও দরকারী, যার মধ্যে রয়েছে স্ট্যাটিকালি এনফোর্স করা যে মানগুলি কখনও বিভ্রান্ত হয় না এবং একটি মানের একক নির্দেশ করা। আপনি Listing 20-16-এ ইউনিট নির্দেশ করার জন্য নিউটাইপ ব্যবহারের একটি উদাহরণ দেখেছেন: মনে রাখবেন যে Millimeters
এবং Meters
স্ট্রাক্টগুলি একটি নিউটাইপে u32
মানগুলিকে র্যাপ করেছে। যদি আমরা Millimeters
টাইপের একটি প্যারামিটার সহ একটি ফাংশন লিখি, তাহলে আমরা এমন একটি প্রোগ্রাম কম্পাইল করতে পারব না যেটি ভুলবশত Meters
টাইপের মান বা একটি সাধারণ u32
দিয়ে সেই ফাংশনটিকে কল করার চেষ্টা করে।
আমরা একটি টাইপের কিছু ইমপ্লিমেন্টেশন বিবরণ অ্যাবস্ট্রাক্ট করার জন্য নিউটাইপ প্যাটার্নটিও ব্যবহার করতে পারি: নতুন টাইপটি একটি পাবলিক API প্রকাশ করতে পারে যা প্রাইভেট ইনার টাইপের API থেকে ভিন্ন।
নিউটাইপগুলি অভ্যন্তরীণ ইমপ্লিমেন্টেশনও লুকাতে পারে। উদাহরণস্বরূপ, আমরা একটি People
টাইপ সরবরাহ করতে পারি যা একটি HashMap<i32, String>
র্যাপ করে যা একজন ব্যক্তির ID-কে তার নামের সাথে যুক্ত করে সংরক্ষণ করে। People
ব্যবহার করা কোড শুধুমাত্র আমাদের দেওয়া পাবলিক API-এর সাথে ইন্টারঅ্যাক্ট করবে, যেমন People
কালেকশনে একটি নামের স্ট্রিং যোগ করার একটি মেথড; সেই কোডটির জানার প্রয়োজন হবে না যে আমরা অভ্যন্তরীণভাবে নামগুলিতে একটি i32
ID অ্যাসাইন করি। নিউটাইপ প্যাটার্ন হল ইমপ্লিমেন্টেশনের বিশদ বিবরণ লুকানোর জন্য এনক্যাপসুলেশন অর্জনের একটি হালকা উপায়, যা আমরা Chapter 18-এর “Encapsulation that Hides Implementation Details”-এ আলোচনা করেছি।
টাইপ অ্যালিয়াস দিয়ে টাইপ সিনোনিম তৈরি করা (Creating Type Synonyms with Type Aliases)
Rust একটি বিদ্যমান টাইপকে অন্য নাম দেওয়ার জন্য একটি টাইপ অ্যালিয়াস ঘোষণা করার ক্ষমতা প্রদান করে। এর জন্য আমরা type
কীওয়ার্ড ব্যবহার করি। উদাহরণস্বরূপ, আমরা i32
-এর জন্য Kilometers
অ্যালিয়াস তৈরি করতে পারি এভাবে:
fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
এখন, Kilometers
অ্যালিয়াসটি i32
-এর জন্য একটি সমার্থক শব্দ; Listing 20-16-এ আমরা যে 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
একই টাইপ, তাই আমরা উভয় টাইপের মান যোগ করতে পারি এবং আমরা Kilometers
মানগুলিকে এমন ফাংশনগুলিতে পাস করতে পারি যা i32
প্যারামিটার নেয়। যাইহোক, এই পদ্ধতি ব্যবহার করে, আমরা টাইপ চেকিং সুবিধা পাই না যা আমরা আগে আলোচনা করা নিউটাইপ প্যাটার্ন থেকে পাই। অন্য কথায়, আমরা যদি কোথাও Kilometers
এবং i32
মানগুলিকে মিশিয়ে ফেলি, তাহলে কম্পাইলার আমাদের কোনও error দেবে না।
টাইপ সিনোনিমের প্রধান ব্যবহারের ক্ষেত্র হল পুনরাবৃত্তি কমানো। উদাহরণস্বরূপ, আমাদের কাছে এইরকম একটি দীর্ঘ টাইপ থাকতে পারে:
Box<dyn Fn() + Send + 'static>
ফাংশন স্বাক্ষর এবং টাইপ অ্যানোটেশন হিসাবে এই দীর্ঘ টাইপটি কোডের সর্বত্র লেখা ক্লান্তিকর এবং ত্রুটিপ্রবণ হতে পারে। Listing 20-25-এর মতো কোডে পূর্ণ একটি প্রোজেক্ট থাকার কল্পনা করুন।
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(|| ()) } }
একটি টাইপ অ্যালিয়াস পুনরাবৃত্তি কমিয়ে এই কোডটিকে আরও পরিচালনাযোগ্য করে তোলে। Listing 20-26-এ, আমরা ভারবোস টাইপের জন্য 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
স্ট্রাক্ট রয়েছে যা সমস্ত সম্ভাব্য I/O ত্রুটিগুলিকে উপস্থাপন করে। std::io
-এর অনেকগুলি ফাংশন Result<T, E>
রিটার্ন করবে যেখানে E
হল std::io::Error
, যেমন Write
ট্রেইটের এই ফাংশনগুলি:
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
ট্রেইট ফাংশন স্বাক্ষরগুলি দেখতে এইরকম হয়:
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 that Never Returns)
Rust-এর একটি বিশেষ টাইপ রয়েছে যার নাম !
যা টাইপ থিওরির পরিভাষায় এম্পটি টাইপ নামে পরিচিত কারণ এর কোনও মান নেই। আমরা এটিকে নেভার টাইপ বলতে পছন্দ করি কারণ এটি ফাংশনের রিটার্ন টাইপের জায়গায় ব্যবহৃত হয় যখন একটি ফাংশন কখনও রিটার্ন করবে না। এখানে একটি উদাহরণ:
fn bar() -> ! {
// --snip--
panic!();
}
এই কোডটি এভাবে পড়া হয় “bar
ফাংশনটি কখনও রিটার্ন করে না।” যে ফাংশনগুলি কখনও রিটার্ন করে না তাদের ডাইভার্জিং ফাংশন বলা হয়। আমরা !
টাইপের মান তৈরি করতে পারি না তাই bar
কখনও রিটার্ন করতে পারে না।
কিন্তু যে টাইপের জন্য আপনি কখনই মান তৈরি করতে পারবেন না তার ব্যবহার কী? Listing 2-5-এর কোডটি স্মরণ করুন, সংখ্যা অনুমান করার গেমের অংশ; আমরা এখানে Listing 20-27-এ এর একটি অংশ পুনরুত্পাদন করেছি।
use rand::Rng;
use std::cmp::Ordering;
use std::io;
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 Operator” in Chapter 6-এ, আমরা আলোচনা করেছি যে match
আর্মগুলিকে অবশ্যই একই টাইপ রিটার্ন করতে হবে। সুতরাং, উদাহরণস্বরূপ, নিম্নলিখিত কোডটি কাজ করে না:
fn main() {
let guess = "3";
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
}
এই কোডে guess
-এর টাইপটি অবশ্যই একটি পূর্ণসংখ্যা এবং একটি স্ট্রিং হতে হবে এবং Rust-এর প্রয়োজন যে guess
-এর শুধুমাত্র একটি টাইপ থাকুক। তাহলে continue
কী রিটার্ন করে? Listing 20-27-এ আমরা কীভাবে একটি আর্ম থেকে একটি u32
রিটার্ন করতে পেরেছিলাম এবং অন্য একটি আর্ম যা continue
দিয়ে শেষ হয়েছিল?
আপনি যেমন অনুমান করতে পারেন, continue
-এর একটি !
মান রয়েছে। অর্থাৎ, যখন Rust guess
-এর টাইপ গণনা করে, তখন এটি উভয় ম্যাচ আর্মের দিকে তাকায়, পূর্বেরটি u32
মান সহ এবং পরেরটি !
মান সহ। যেহেতু !
-এর কখনও কোনও মান থাকতে পারে না, তাই Rust সিদ্ধান্ত নেয় যে guess
-এর টাইপ হল u32
।
এই আচরণের আনুষ্ঠানিক উপায় হল যে !
টাইপের এক্সপ্রেশনগুলিকে অন্য কোনও টাইপে জোর করে আনা যেতে পারে। আমাদের এই match
আর্মটিকে 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"),
}
}
}
এই কোডে, Listing 20-27-এর match
-এর মতোই একই জিনিস ঘটে: Rust দেখে যে val
-এর টাইপ T
এবং panic!
-এর টাইপ !
, তাই সামগ্রিক match
এক্সপ্রেশনের ফলাফল হল T
। এই কোডটি কাজ করে কারণ panic!
কোনও মান তৈরি করে না; এটি প্রোগ্রামটি শেষ করে দেয়। None
ক্ষেত্রে, আমরা unwrap
থেকে কোনও মান রিটার্ন করব না, তাই এই কোডটি বৈধ।
!
টাইপের একটি চূড়ান্ত এক্সপ্রেশন হল একটি loop
:
fn main() {
print!("forever ");
loop {
print!("and ever ");
}
}
এখানে, লুপটি কখনই শেষ হয় না, তাই !
হল এক্সপ্রেশনের মান। যাইহোক, আমরা যদি একটি break
অন্তর্ভুক্ত করি তবে এটি সত্য হবে না, কারণ লুপটি যখন break
-এ পৌঁছাবে তখন শেষ হবে।
ডায়নামিকভাবে সাইজড টাইপ এবং Sized
ট্রেইট (Dynamically Sized Types and the Sized
Trait)
Rust-কে তার টাইপ সম্পর্কে কিছু বিশদ জানতে হবে, যেমন একটি নির্দিষ্ট টাইপের মানের জন্য কতটা জায়গা বরাদ্দ করতে হবে। এটি তার টাইপ সিস্টেমের একটি কোণকে প্রথমে একটু বিভ্রান্তিকর করে তোলে: ডায়নামিকালি সাইজড টাইপের ধারণা। কখনও কখনও DST বা আনসাইজড টাইপ হিসাবে উল্লেখ করা হয়, এই টাইপগুলি আমাদের এমন মান ব্যবহার করে কোড লিখতে দেয় যার আকার আমরা শুধুমাত্র রানটাইমে জানতে পারি।
আসুন str
নামক একটি ডায়নামিকালি সাইজড টাইপের বিশদ বিবরণে ডুব দিই, যা আমরা পুরো বইটি জুড়ে ব্যবহার করে আসছি। ঠিক আছে, &str
নয়, শুধুমাত্র str
, একটি DST। আমরা রানটাইম পর্যন্ত জানতে পারি না স্ট্রিংটি কত লম্বা, মানে আমরা str
টাইপের একটি ভেরিয়েবল তৈরি করতে পারি না, বা আমরা str
টাইপের একটি আর্গুমেন্ট নিতে পারি না। নিম্নলিখিত কোডটি বিবেচনা করুন, যা কাজ করে না:
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}
Rust-কে জানতে হবে একটি নির্দিষ্ট টাইপের যেকোনো মানের জন্য কতটা মেমরি বরাদ্দ করতে হবে এবং একটি টাইপের সমস্ত মানের অবশ্যই একই পরিমাণ মেমরি ব্যবহার করতে হবে। যদি Rust আমাদের এই কোডটি লেখার অনুমতি দিত, তাহলে এই দুটি str
মানকে একই পরিমাণ জায়গা নিতে হত। কিন্তু তাদের দৈর্ঘ্য ভিন্ন: s1
-এর 12 বাইট স্টোরেজ প্রয়োজন এবং s2
-এর 15 বাইট প্রয়োজন। এই কারণেই ডায়নামিকালি সাইজড টাইপ ধারণকারী একটি ভেরিয়েবল তৈরি করা সম্ভব নয়।
তাহলে আমরা কি করব? এই ক্ষেত্রে, আপনি ইতিমধ্যেই উত্তরটি জানেন: আমরা s1
এবং s2
-এর টাইপ str
-এর পরিবর্তে &str
করি। “String Slices” in Chapter 4 থেকে স্মরণ করুন যে স্লাইস ডেটা স্ট্রাকচারটি শুধুমাত্র স্লাইসের শুরুর অবস্থান এবং দৈর্ঘ্য সংরক্ষণ করে। সুতরাং যদিও একটি &T
হল একটি একক মান যা মেমরির ঠিকানা সংরক্ষণ করে যেখানে T
অবস্থিত, একটি &str
হল দুটি মান: str
-এর ঠিকানা এবং এর দৈর্ঘ্য। যেমন, আমরা কম্পাইল করার সময় একটি &str
মানের আকার জানতে পারি: এটি একটি usize
-এর দৈর্ঘ্যের দ্বিগুণ। অর্থাৎ, আমরা সবসময় একটি &str
-এর আকার জানি, এটি যে স্ট্রিংটিকে নির্দেশ করে সেটি যতই দীর্ঘ হোক না কেন। সাধারণভাবে, এইভাবেই Rust-এ ডায়নামিকালি সাইজড টাইপগুলি ব্যবহার করা হয়: তাদের কাছে অতিরিক্ত মেটাডেটা থাকে যা ডায়নামিক তথ্যের আকার সংরক্ষণ করে। ডায়নামিকালি সাইজড টাইপের গোল্ডেন রুল হল যে আমাদের অবশ্যই ডায়নামিকালি সাইজড টাইপের মানগুলিকে কোনও ধরণের পয়েন্টারের পিছনে রাখতে হবে।
আমরা str
-কে সব ধরনের পয়েন্টারের সাথে একত্রিত করতে পারি: উদাহরণস্বরূপ, Box<str>
বা Rc<str>
। প্রকৃতপক্ষে, আপনি এটি আগেও দেখেছেন কিন্তু একটি ভিন্ন ডায়নামিকালি সাইজড টাইপের সাথে: ট্রেইট। প্রতিটি ট্রেইট হল একটি ডায়নামিকালি সাইজড টাইপ যা আমরা ট্রেইটের নাম ব্যবহার করে উল্লেখ করতে পারি। “Using Trait Objects That Allow for Values of Different Types” in Chapter 18-এ, আমরা উল্লেখ করেছি যে ট্রেইটগুলিকে ট্রেইট অবজেক্ট হিসাবে ব্যবহার করার জন্য, আমাদের অবশ্যই সেগুলিকে একটি পয়েন্টারের পিছনে রাখতে হবে, যেমন &dyn Trait
বা Box<dyn Trait>
(Rc<dyn Trait>
-ও কাজ করবে)।
DST-গুলির সাথে কাজ করার জন্য, Rust কম্পাইল করার সময় কোনও টাইপের আকার জানা আছে কিনা তা নির্ধারণ করতে Sized
ট্রেইট সরবরাহ করে। এই ট্রেইটটি স্বয়ংক্রিয়ভাবে সবকিছুর জন্য ইমপ্লিমেন্ট করা হয় যার আকার কম্পাইল করার সময় জানা যায়। এছাড়াও, Rust স্পষ্টভাবে প্রতিটি জেনেরিক ফাংশনে Sized
-এর উপর একটি বাউন্ড যোগ করে। অর্থাৎ, এইরকম একটি জেনেরিক ফাংশন সংজ্ঞা:
fn generic<T>(t: T) {
// --snip--
}
আসলে এমনভাবে আচরণ করে যেন আমরা এটি লিখেছি:
fn generic<T: Sized>(t: T) {
// --snip--
}
ডিফল্টরূপে, জেনেরিক ফাংশনগুলি শুধুমাত্র সেই টাইপগুলিতে কাজ করবে যেগুলির কম্পাইল করার সময় একটি পরিচিত আকার রয়েছে। যাইহোক, আপনি এই সীমাবদ্ধতা শিথিল করতে নিম্নলিখিত বিশেষ সিনট্যাক্স ব্যবহার করতে পারেন:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
-এর উপর একটি ট্রেইট বাউন্ড মানে "T
Sized
হতে পারে বা নাও হতে পারে" এবং এই নোটেশনটি ডিফল্টকে ওভাররাইড করে যে জেনেরিক টাইপগুলির কম্পাইল করার সময় একটি পরিচিত আকার থাকতে হবে। এই অর্থ সহ ?Trait
সিনট্যাক্স শুধুমাত্র Sized
-এর জন্য উপলব্ধ, অন্য কোনও ট্রেইটের জন্য নয়।
এছাড়াও লক্ষ্য করুন যে আমরা t
প্যারামিটারের টাইপ T
থেকে &T
-তে পরিবর্তন করেছি। কারণ টাইপটি Sized
নাও হতে পারে, আমাদের এটিকে কোনও ধরণের পয়েন্টারের পিছনে ব্যবহার করতে হবে। এক্ষেত্রে, আমরা একটি রেফারেন্স বেছে নিয়েছি।
এরপর, আমরা ফাংশন এবং ক্লোজার সম্পর্কে কথা বলব!