অ্যাডভান্সড ট্রেইট (Advanced Traits)
আমরা দশম অধ্যায়ে "ট্রেইট: শেয়ার্ড বিহেভিয়ার নির্ধারণ" অংশে ট্রেইট নিয়ে আলোচনা করেছিলাম, কিন্তু এর আরও advanced বিষয়গুলো আলোচনা করিনি। এখন যেহেতু আপনি Rust সম্পর্কে আরও জানেন, তাই আমরা গভীরে যেতে পারি।
অ্যাসোসিয়েটেড টাইপ ব্যবহার করে ট্রেইট সংজ্ঞায় প্লেসহোল্ডার টাইপ নির্দিষ্ট করা
অ্যাসোসিয়েটেড টাইপ (Associated types) একটি টাইপ প্লেসহোল্ডারের সাথে ট্রেইটকে সংযুক্ত করে, যাতে ট্রেইট মেথডের সংজ্ঞাগুলো তাদের সিগনেচারে এই প্লেসহোল্ডার টাইপগুলো ব্যবহার করতে পারে। একটি ট্রেইটের implementor, প্লেসহোল্ডার টাইপের পরিবর্তে ব্যবহৃত হওয়ার জন্য একটি concrete টাইপ নির্দিষ্ট করবে। এইভাবে, আমরা এমন একটি ট্রেইট সংজ্ঞায়িত করতে পারি যা কিছু টাইপ ব্যবহার করে, কিন্তু ট্রেইটটি ইমপ্লিমেন্ট করার আগ পর্যন্ত সেই টাইপগুলো আসলে কী, তা জানার প্রয়োজন নেই।
এই অধ্যায়ের বেশিরভাগ advanced ফিচারকে আমরা বলেছি কদাচিৎ প্রয়োজন হয়। অ্যাসোসিয়েটেড টাইপগুলি মাঝখানে কোথাও রয়েছে: এগুলি বইয়ের বাকি অংশে বর্ণিত ফিচারগুলোর চেয়ে কম ব্যবহৃত হয়, তবে এই অধ্যায়ে আলোচিত অন্যান্য অনেক ফিচারের চেয়ে বেশি ব্যবহৃত হয়।
অ্যাসোসিয়েটেড টাইপ সহ একটি ট্রেইটের উদাহরণ হল Iterator
ট্রেইট, যা standard library সরবরাহ করে। অ্যাসোসিয়েটেড টাইপটির নাম Item
এবং এটি Iterator
ট্রেইট ইমপ্লিমেন্ট করা টাইপের ভ্যালুগুলোর টাইপের প্রতিনিধিত্ব করে, যার উপর ইটারেশন করা হচ্ছে। Iterator
ট্রেইটের সংজ্ঞাটি লিস্টিং ২০-১৩-তে দেখানো হয়েছে।
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Item
টাইপটি একটি প্লেসহোল্ডার, এবং next
মেথডের সংজ্ঞা দেখায় যে এটি Option<Self::Item>
টাইপের ভ্যালু রিটার্ন করবে। Iterator
ট্রেইটের ইমপ্লিমেন্টররা Item
-এর জন্য concrete টাইপ নির্দিষ্ট করবে এবং next
মেথড সেই concrete টাইপের একটি ভ্যালু ধারণকারী একটি Option
রিটার্ন করবে।
অ্যাসোসিয়েটেড টাইপগুলো জেনেরিকের মতোই মনে হতে পারে, কারণ জেনেরিক আমাদের কোন টাইপ হ্যান্ডেল করতে পারে তা নির্দিষ্ট না করেই একটি ফাংশন সংজ্ঞায়িত করার অনুমতি দেয়। দুটি ধারণার মধ্যে পার্থক্য পরীক্ষা করার জন্য, আমরা Counter
নামক একটি টাইপে Iterator
ট্রেইটের একটি ইমপ্লিমেন্টেশন দেখব, যেখানে Item
টাইপটি u32
হিসেবে নির্দিষ্ট করা হয়েছে:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
এই সিনট্যাক্সটি জেনেরিকের সিনট্যাক্সের মতোই। তাহলে কেন জেনেরিক ব্যবহার করে Iterator
ট্রেইটকে সংজ্ঞায়িত করা হয় না, যেমনটি লিস্টিং ২০-১৪-তে দেখানো হয়েছে?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
পার্থক্য হল, লিস্টিং ২০-১৪ এর মতো জেনেরিক ব্যবহার করার সময়, আমাদের প্রতিটি ইমপ্লিমেন্টেশনে টাইপ annotate করতে হবে; কারণ আমরা Iterator<String> for Counter
বা অন্য কোনো টাইপের জন্যও ইমপ্লিমেন্ট করতে পারি, তাই Counter
-এর জন্য Iterator
-এর একাধিক ইমপ্লিমেন্টেশন থাকতে পারে। অন্য কথায়, যখন একটি ট্রেইটের একটি জেনেরিক প্যারামিটার থাকে, তখন এটি একটি টাইপের জন্য একাধিকবার ইমপ্লিমেন্ট করা যেতে পারে, প্রতিবার জেনেরিক টাইপ প্যারামিটারের concrete টাইপ পরিবর্তন করে। যখন আমরা Counter
-এ next
মেথড ব্যবহার করি, তখন আমাদের টাইপ অ্যানোটেশন দিতে হবে, যাতে বোঝা যায় Iterator
-এর কোন ইমপ্লিমেন্টেশনটি আমরা ব্যবহার করতে চাই।
অ্যাসোসিয়েটেড টাইপের সাথে, আমাদের টাইপ annotate করার দরকার নেই, কারণ আমরা একটি টাইপের উপর একটি ট্রেইট একাধিকবার ইমপ্লিমেন্ট করতে পারি না। লিস্টিং ২০-১৩-তে অ্যাসোসিয়েটেড টাইপ ব্যবহার করা সংজ্ঞার সাথে, আমরা কেবল একবার বেছে নিতে পারি Item
-এর টাইপ কী হবে, কারণ impl Iterator for Counter
কেবল একবারই থাকতে পারে। Counter
-এ next
কল করার সময় আমাদের প্রতিবার উল্লেখ করতে হবে না যে আমরা u32
ভ্যালুর একটি ইটারেটর চাই।
অ্যাসোসিয়েটেড টাইপগুলি ট্রেইটের কন্ট্রাক্টের অংশ হয়ে ওঠে: ট্রেইটের ইমপ্লিমেন্টরদের অবশ্যই অ্যাসোসিয়েটেড টাইপ প্লেসহোল্ডারের জন্য একটি টাইপ সরবরাহ করতে হবে। অ্যাসোসিয়েটেড টাইপগুলির প্রায়শই এমন একটি নাম থাকে যা বর্ণনা করে কিভাবে টাইপটি ব্যবহার করা হবে, এবং API ডকুমেন্টেশনে অ্যাসোসিয়েটেড টাইপটি ডকুমেন্ট করা ভাল প্র্যাকটিস।
ডিফল্ট জেনেরিক টাইপ প্যারামিটার এবং অপারেটর ওভারলোডিং
যখন আমরা জেনেরিক টাইপ প্যারামিটার ব্যবহার করি, তখন আমরা জেনেরিক টাইপের জন্য একটি ডিফল্ট concrete টাইপ নির্দিষ্ট করতে পারি। এটি ট্রেইটের ইমপ্লিমেন্টরদের জন্য একটি concrete টাইপ নির্দিষ্ট করার প্রয়োজনীয়তা দূর করে, যদি ডিফল্ট টাইপটি কাজ করে। আপনি <PlaceholderType=ConcreteType>
সিনট্যাক্স দিয়ে জেনেরিক টাইপ ঘোষণা করার সময় একটি ডিফল্ট টাইপ নির্দিষ্ট করতে পারেন।
এই কৌশলটি যেখানে দরকারী তার একটি দুর্দান্ত উদাহরণ হল অপারেটর ওভারলোডিং, যেখানে আপনি নির্দিষ্ট পরিস্থিতিতে একটি অপারেটরের (যেমন +
) আচরণ কাস্টমাইজ করেন।
Rust আপনাকে আপনার নিজের অপারেটর তৈরি করতে বা নির্বিচারে অপারেটর ওভারলোড করার অনুমতি দেয় না। কিন্তু আপনি std::ops
-এ তালিকাভুক্ত অপারেটর এবং সংশ্লিষ্ট ট্রেইটগুলিকে ইমপ্লিমেন্ট করে সেই অপারেশনগুলি ওভারলোড করতে পারেন। উদাহরণস্বরূপ, লিস্টিং ২০-১৫-তে আমরা দুটি Point
ইন্সট্যান্সকে একসাথে যোগ করতে +
অপারেটরকে ওভারলোড করি। আমরা একটি Point
স্ট্রাকটে Add
ট্রেইট ইমপ্লিমেন্ট করে এটি করি:
use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); }
add
মেথড দুটি Point
ইন্সট্যান্সের x
ভ্যালু এবং দুটি Point
ইন্সট্যান্সের y
ভ্যালু যোগ করে একটি নতুন Point
তৈরি করে। Add
ট্রেইটের Output
নামে একটি অ্যাসোসিয়েটেড টাইপ রয়েছে যা add
মেথড থেকে রিটার্ন করা টাইপ নির্ধারণ করে।
এই কোডের ডিফল্ট জেনেরিক টাইপটি Add
ট্রেইটের মধ্যে রয়েছে। এখানে এর সংজ্ঞা দেওয়া হল:
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
এই কোডটি সাধারণভাবে পরিচিত হওয়া উচিত: একটি মেথড এবং একটি অ্যাসোসিয়েটেড টাইপ সহ একটি ট্রেইট। নতুন অংশটি হল Rhs=Self
: এই সিনট্যাক্সটিকে ডিফল্ট টাইপ প্যারামিটার বলা হয়। Rhs
জেনেরিক টাইপ প্যারামিটারটি (সংক্ষেপে "right hand side") add
মেথডের rhs
প্যারামিটারের টাইপ সংজ্ঞায়িত করে। যদি আমরা Add
ট্রেইট ইমপ্লিমেন্ট করার সময় Rhs
-এর জন্য একটি concrete টাইপ নির্দিষ্ট না করি, তাহলে Rhs
-এর টাইপ ডিফল্টভাবে Self
হবে, যেটি হবে সেই টাইপ যার উপর আমরা Add
ইমপ্লিমেন্ট করছি।
যখন আমরা Point
-এর জন্য Add
ইমপ্লিমেন্ট করেছি, তখন আমরা Rhs
-এর জন্য ডিফল্ট ব্যবহার করেছি কারণ আমরা দুটি Point
ইন্সট্যান্স যোগ করতে চেয়েছিলাম। আসুন Add
ট্রেইট ইমপ্লিমেন্ট করার একটি উদাহরণ দেখি যেখানে আমরা ডিফল্ট ব্যবহার না করে Rhs
টাইপ কাস্টমাইজ করতে চাই।
আমাদের দুটি স্ট্রাক্ট রয়েছে, Millimeters
এবং Meters
, যা বিভিন্ন ইউনিটে ভ্যালু ধারণ করে। অন্য একটি স্ট্রাকটে বিদ্যমান টাইপের এই পাতলা র্যাপিংটি নিউটাইপ প্যাটার্ন নামে পরিচিত, যা আমরা “Using the Newtype Pattern to Implement External Traits on External Types” বিভাগে আরও বিশদভাবে বর্ণনা করি। আমরা মিলিমিটারের ভ্যালুগুলিকে মিটারের ভ্যালুতে যোগ করতে চাই এবং Add
-এর ইমপ্লিমেন্টেশনকে সঠিকভাবে কনভার্সন করতে চাই। আমরা Millimeters
-এর জন্য Add
ইমপ্লিমেন্ট করতে পারি Meters
কে Rhs
হিসেবে রেখে, যেমনটি লিস্টিং ২০-১৬-তে দেখানো হয়েছে।
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Millimeters
এবং Meters
যোগ করার জন্য, আমরা Self
-এর ডিফল্ট ব্যবহার না করে Rhs
টাইপ প্যারামিটারের ভ্যালু সেট করতে impl Add<Meters>
নির্দিষ্ট করি।
আপনি দুটি প্রধান উপায়ে ডিফল্ট টাইপ প্যারামিটার ব্যবহার করবেন:
- বিদ্যমান কোড না ভেঙে একটি টাইপ প্রসারিত করতে।
- নির্দিষ্ট ক্ষেত্রে কাস্টমাইজেশনের অনুমতি দিতে, যা বেশিরভাগ ব্যবহারকারীর প্রয়োজন হবে না।
স্ট্যান্ডার্ড লাইব্রেরির Add
ট্রেইটটি দ্বিতীয় উদ্দেশ্যের একটি উদাহরণ: সাধারণত, আপনি দুটি একই রকম টাইপ যোগ করবেন, কিন্তু Add
ট্রেইট এর বাইরেও কাস্টমাইজ করার ক্ষমতা প্রদান করে। Add
ট্রেইট সংজ্ঞায় ডিফল্ট টাইপ প্যারামিটার ব্যবহার করার অর্থ হল আপনাকে বেশিরভাগ সময় অতিরিক্ত প্যারামিটারটি নির্দিষ্ট করতে হবে না। অন্য কথায়, সামান্য ইমপ্লিমেন্টেশন বয়লারপ্লেটের প্রয়োজন নেই, যা ট্রেইট ব্যবহার করা সহজ করে তোলে।
প্রথম উদ্দেশ্যটি দ্বিতীয়টির মতোই, কিন্তু বিপরীত: আপনি যদি একটি বিদ্যমান ট্রেইটে একটি টাইপ প্যারামিটার যুক্ত করতে চান, তাহলে আপনি বিদ্যমান ইমপ্লিমেন্টেশন কোড না ভেঙে ট্রেইটের কার্যকারিতা প্রসারিত করার অনুমতি দিতে একটি ডিফল্ট দিতে পারেন।
ডিসঅ্যাম্বিগুইয়েশন এর জন্য সম্পূর্ণ Qualified সিনট্যাক্স: একই নামের মেথড কল করা
Rust-এ এমন কিছু নেই যা একটি ট্রেইটের অন্য একটি ট্রেইটের মেথডের মতো একই নামের একটি মেথড থাকতে বাধা দেয়, এবং Rust আপনাকে একটি টাইপের উপর উভয় ট্রেইট ইমপ্লিমেন্ট করা থেকেও আটকায় না। টাইপের উপর সরাসরি ট্রেইটের মেথডগুলোর মতো একই নামের একটি মেথড ইমপ্লিমেন্ট করাও সম্ভব।
একই নামের মেথড কল করার সময়, আপনাকে Rust-কে জানাতে হবে আপনি কোনটি ব্যবহার করতে চান। লিস্টিং ২০-১৭-এর কোডটি বিবেচনা করুন, যেখানে আমরা দুটি ট্রেইট সংজ্ঞায়িত করেছি, Pilot
এবং Wizard
, উভয়েরই fly
নামে একটি মেথড রয়েছে। তারপর আমরা উভয় ট্রেইটকে একটি Human
টাইপের উপর ইমপ্লিমেন্ট করি, যার ইতিমধ্যেই fly
নামে একটি মেথড ইমপ্লিমেন্ট করা আছে। প্রতিটি fly
মেথড আলাদা কিছু করে।
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() {}
যখন আমরা Human
-এর একটি ইন্সট্যান্সে fly
কল করি, তখন কম্পাইলার ডিফল্টভাবে সেই মেথডটিকে কল করে যা সরাসরি টাইপের উপর ইমপ্লিমেন্ট করা হয়েছে, যেমনটি লিস্টিং ২০-১৮-তে দেখানো হয়েছে।
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; person.fly(); }
এই কোডটি চালালে *waving arms furiously*
প্রিন্ট হবে, এটি দেখায় যে Rust সরাসরি Human
-এর উপর ইমপ্লিমেন্ট করা fly
মেথডটিকে কল করেছে।
Pilot
ট্রেইট বা Wizard
ট্রেইট থেকে fly
মেথডগুলিকে কল করার জন্য, আমরা কোন fly
মেথড বোঝাতে চাই তা নির্দিষ্ট করতে আমাদের আরও স্পষ্ট সিনট্যাক্স ব্যবহার করতে হবে। লিস্টিং ২০-১৯ এই সিনট্যাক্স প্রদর্শন করে।
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
মেথডের নামের আগে ট্রেইটের নাম নির্দিষ্ট করা Rust-এর কাছে স্পষ্ট করে যে আমরা fly
-এর কোন ইমপ্লিমেন্টেশনটি কল করতে চাই। আমরা Human::fly(&person)
লিখতে পারতাম, যা লিস্টিং ২০-১৯-এ ব্যবহৃত person.fly()
-এর সমতুল্য, কিন্তু আমাদের যদি ডিসঅ্যাম্বিগুইয়েট করার প্রয়োজন না হয় তবে এটি লিখতে একটু বেশি সময় লাগবে।
এই কোডটি চালালে নিম্নলিখিতগুলি প্রিন্ট হবে:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
যেহেতু fly
মেথড একটি self
প্যারামিটার নেয়, যদি আমাদের দুটি টাইপ থাকে যারা উভয়েই একটি ট্রেইট ইমপ্লিমেন্ট করে, তাহলে Rust self
-এর টাইপের উপর ভিত্তি করে একটি ট্রেইটের কোন ইমপ্লিমেন্টেশন ব্যবহার করতে হবে তা নির্ধারণ করতে পারে।
যাইহোক, অ্যাসোসিয়েটেড ফাংশন যেগুলি মেথড নয়, সেগুলির একটি self
প্যারামিটার নেই। যখন একাধিক টাইপ বা ট্রেইট থাকে যা একই ফাংশন নাম সহ নন-মেথড ফাংশন সংজ্ঞায়িত করে, তখন Rust সব সময় জানে না আপনি কোন টাইপটি বোঝাতে চেয়েছেন, যদি না আপনি সম্পূর্ণ qualified সিনট্যাক্স ব্যবহার করেন। উদাহরণস্বরূপ, লিস্টিং ২০-২০-তে আমরা একটি পশু আশ্রয়ের জন্য একটি ট্রেইট তৈরি করি, যারা সমস্ত বাচ্চা কুকুরকে Spot নাম দিতে চায়। আমরা একটি Animal
ট্রেইট তৈরি করি, যার সাথে একটি অ্যাসোসিয়েটেড নন-মেথড ফাংশন baby_name
রয়েছে। Animal
ট্রেইটটি Dog
স্ট্রাক্টের জন্য ইমপ্লিমেন্ট করা হয়েছে, যার উপর আমরা সরাসরি baby_name
নামে একটি অ্যাসোসিয়েটেড নন-মেথড ফাংশনও সরবরাহ করি।
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); }
আমরা সমস্ত কুকুরছানাকে Spot নামকরণের কোডটি Dog
-এর উপর সংজ্ঞায়িত baby_name
অ্যাসোসিয়েটেড ফাংশনে ইমপ্লিমেন্ট করি। Dog
টাইপটি Animal
ট্রেইটও ইমপ্লিমেন্ট করে, যা সমস্ত প্রাণীর বৈশিষ্ট্য বর্ণনা করে। বাচ্চা কুকুরদের কুকুরছানা বলা হয়, এবং এটি Dog
-এর উপর Animal
ট্রেইটের ইমপ্লিমেন্টেশনে Animal
ট্রেইটের সাথে অ্যাসোসিয়েটেড baby_name
ফাংশনে প্রকাশ করা হয়েছে।
main
-এ, আমরা Dog::baby_name
ফাংশনটি কল করি, যা সরাসরি Dog
-এর উপর সংজ্ঞায়িত অ্যাসোসিয়েটেড ফাংশনকে কল করে। এই কোডটি নিম্নলিখিতগুলি প্রিন্ট করে:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
এই আউটপুটটি আমরা যা চেয়েছিলাম তা নয়। আমরা baby_name
ফাংশনটি কল করতে চাই যা Animal
ট্রেইটের অংশ, যা আমরা Dog
-এর উপর ইমপ্লিমেন্ট করেছি, যাতে কোডটি A baby dog is called a puppy
প্রিন্ট করে। লিস্টিং ২০-১৯-এ আমরা যে ট্রেইটের নাম নির্দিষ্ট করার কৌশল ব্যবহার করেছি তা এখানে সাহায্য করে না; যদি আমরা main
কে লিস্টিং ২০-২১-এর কোডে পরিবর্তন করি, তাহলে আমরা একটি কম্পাইলেশন ত্রুটি পাব।
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
যেহেতু Animal::baby_name
-এর একটি self
প্যারামিটার নেই, এবং অন্যান্য টাইপ থাকতে পারে যা Animal
ট্রেইট ইমপ্লিমেন্ট করে, তাই Rust নির্ধারণ করতে পারে না যে আমরা Animal::baby_name
-এর কোন ইমপ্লিমেন্টেশনটি চাই। আমরা এই কম্পাইলার ত্রুটি পাব:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
ডিসঅ্যাম্বিগুইয়েট করতে এবং Rust-কে জানাতে যে আমরা অন্য কোনো টাইপের জন্য Animal
-এর ইমপ্লিমেন্টেশনের পরিবর্তে Dog
-এর জন্য Animal
-এর ইমপ্লিমেন্টেশন ব্যবহার করতে চাই, আমাদের সম্পূর্ণ qualified সিনট্যাক্স ব্যবহার করতে হবে। লিস্টিং ২০-২২ প্রদর্শন করে কিভাবে সম্পূর্ণ qualified সিনট্যাক্স ব্যবহার করতে হয়।
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); }
আমরা অ্যাঙ্গেল ব্র্যাকেটের মধ্যে Rust-কে একটি টাইপ অ্যানোটেশন সরবরাহ করছি, যা নির্দেশ করে যে আমরা এই ফাংশন কলের জন্য Dog
টাইপটিকে Animal
হিসেবে বিবেচনা করতে চাই, এটি বলে Dog
-এর উপর ইমপ্লিমেন্ট করা Animal
ট্রেইট থেকে baby_name
মেথডটিকে কল করতে চাই। এই কোডটি এখন আমরা যা চাই তা প্রিন্ট করবে:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
সাধারণভাবে, সম্পূর্ণ qualified সিনট্যাক্স নিম্নরূপ সংজ্ঞায়িত করা হয়েছে:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
অ্যাসোসিয়েটেড ফাংশন যেগুলি মেথড নয়, সেগুলির জন্য কোনও receiver
থাকবে না: কেবল অন্যান্য আর্গুমেন্টের তালিকা থাকবে। আপনি সর্বত্র সম্পূর্ণ qualified সিনট্যাক্স ব্যবহার করতে পারেন যেখানে আপনি ফাংশন বা মেথড কল করেন। যাইহোক, আপনি এই সিনট্যাক্সের যেকোনো অংশ বাদ দিতে পারেন যা Rust প্রোগ্রামের অন্যান্য তথ্য থেকে বের করতে পারে। আপনাকে এই আরও ভারবোস সিনট্যাক্সটি কেবল সেই ক্ষেত্রেই ব্যবহার করতে হবে যেখানে একই নাম ব্যবহার করে একাধিক ইমপ্লিমেন্টেশন রয়েছে এবং Rust-কে সনাক্ত করতে সাহায্যের প্রয়োজন হয় আপনি কোন ইমপ্লিমেন্টেশনটি কল করতে চান।
একটি ট্রেইটের মধ্যে অন্য ট্রেইটের কার্যকারিতা প্রয়োজন করতে Supertrait ব্যবহার
কখনও কখনও, আপনি এমন একটি ট্রেইট সংজ্ঞা লিখতে পারেন যা অন্য ট্রেইটের উপর নির্ভরশীল: একটি টাইপের জন্য প্রথম ট্রেইটটি ইমপ্লিমেন্ট করতে, আপনি চাইতে পারেন যে টাইপটি দ্বিতীয় ট্রেইটটিও ইমপ্লিমেন্ট করুক। আপনি এটি করবেন যাতে আপনার ট্রেইট সংজ্ঞাটি দ্বিতীয় ট্রেইটের অ্যাসোসিয়েটেড আইটেমগুলি ব্যবহার করতে পারে। যে ট্রেইটের উপর আপনার ট্রেইট সংজ্ঞা নির্ভর করে তাকে আপনার ট্রেইটের supertrait বলা হয়।
উদাহরণস্বরূপ, ধরা যাক আমরা একটি OutlinePrint
ট্রেইট তৈরি করতে চাই, যেখানে outline_print
নামে একটি মেথড থাকবে, যা প্রদত্ত একটি ভ্যালুকে এমনভাবে ফর্ম্যাট করে প্রিন্ট করবে যেন এটি অ্যাস্টেরিস্ক (*) এর মধ্যে আবদ্ধ থাকে। অর্থাৎ, একটি Point
স্ট্রাক্ট দেওয়া হল যা স্ট্যান্ডার্ড লাইব্রেরির Display
ট্রেইট ইমপ্লিমেন্ট করে (x, y)
আউটপুট দেয়, যখন আমরা x
-এর জন্য 1
এবং y
-এর জন্য 3
আছে এমন একটি Point
ইন্সট্যান্সে outline_print
কল করি, তখন এটি নিম্নলিখিতগুলি প্রিন্ট করবে:
**********
* *
* (1, 3) *
* *
**********
outline_print
মেথডের ইমপ্লিমেন্টেশনে, আমরা Display
ট্রেইটের কার্যকারিতা ব্যবহার করতে চাই। অতএব, আমাদের নির্দিষ্ট করতে হবে যে OutlinePrint
ট্রেইটটি কেবল সেই সমস্ত টাইপের জন্য কাজ করবে যারা Display
ইমপ্লিমেন্ট করে এবং OutlinePrint
-এর প্রয়োজনীয় কার্যকারিতা সরবরাহ করে। আমরা ট্রেইট সংজ্ঞায় OutlinePrint: Display
নির্দিষ্ট করে এটি করতে পারি। এই কৌশলটি ট্রেইটে একটি ট্রেইট বাউন্ড যোগ করার মতোই। লিস্টিং ২০-২৩ OutlinePrint
ট্রেইটের একটি ইমপ্লিমেন্টেশন দেখায়।
use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {output} *"); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } fn main() {}
যেহেতু আমরা নির্দিষ্ট করেছি যে OutlinePrint
-এর Display
ট্রেইট প্রয়োজন, তাই আমরা to_string
ফাংশনটি ব্যবহার করতে পারি, যা Display
ইমপ্লিমেন্ট করে এমন যেকোনো টাইপের জন্য স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করা হয়। যদি আমরা একটি কোলন যোগ না করে এবং ট্রেইটের নামের পরে Display
ট্রেইট নির্দিষ্ট না করে to_string
ব্যবহার করার চেষ্টা করতাম, তাহলে আমরা একটি এরর পেতাম, যেখানে বলা হত যে বর্তমান স্কোপে &Self
টাইপের জন্য to_string
নামে কোনো মেথড পাওয়া যায়নি।
আসুন দেখি কী ঘটে যখন আমরা OutlinePrint
কে এমন একটি টাইপে ইমপ্লিমেন্ট করার চেষ্টা করি যা Display
ইমপ্লিমেন্ট করে না, যেমন Point
স্ট্রাক্ট:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
আমরা একটি এরর পাই যেখানে বলা হয়েছে যে Display
প্রয়োজন কিন্তু ইমপ্লিমেন্ট করা হয়নি:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
এটি ঠিক করার জন্য, আমরা Point
-এ Display
ইমপ্লিমেন্ট করি এবং OutlinePrint
-এর প্রয়োজনীয় শর্ত পূরণ করি, এইভাবে:
trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {output} *"); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3 }; p.outline_print(); }
তারপর Point
-এ OutlinePrint
ট্রেইট ইমপ্লিমেন্ট করা সফলভাবে কম্পাইল হবে, এবং আমরা একটি Point
ইন্সট্যান্সে outline_print
কল করে অ্যাস্টেরিস্কের আউটলাইনের মধ্যে এটি প্রদর্শন করতে পারি।
বাহ্যিক টাইপের উপর বাহ্যিক ট্রেইট ইমপ্লিমেন্ট করতে নিউটাইপ প্যাটার্ন ব্যবহার
দশম অধ্যায়ের “একটি টাইপের উপর একটি ট্রেইট ইমপ্লিমেন্ট করা” অংশে, আমরা অরফান নিয়ম (orphan rule) উল্লেখ করেছি, যেখানে বলা হয়েছে যে আমরা কেবল তখনই একটি টাইপের উপর একটি ট্রেইট ইমপ্লিমেন্ট করতে পারি যদি ট্রেইট বা টাইপ উভয়ই আমাদের ক্রেটের লোকাল হয়। নিউটাইপ প্যাটার্ন ব্যবহার করে এই সীমাবদ্ধতা অতিক্রম করা সম্ভব, যেখানে একটি টাপল স্ট্রাকটে একটি নতুন টাইপ তৈরি করা হয়। (আমরা পঞ্চম অধ্যায়ে “নামযুক্ত ফিল্ড ছাড়া টাপল স্ট্রাক্ট ব্যবহার করে ভিন্ন টাইপ তৈরি করা” অংশে টাপল স্ট্রাক্ট নিয়ে আলোচনা করেছি।) টাপল স্ট্রাক্টটিতে একটি ফিল্ড থাকবে এবং এটি যে টাইপের জন্য আমরা একটি ট্রেইট ইমপ্লিমেন্ট করতে চাই তার চারপাশে একটি পাতলা র্যাপার হবে। তারপর র্যাপার টাইপটি আমাদের ক্রেটের লোকাল হবে, এবং আমরা র্যাপারের উপর ট্রেইটটি ইমপ্লিমেন্ট করতে পারি। Newtype হল একটি শব্দ যা হ্যাসকেল (Haskell) প্রোগ্রামিং ভাষা থেকে এসেছে। এই প্যাটার্নটি ব্যবহার করার জন্য কোনো রানটাইম পারফরম্যান্স পেনাল্টি নেই, এবং কম্পাইল করার সময় র্যাপার টাইপটি বাদ দেওয়া হয়।
উদাহরণস্বরূপ, ধরা যাক আমরা Vec<T>
-এর উপর Display
ইমপ্লিমেন্ট করতে চাই, যা অরফান নিয়ম আমাদের সরাসরি করতে বাধা দেয় কারণ Display
ট্রেইট এবং Vec<T>
টাইপ উভয়ই আমাদের ক্রেটের বাইরে সংজ্ঞায়িত। আমরা একটি Wrapper
স্ট্রাক্ট তৈরি করতে পারি যা Vec<T>
-এর একটি ইন্সট্যান্স ধারণ করে; তারপর আমরা Wrapper
-এর উপর Display
ইমপ্লিমেন্ট করতে পারি এবং Vec<T>
ভ্যালু ব্যবহার করতে পারি, যেমনটি লিস্টিং ২০-২৪-এ দেখানো হয়েছে।
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {w}"); }
Display
-এর ইমপ্লিমেন্টেশন ভেতরের Vec<T>
অ্যাক্সেস করতে self.0
ব্যবহার করে, কারণ Wrapper
হল একটি টাপল স্ট্রাক্ট এবং Vec<T>
হল টাপলের 0 ইনডেক্সের আইটেম। তারপর আমরা Wrapper
-এর উপর Display
ট্রেইটের কার্যকারিতা ব্যবহার করতে পারি।
এই কৌশলটি ব্যবহারের অসুবিধা হল Wrapper
একটি নতুন টাইপ, তাই এটির ধারণ করা ভ্যালুর মেথডগুলি নেই। আমাদের Vec<T>
-এর সমস্ত মেথড সরাসরি Wrapper
-এর উপর ইমপ্লিমেন্ট করতে হবে যাতে মেথডগুলি self.0
-তে ডেলিগেট করে, যা আমাদের Wrapper
-কে অবিকল Vec<T>
-এর মতো আচরণ করার অনুমতি দেবে। আমরা যদি চাইতাম যে নতুন টাইপটির ভেতরের টাইপের প্রতিটি মেথড থাকুক, তাহলে ভেতরের টাইপটি রিটার্ন করার জন্য Wrapper
-এর উপর Deref
ট্রেইট (পঞ্চদশ অধ্যায়ের “Deref
ট্রেইট ব্যবহার করে স্মার্ট পয়েন্টারগুলিকে সাধারণ রেফারেন্সের মতো ব্যবহার করা” অংশে আলোচিত) ইমপ্লিমেন্ট করা একটি সমাধান হবে। যদি আমরা না চাই যে Wrapper
টাইপটির ভেতরের টাইপের সমস্ত মেথড থাকুক—উদাহরণস্বরূপ, Wrapper
টাইপের আচরণ সীমাবদ্ধ করতে—তাহলে আমাদের কেবল সেই মেথডগুলি ইমপ্লিমেন্ট করতে হবে যা আমরা ম্যানুয়ালি চাই।
এই নিউটাইপ প্যাটার্নটি তখনও দরকারী, যখন ট্রেইট জড়িত থাকে না। আসুন ফোকাস পরিবর্তন করি এবং Rust-এর টাইপ সিস্টেমের সাথে ইন্টারঅ্যাক্ট করার কিছু advanced উপায় দেখি।