সাধারণ আচরণ অ্যাবস্ট্র্যাক্ট করতে ট্রেইট অবজেক্ট ব্যবহার (Using Trait Objects to Abstract over Shared Behavior)
৮ম অধ্যায়ে আমরা উল্লেখ করেছিলাম যে ভেক্টরের একটি সীমাবদ্ধতা হলো এটি কেবল এক প্রকারের (one type) উপাদান সংরক্ষণ করতে পারে। আমরা লিস্টিং ৮-৯-এ এর একটি সমাধান তৈরি করেছিলাম যেখানে আমরা একটি SpreadsheetCell
নামের enum
সংজ্ঞায়িত করেছিলাম, যার মধ্যে ইন্টিজার, ফ্লোট এবং টেক্সট রাখার জন্য ভ্যারিয়েন্ট (variant) ছিল। এর মানে হলো আমরা প্রতিটি সেলে বিভিন্ন ধরণের ডেটা সংরক্ষণ করতে পারতাম এবং তারপরেও একটি ভেক্টর পেতাম যা সেলের একটি সারিকে উপস্থাপন করত। যখন আমাদের अदलाबदलযোগ্য আইটেমগুলো একটি নির্দিষ্ট সেটের টাইপ হয় যা কোড কম্পাইল করার সময় আমরা জানি, তখন এটি একটি চমৎকার সমাধান।
তবে, কখনও কখনও আমরা চাই যে আমাদের লাইব্রেরি ব্যবহারকারী একটি নির্দিষ্ট পরিস্থিতিতে বৈধ টাইপের সেটকে প্রসারিত (extend) করতে সক্ষম হোক। এটি কীভাবে অর্জন করা যেতে পারে তা দেখানোর জন্য, আমরা একটি উদাহরণ হিসাবে গ্রাফিক্যাল ইউজার ইন্টারফেস (GUI) টুল তৈরি করব যা আইটেমগুলির একটি তালিকার মধ্য দিয়ে যায় এবং স্ক্রিনে আঁকার জন্য প্রতিটির উপর একটি draw
মেথড কল করে—এটি GUI টুলগুলোর জন্য একটি সাধারণ কৌশল। আমরা gui
নামে একটি লাইব্রেরি ক্রেট তৈরি করব যাতে একটি GUI লাইব্রেরির কাঠামো থাকবে। এই ক্রেটে মানুষের ব্যবহারের জন্য কিছু টাইপ অন্তর্ভুক্ত থাকতে পারে, যেমন Button
বা TextField
। এছাড়াও, gui
ব্যবহারকারীরা তাদের নিজস্ব টাইপ তৈরি করতে চাইবে যা আঁকা যায়: উদাহরণস্বরূপ, একজন প্রোগ্রামার একটি Image
যোগ করতে পারে এবং অন্য একজন একটি SelectBox
যোগ করতে পারে।
লাইব্রেরি লেখার সময়, আমরা অন্য প্রোগ্রামাররা যে সমস্ত টাইপ তৈরি করতে চাইতে পারে তা আগে থেকে জানতে ও সংজ্ঞায়িত করতে পারি না। কিন্তু আমরা জানি যে gui
-কে বিভিন্ন ধরণের অনেকগুলো মান ট্র্যাক করতে হবে এবং এই ভিন্ন ভিন্ন টাইপের মানগুলির প্রতিটির উপর একটি draw
মেথড কল করতে হবে। draw
মেথড কল করার সময় ঠিক কী ঘটবে তা জানার প্রয়োজন নেই, শুধু জানা দরকার যে মানটির কাছে কল করার জন্য সেই মেথডটি থাকবে।
ইনহেরিটেন্স আছে এমন কোনো ল্যাঙ্গুয়েজে এটি করার জন্য, আমরা Component
নামে একটি ক্লাস সংজ্ঞায়িত করতে পারতাম যার একটি draw
নামের মেথড থাকবে। অন্যান্য ক্লাস, যেমন Button
, Image
, এবং SelectBox
, Component
থেকে ইনহেরিট করত এবং এর ফলে draw
মেথডটিও ইনহেরিট করত। তারা প্রত্যেকে তাদের নিজস্ব আচরণ সংজ্ঞায়িত করতে draw
মেথডকে ওভাররাইড করতে পারত, কিন্তু ফ্রেমওয়ার্কটি সমস্ত টাইপকে Component
-এর ইনস্ট্যান্স হিসাবে বিবেচনা করতে পারত এবং তাদের উপর draw
কল করতে পারত। কিন্তু যেহেতু Rust-এ ইনহেরিটেন্স নেই, তাই আমাদের gui
লাইব্রেরিটি এমনভাবে গঠন করার জন্য অন্য কোনো উপায়ের প্রয়োজন যাতে ব্যবহারকারীরা লাইব্রেরির সাথে সামঞ্জস্যপূর্ণ নতুন টাইপ তৈরি করতে পারে।
সাধারণ আচরণের জন্য একটি ট্রেইট সংজ্ঞায়িত করা (Defining a Trait for Common Behavior)
gui
-এর যে আচরণ আমরা চাই তা বাস্তবায়ন করতে, আমরা Draw
নামে একটি ট্রেইট সংজ্ঞায়িত করব যার একটি মেথড থাকবে, যার নাম draw
। তারপরে আমরা একটি ভেক্টর সংজ্ঞায়িত করতে পারি যা একটি ট্রেইট অবজেক্ট (trait object) নেয়। একটি ট্রেইট অবজেক্ট একই সাথে আমাদের নির্দিষ্ট করা ট্রেইট বাস্তবায়নকারী একটি টাইপের ইনস্ট্যান্স এবং রানটাইমে সেই টাইপের ট্রেইট মেথডগুলো খুঁজে বের করার জন্য ব্যবহৃত একটি টেবিলের দিকে নির্দেশ করে। আমরা একটি ট্রেইট অবজেক্ট তৈরি করি কোনো এক ধরণের পয়েন্টার, যেমন একটি &
রেফারেন্স বা একটি Box<T>
স্মার্ট পয়েন্টার, তারপর dyn
কীওয়ার্ড এবং তারপরে প্রাসঙ্গিক ট্রেইট নির্দিষ্ট করে। (ট্রেইট অবজেক্টকে কেন একটি পয়েন্টার ব্যবহার করতে হবে তার কারণ সম্পর্কে আমরা ২০তম অধ্যায়ে "ডাইনামিক্যালি সাইজড টাইপস এবং Sized
ট্রেইট" অংশে আলোচনা করব।) আমরা একটি জেনেরিক বা সুনির্দিষ্ট টাইপের জায়গায় ট্রেইট অবজেক্ট ব্যবহার করতে পারি। যেখানেই আমরা একটি ট্রেইট অবজেক্ট ব্যবহার করি, Rust-এর টাইপ সিস্টেম কম্পাইল টাইমে নিশ্চিত করবে যে সেই প্রেক্ষাপটে ব্যবহৃত যেকোনো মান ট্রেইট অবজেক্টের ট্রেইটটি বাস্তবায়ন করবে। ফলস্বরূপ, আমাদের কম্পাইল টাইমে সমস্ত সম্ভাব্য টাইপ জানার প্রয়োজন নেই।
আমরা উল্লেখ করেছি যে, Rust-এ, আমরা struct এবং enum-কে অন্য ল্যাঙ্গুয়েজের অবজেক্ট থেকে আলাদা করার জন্য "অবজেক্ট" বলা থেকে বিরত থাকি। একটি struct বা enum-এ, struct ফিল্ডের ডেটা এবং impl
ব্লকের আচরণ আলাদা থাকে, যেখানে অন্য ল্যাঙ্গুয়েজে ডেটা এবং আচরণ একত্রিত হয়ে একটি ধারণা তৈরি করে, যাকে প্রায়ই অবজেক্ট বলা হয়। ট্রেইট অবজেক্ট অন্য ল্যাঙ্গুয়েজের অবজেক্ট থেকে ভিন্ন কারণ আমরা একটি ট্রেইট অবজেক্টে ডেটা যোগ করতে পারি না। ট্রেইট অবজেক্ট অন্য ল্যাঙ্গুয়েজের অবজেক্টের মতো ততটা সাধারণভাবে দরকারী নয়: তাদের নির্দিষ্ট উদ্দেশ্য হলো সাধারণ আচরণের উপর অ্যাবস্ট্র্যাকশন তৈরি করা।
লিস্টিং ১৮-৩ দেখাচ্ছে কীভাবে draw
নামে একটি মেথড সহ Draw
নামে একটি ট্রেইট সংজ্ঞায়িত করতে হয়।
pub trait Draw {
fn draw(&self);
}
১০ম অধ্যায়ে ট্রেইট কীভাবে সংজ্ঞায়িত করতে হয় সে সম্পর্কে আমাদের আলোচনা থেকে এই সিনট্যাক্সটি পরিচিত মনে হওয়া উচিত। এরপর আসছে কিছু নতুন সিনট্যাক্স: লিস্টিং ১৮-৪ একটি Screen
নামের struct সংজ্ঞায়িত করে যা components
নামের একটি ভেক্টর ধারণ করে। এই ভেক্টরটির টাইপ হলো Box<dyn Draw>
, যা একটি ট্রেইট অবজেক্ট; এটি Box
-এর ভিতরে থাকা যেকোনো টাইপের জন্য একটি বিকল্প যা Draw
ট্রেইটটি বাস্তবায়ন করে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Screen
struct-এর উপর, আমরা run
নামে একটি মেথড সংজ্ঞায়িত করব যা তার প্রতিটি components
-এর উপর draw
মেথড কল করবে, যেমনটি লিস্টিং ১৮-৫-এ দেখানো হয়েছে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
এটি একটি জেনেরিক টাইপ প্যারামিটার এবং ট্রেইট বাউন্ড ব্যবহার করে struct সংজ্ঞায়িত করার থেকে ভিন্নভাবে কাজ করে। একটি জেনেরিক টাইপ প্যারামিটার একবারে কেবল একটি সুনির্দিষ্ট টাইপ দ্বারা প্রতিস্থাপিত হতে পারে, যেখানে ট্রেইট অবজেক্ট রানটাইমে একাধিক সুনির্দিষ্ট টাইপকে ট্রেইট অবজেক্টের জায়গা পূরণ করার সুযোগ দেয়। উদাহরণস্বরূপ, আমরা Screen
struct-কে একটি জেনেরিক টাইপ এবং একটি ট্রেইট বাউন্ড ব্যবহার করে সংজ্ঞায়িত করতে পারতাম, যেমন লিস্টিং ১৮-৬-এ দেখানো হয়েছে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
এটি আমাদের এমন একটি Screen
ইনস্ট্যান্সের মধ্যে সীমাবদ্ধ করে ফেলে যার কম্পোনেন্টের তালিকায় হয় সবগুলো Button
টাইপের অথবা সবগুলো TextField
টাইপের হবে। আপনি যদি শুধুমাত্র সমজাতীয় (homogeneous) সংগ্রহ ব্যবহার করেন, তবে জেনেরিক এবং ট্রেইট বাউন্ড ব্যবহার করা শ্রেয়, কারণ সংজ্ঞাগুলো কম্পাইল টাইমে সুনির্দিষ্ট টাইপ ব্যবহার করার জন্য মনোমর্ফাইজ (monomorphized) করা হবে।
অন্যদিকে, ট্রেইট অবজেক্ট ব্যবহারকারী মেথডটির মাধ্যমে, একটি Screen
ইনস্ট্যান্স এমন একটি Vec<T>
ধারণ করতে পারে যেখানে একটি Box<Button>
এবং একটি Box<TextField>
উভয়ই থাকতে পারে। চলুন দেখি এটি কীভাবে কাজ করে এবং তারপর আমরা রানটাইম পারফরম্যান্সের প্রভাব নিয়ে আলোচনা করব।
ট্রেইট ইমপ্লিমেন্ট করা (Implementing the Trait)
এখন আমরা Draw
ট্রেইট ইমপ্লিমেন্ট করে এমন কিছু টাইপ যোগ করব। আমরা Button
টাইপটি সরবরাহ করব। আবারও বলছি, একটি GUI লাইব্রেরি বাস্তবে ইমপ্লিমেন্ট করা এই বইয়ের আওতার বাইরে, তাই draw
মেথডের বডিতে কোনো দরকারী ইমপ্লিমেন্টেশন থাকবে না। ইমপ্লিমেন্টেশনটি কেমন হতে পারে তা কল্পনা করার জন্য, একটি Button
struct-এ width
, height
, এবং label
-এর জন্য ফিল্ড থাকতে পারে, যেমনটি লিস্টিং ১৮-৭-এ দেখানো হয়েছে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
Button
-এর width
, height
, এবং label
ফিল্ডগুলো অন্যান্য কম্পোনেন্টের ফিল্ড থেকে ভিন্ন হবে; উদাহরণস্বরূপ, একটি TextField
টাইপের এই একই ফিল্ডগুলো ছাড়াও একটি placeholder
ফিল্ড থাকতে পারে। আমরা স্ক্রিনে যে সব টাইপ আঁকতে চাই, তার প্রত্যেকটি Draw
ট্রেইট ইমপ্লিমেন্ট করবে কিন্তু draw
মেথডে ভিন্ন কোড ব্যবহার করবে সেই নির্দিষ্ট টাইপটি কীভাবে আঁকতে হয় তা সংজ্ঞায়িত করার জন্য, যেমন Button
এখানে করেছে (প্রকৃত GUI কোড ছাড়া, যেমনটি উল্লেখ করা হয়েছে)। Button
টাইপটির, উদাহরণস্বরূপ, একটি অতিরিক্ত impl
ব্লক থাকতে পারে যেখানে ব্যবহারকারী বাটনে ক্লিক করলে কী ঘটবে তার সাথে সম্পর্কিত মেথড থাকবে। এই ধরণের মেথড TextField
-এর মতো টাইপের ক্ষেত্রে প্রযোজ্য হবে না।
যদি আমাদের লাইব্রেরি ব্যবহারকারী কেউ width
, height
, এবং options
ফিল্ড সহ একটি SelectBox
struct ইমপ্লিমেন্ট করার সিদ্ধান্ত নেয়, তবে সে SelectBox
টাইপের উপরও Draw
ট্রেইট ইমপ্লিমেন্ট করবে, যেমনটি লিস্টিং ১৮-৮-এ দেখানো হয়েছে।
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
আমাদের লাইব্রেরির ব্যবহারকারী এখন একটি Screen
ইনস্ট্যান্স তৈরি করার জন্য তার main
ফাংশন লিখতে পারে। Screen
ইনস্ট্যান্সে, তারা একটি SelectBox
এবং একটি Button
যোগ করতে পারে, প্রতিটিকে Box<T>
-এর মধ্যে রেখে একটি ট্রেইট অবজেক্টে পরিণত করে। তারপর তারা Screen
ইনস্ট্যান্সের উপর run
মেথড কল করতে পারে, যা প্রতিটি কম্পোনেন্টের উপর draw
কল করবে। লিস্টিং ১৮-৯ এই ইমপ্লিমেন্টেশনটি দেখায়।
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
যখন আমরা লাইব্রেরিটি লিখেছিলাম, তখন আমরা জানতাম না যে কেউ SelectBox
টাইপটি যোগ করতে পারে, কিন্তু আমাদের Screen
ইমপ্লিমেন্টেশন নতুন টাইপের উপর কাজ করতে এবং এটি আঁকতে সক্ষম হয়েছিল কারণ SelectBox
Draw
ট্রেইটটি ইমপ্লিমেন্ট করে, যার মানে এটি draw
মেথডটি ইমপ্লিমেন্ট করে।
এই ধারণাটি—একটি মানের সুনির্দিষ্ট টাইপের পরিবর্তে শুধুমাত্র মানটি কোন বার্তাগুলিতে সাড়া দেয় সে সম্পর্কে উদ্বিগ্ন থাকা—ডাইনামিক্যালি টাইপড ল্যাঙ্গুয়েজগুলোর ডাক টাইপিং (duck typing) ধারণার মতো: যদি এটি হাঁসের মতো হাঁটে এবং হাঁসের মতো ডাকে, তবে এটি অবশ্যই একটি হাঁস! লিস্টিং ১৮-৫-এ Screen
-এর run
ইমপ্লিমেন্টেশনে, run
-কে জানতে হবে না প্রতিটি কম্পোনেন্টের সুনির্দিষ্ট টাইপ কী। এটি পরীক্ষা করে না যে একটি কম্পোনেন্ট Button
বা SelectBox
-এর ইনস্ট্যান্স কিনা, এটি কেবল কম্পোনেন্টের উপর draw
মেথড কল করে। components
ভেক্টরের মানগুলোর টাইপ হিসাবে Box<dyn Draw>
নির্দিষ্ট করে, আমরা Screen
-কে এমনভাবে সংজ্ঞায়িত করেছি যার এমন মান প্রয়োজন যার উপর আমরা draw
মেথড কল করতে পারি।
ডাক টাইপিং ব্যবহার করে লেখা কোডের মতো কোড লেখার জন্য ট্রেইট অবজেক্ট এবং Rust-এর টাইপ সিস্টেম ব্যবহার করার সুবিধা হলো আমাদের রানটাইমে কখনও পরীক্ষা করতে হবে না যে একটি মান একটি নির্দিষ্ট মেথড ইমপ্লিমেন্ট করে কিনা বা একটি মান মেথডটি ইমপ্লিমেন্ট না করলেও আমরা যদি তা কল করি তবে ত্রুটি পাওয়ার বিষয়ে চিন্তা করতে হবে না। যদি মানগুলো ট্রেইট অবজেক্টের প্রয়োজনীয় ট্রেইটগুলো ইমপ্লিমেন্ট না করে তবে Rust আমাদের কোড কম্পাইল করবে না।
উদাহরণস্বরূপ, লিস্টিং ১৮-১০ দেখায় যে আমরা যদি একটি String
-কে কম্পোনেন্ট হিসাবে নিয়ে একটি Screen
তৈরি করার চেষ্টা করি তবে কী ঘটে।
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
আমরা এই ত্রুটিটি পাব কারণ String
Draw
ট্রেইটটি ইমপ্লিমেন্ট করে না:
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
এই ত্রুটিটি আমাদের জানায় যে হয় আমরা Screen
-কে এমন কিছু পাঠাচ্ছি যা আমরা পাঠাতে চাইনি এবং তাই একটি ভিন্ন টাইপ পাঠানো উচিত, অথবা আমাদের String
-এর উপর Draw
ইমপ্লিমেন্ট করা উচিত যাতে Screen
এটির উপর draw
কল করতে পারে।
ট্রেইট অবজেক্ট ডাইনামিক ডিসপ্যাচ সম্পাদন করে (Trait Objects Perform Dynamic Dispatch)
১০ম অধ্যায়ে "জেনেরিক ব্যবহারকারী কোডের পারফরম্যান্স" অংশে কম্পাইলার দ্বারা জেনেরিকের উপর সঞ্চালিত মনোমর্ফাইজেশন প্রক্রিয়া সম্পর্কে আমাদের আলোচনা স্মরণ করুন: কম্পাইলার ফাংশন এবং মেথডগুলোর নন-জেনেরিক ইমপ্লিমেন্টেশন তৈরি করে প্রতিটি সুনির্দিষ্ট টাইপের জন্য যা আমরা একটি জেনেরিক টাইপ প্যারামিটারের জায়গায় ব্যবহার করি। মনোমর্ফাইজেশনের ফলে যে কোড তৈরি হয় তা স্ট্যাটিক ডিসপ্যাচ (static dispatch) করছে, যা তখন ঘটে যখন কম্পাইলার কম্পাইল টাইমে জানে আপনি কোন মেথড কল করছেন। এটি ডাইনামিক ডিসপ্যাচ (dynamic dispatch)-এর বিপরীত, যা তখন ঘটে যখন কম্পাইলার কম্পাইল টাইমে বলতে পারে না আপনি কোন মেথড কল করছেন। ডাইনামিক ডিসপ্যাচের ক্ষেত্রে, কম্পাইলার এমন কোড তৈরি করে যা রানটাইমে জানবে কোন মেথড কল করতে হবে।
যখন আমরা ট্রেইট অবজেক্ট ব্যবহার করি, তখন Rust-কে অবশ্যই ডাইনামিক ডিসপ্যাচ ব্যবহার করতে হবে। কম্পাইলার সমস্ত টাইপ জানে না যা ট্রেইট অবজেক্ট ব্যবহারকারী কোডের সাথে ব্যবহৃত হতে পারে, তাই এটি জানে না কোন টাইপের উপর ইমপ্লিমেন্ট করা কোন মেথড কল করতে হবে। পরিবর্তে, রানটাইমে, Rust ট্রেইট অবজেক্টের ভিতরের পয়েন্টারগুলো ব্যবহার করে জানে কোন মেথড কল করতে হবে। এই লুকআপ একটি রানটাইম খরচ বহন করে যা স্ট্যাটিক ডিসপ্যাচের সাথে ঘটে না। ডাইনামিক ডিসপ্যাচ কম্পাইলারকে একটি মেথডের কোড ইনলাইন করার পছন্দ থেকেও বিরত রাখে, যা ফলস্বরূপ কিছু অপটিমাইজেশন প্রতিরোধ করে। এবং Rust-এর কিছু নিয়ম আছে যেখানে আপনি ডাইনামিক ডিসপ্যাচ ব্যবহার করতে পারেন এবং কোথায় পারেন না, যাকে ডাইন কম্প্যাটিবিলিটি (dyn compatibility) বলা হয়। সেই নিয়মগুলি এই আলোচনার আওতার বাইরে, তবে আপনি তাদের সম্পর্কে আরও পড়তে পারেন রেফারেন্সে। তবে, আমরা লিস্টিং ১৮-৫-এ যে কোড লিখেছিলাম তাতে অতিরিক্ত নমনীয়তা পেয়েছিলাম এবং লিস্টিং ১৮-৯-এ সমর্থন করতে পেরেছিলাম, তাই এটি বিবেচনা করার মতো একটি ট্রেড-অফ।