অ্যাডভান্সড টাইপ (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
নাও হতে পারে, আমাদের এটিকে কোনো না কোনো পয়েন্টারের পিছনে ব্যবহার করতে হবে। এই ক্ষেত্রে, আমরা একটি রেফারেন্স বেছে নিয়েছি।
পরবর্তীতে, আমরা ফাংশন এবং ক্লোজার নিয়ে কথা বলব!