ম্যাক্রো (Macros)
আমরা এই বই জুড়ে println!
-এর মতো ম্যাক্রো ব্যবহার করেছি, কিন্তু একটি ম্যাক্রো কী এবং এটি কীভাবে কাজ করে তা আমরা পুরোপুরিভাবে আলোচনা করিনি। ম্যাক্রো শব্দটি রাস্টের বিভিন্ন ফিচারের একটি পরিবারকে বোঝায়: macro_rules!
সহ ডিক্লারেটিভ ম্যাক্রো এবং তিন ধরনের প্রসিডিউরাল ম্যাক্রো:
- কাস্টম
#[derive]
ম্যাক্রো যা struct এবং enum-এ ব্যবহৃতderive
অ্যাট্রিবিউটের সাথে যোগ করা কোড নির্দিষ্ট করে। - অ্যাট্রিবিউট-লাইক ম্যাক্রো যা যেকোনো আইটেমে ব্যবহারযোগ্য কাস্টম অ্যাট্রিবিউট সংজ্ঞায়িত করে।
- ফাংশন-লাইক ম্যাক্রো যা ফাংশন কলের মতো দেখায় কিন্তু তাদের আর্গুমেন্ট হিসেবে নির্দিষ্ট করা টোকেনগুলোর উপর কাজ করে।
আমরা একে একে প্রতিটি নিয়ে আলোচনা করব, কিন্তু প্রথমে, চলুন দেখি যখন আমাদের কাছে ফাংশন আছে তখন আমাদের ম্যাক্রোর প্রয়োজন কেন।
ম্যাক্রো এবং ফাংশনের মধ্যে পার্থক্য
মৌলিকভাবে, ম্যাক্রোগুলো হলো এমন কোড লেখার একটি উপায় যা অন্য কোড লেখে, যা মেটাপ্রোগ্রামিং (metaprogramming) নামে পরিচিত। পরিশিষ্ট সি-তে, আমরা derive
অ্যাট্রিবিউট নিয়ে আলোচনা করি, যা আপনার জন্য বিভিন্ন trait-এর একটি ইমপ্লিমেন্টেশন তৈরি করে। আমরা বই জুড়ে println!
এবং vec!
ম্যাক্রোগুলোও ব্যবহার করেছি। এই সমস্ত ম্যাক্রোগুলো আপনার ম্যানুয়ালি লেখা কোডের চেয়ে বেশি কোড তৈরি করতে এক্সপ্যান্ড (expand) হয়।
মেটাপ্রোগ্রামিং আপনাকে যে পরিমাণ কোড লিখতে এবং রক্ষণাবেক্ষণ করতে হয় তা কমানোর জন্য দরকারী, যা ফাংশনেরও একটি ভূমিকা। তবে, ম্যাক্রোগুলোর কিছু অতিরিক্ত ক্ষমতা রয়েছে যা ফাংশনের নেই।
একটি ফাংশন সিগনেচারকে অবশ্যই ফাংশনের প্যারামিটারের সংখ্যা এবং টাইপ ঘোষণা করতে হয়। অন্যদিকে, ম্যাক্রোগুলো পরিবর্তনশীল সংখ্যক প্যারামিটার নিতে পারে: আমরা println!("hello")
একটি আর্গুমেন্ট দিয়ে অথবা println!("hello {}", name)
দুটি আর্গুমেন্ট দিয়ে কল করতে পারি। এছাড়াও, কম্পাইলার কোডের অর্থ ব্যাখ্যা করার আগে ম্যাক্রোগুলো এক্সপ্যান্ড হয়, তাই একটি ম্যাক্রো, উদাহরণস্বরূপ, একটি প্রদত্ত টাইপের উপর একটি trait ইমপ্লিমেন্ট করতে পারে। একটি ফাংশন তা করতে পারে না, কারণ এটি রানটাইমে কল করা হয় এবং একটি trait কম্পাইল টাইমে ইমপ্লিমেন্ট করা প্রয়োজন।
একটি ফাংশনের পরিবর্তে একটি ম্যাক্রো ইমপ্লিমেন্ট করার অসুবিধা হলো ম্যাক্রো সংজ্ঞা ফাংশন সংজ্ঞার চেয়ে বেশি জটিল কারণ আপনি রাস্ট কোড লিখছেন যা রাস্ট কোড লেখে। এই পরোক্ষতার কারণে, ম্যাক্রো সংজ্ঞা সাধারণত ফাংশন সংজ্ঞার চেয়ে পড়া, বোঝা এবং রক্ষণাবেক্ষণ করা বেশি কঠিন।
ম্যাক্রো এবং ফাংশনের মধ্যে আরেকটি গুরুত্বপূর্ণ পার্থক্য হলো আপনাকে অবশ্যই একটি ফাইলে কল করার আগে ম্যাক্রোগুলোকে সংজ্ঞায়িত করতে হবে বা স্কোপে আনতে হবে, ফাংশনের বিপরীতে যা আপনি যেকোনো জায়গায় সংজ্ঞায়িত করতে এবং যেকোনো জায়গায় কল করতে পারেন।
সাধারণ মেটাপ্রোগ্রামিংয়ের জন্য macro_rules!
সহ ডিক্লারেটিভ ম্যাক্রো
রাস্টে সবচেয়ে বহুল ব্যবহৃত ম্যাক্রোর রূপ হলো ডিক্লারেটিভ ম্যাক্রো (declarative macro)। এগুলোকে কখনও কখনও "macros by example", "macro_rules!
macros", বা শুধু "macros" বলা হয়। তাদের মূল ভিত্তি হলো, ডিক্লারেটিভ ম্যাক্রোগুলো আপনাকে রাস্টের match
এক্সপ্রেশনের মতো কিছু লেখার সুযোগ দেয়। চ্যাপ্টার ৬-এ যেমন আলোচনা করা হয়েছে, match
এক্সপ্রেশনগুলো হলো কন্ট্রোল স্ট্রাকচার যা একটি এক্সপ্রেশন নেয়, এক্সপ্রেশনের ফলস্বরূপ মানকে প্যাটার্নের সাথে তুলনা করে, এবং তারপর ম্যাচিং প্যাটার্নের সাথে যুক্ত কোড চালায়। ম্যাক্রোগুলোও একটি মানকে নির্দিষ্ট কোডের সাথে যুক্ত প্যাটার্নের সাথে তুলনা করে: এই পরিস্থিতিতে, মানটি হলো ম্যাক্রোতে পাস করা আক্ষরিক রাস্ট সোর্স কোড; প্যাটার্নগুলো সেই সোর্স কোডের কাঠামোর সাথে তুলনা করা হয়; এবং প্রতিটি প্যাটার্নের সাথে যুক্ত কোড, ম্যাচ হলে, ম্যাক্রোতে পাস করা কোডকে প্রতিস্থাপন করে। এই সবকিছু কম্পাইলেশনের সময় ঘটে।
একটি ম্যাক্রো সংজ্ঞায়িত করতে, আপনি macro_rules!
কনস্ট্রাক্ট ব্যবহার করেন। চলুন vec!
ম্যাক্রোটি কীভাবে সংজ্ঞায়িত করা হয় তা দেখে macro_rules!
কীভাবে ব্যবহার করতে হয় তা অন্বেষণ করি। চ্যাপ্টার ৮-এ আমরা দেখেছি কীভাবে আমরা নির্দিষ্ট মান সহ একটি নতুন ভেক্টর তৈরি করতে vec!
ম্যাক্রো ব্যবহার করতে পারি। উদাহরণস্বরূপ, নিম্নলিখিত ম্যাক্রোটি তিনটি ইন্টিজার ধারণকারী একটি নতুন ভেক্টর তৈরি করে:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
আমরা vec!
ম্যাক্রোটি দুটি ইন্টিজারের ভেক্টর বা পাঁচটি স্ট্রিং স্লাইসের ভেক্টর তৈরি করতেও ব্যবহার করতে পারতাম। আমরা একই কাজ করার জন্য একটি ফাংশন ব্যবহার করতে পারতাম না কারণ আমরা আগে থেকে মানের সংখ্যা বা টাইপ জানতাম না।
লিস্টিং ২০-৩৫ vec!
ম্যাক্রোর একটি সামান্য সরলীকৃত সংজ্ঞা দেখায়।
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
দ্রষ্টব্য: স্ট্যান্ডার্ড লাইব্রেরিতে
vec!
ম্যাক্রোর আসল সংজ্ঞায় আগে থেকে সঠিক পরিমাণ মেমরি প্রি-অ্যালোকেট করার কোড অন্তর্ভুক্ত রয়েছে। সেই কোডটি একটি অপ্টিমাইজেশন যা আমরা এখানে উদাহরণটি সহজ করার জন্য অন্তর্ভুক্ত করিনি।
#[macro_export]
অ্যানোটেশনটি নির্দেশ করে যে এই ম্যাক্রোটি যখনই ম্যাক্রোটি সংজ্ঞায়িত করা ক্রেটটি স্কোপে আনা হবে তখনই উপলব্ধ করা উচিত। এই অ্যানোটেশন ছাড়া, ম্যাক্রোটি স্কোপে আনা যায় না।
এরপর আমরা macro_rules!
এবং আমরা যে ম্যাক্রোটি সংজ্ঞায়িত করছি তার নাম ছাড়া বিস্ময় চিহ্ন দিয়ে ম্যাক্রো সংজ্ঞা শুরু করি। নামটি, এই ক্ষেত্রে vec
, এর পরে কোঁকড়া বন্ধনী (curly brackets) থাকে যা ম্যাক্রো সংজ্ঞার বডি বোঝায়।
vec!
বডির গঠনটি একটি match
এক্সপ্রেশনের গঠনের মতোই। এখানে আমাদের একটি আর্ম আছে যার প্যাটার্ন ( $( $x:expr ),* )
, এর পরে =>
এবং এই প্যাটার্নের সাথে যুক্ত কোডের ব্লক। যদি প্যাটার্নটি ম্যাচ করে, তবে সংশ্লিষ্ট কোডের ব্লকটি নির্গত হবে। যেহেতু এটি এই ম্যাক্রোতে একমাত্র প্যাটার্ন, তাই ম্যাচ করার একমাত্র বৈধ উপায় আছে; অন্য কোনো প্যাটার্ন একটি ত্রুটির কারণ হবে। আরও জটিল ম্যাক্রোগুলোর একাধিক আর্ম থাকবে।
ম্যাক্রো সংজ্ঞায় বৈধ প্যাটার্ন সিনট্যাক্স চ্যাপ্টার ১৯-এ কভার করা প্যাটার্ন সিনট্যাক্স থেকে ভিন্ন কারণ ম্যাক্রো প্যাটার্নগুলো মানের পরিবর্তে রাস্ট কোড কাঠামোর সাথে ম্যাচ করা হয়। চলুন লিস্টিং ২০-২৯-এর প্যাটার্নের অংশগুলোর অর্থ কী তা দেখি; সম্পূর্ণ ম্যাক্রো প্যাটার্ন সিনট্যাক্সের জন্য, রাস্ট রেফারেন্স দেখুন।
প্রথমে আমরা পুরো প্যাটার্নটি ঘিরে রাখার জন্য একজোড়া বন্ধনী ব্যবহার করি। আমরা ম্যাক্রো সিস্টেমে একটি ভেরিয়েবল ঘোষণা করার জন্য একটি ডলার চিহ্ন ($
) ব্যবহার করি যা প্যাটার্নের সাথে ম্যাচ করা রাস্ট কোড ধারণ করবে। ডলার চিহ্নটি স্পষ্ট করে দেয় যে এটি একটি ম্যাক্রো ভেরিয়েবল, একটি সাধারণ রাস্ট ভেরিয়েবল নয়। এর পরে একজোড়া বন্ধনী আসে যা প্রতিস্থাপন কোডে ব্যবহারের জন্য বন্ধনীর মধ্যে থাকা প্যাটার্নের সাথে ম্যাচ করা মানগুলো ক্যাপচার করে। $()
-এর মধ্যে $x:expr
রয়েছে, যা যেকোনো রাস্ট এক্সপ্রেশনের সাথে ম্যাচ করে এবং এক্সপ্রেশনটিকে $x
নাম দেয়।
$()
-এর পরে থাকা কমাটি নির্দেশ করে যে $()
-এর মধ্যে থাকা কোডের সাথে ম্যাচ করা কোডের প্রতিটি ইনস্ট্যান্সের মধ্যে একটি আক্ষরিক কমা বিভাজক অক্ষর অবশ্যই উপস্থিত থাকতে হবে। *
নির্দিষ্ট করে যে প্যাটার্নটি *
-এর আগে যা কিছু আছে তার শূন্য বা তার বেশি সংখ্যার সাথে ম্যাচ করে।
যখন আমরা vec![1, 2, 3];
দিয়ে এই ম্যাক্রোটি কল করি, $x
প্যাটার্নটি তিনটি এক্সপ্রেশন 1
, 2
, এবং 3
-এর সাথে তিনবার ম্যাচ করে।
এখন আসুন এই আর্মের সাথে যুক্ত কোডের বডিতে প্যাটার্নটি দেখি: $()*
-এর মধ্যে temp_vec.push()
প্যাটার্নে $()
-এর সাথে ম্যাচ করা প্রতিটি অংশের জন্য তৈরি হয়, প্যাটার্নটি কতবার ম্যাচ করে তার উপর নির্ভর করে শূন্য বা তার বেশিবার। $x
প্রতিটি ম্যাচ করা এক্সপ্রেশন দিয়ে প্রতিস্থাপিত হয়। যখন আমরা vec![1, 2, 3];
দিয়ে এই ম্যাক্রোটি কল করি, তখন এই ম্যাক্রো কলটি প্রতিস্থাপনকারী কোডটি নিম্নলিখিত হবে:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
আমরা এমন একটি ম্যাক্রো সংজ্ঞায়িত করেছি যা যেকোনো সংখ্যক আর্গুমেন্ট যেকোনো টাইপের নিতে পারে এবং নির্দিষ্ট উপাদান ধারণকারী একটি ভেক্টর তৈরি করার জন্য কোড তৈরি করতে পারে।
ম্যাক্রো কীভাবে লিখতে হয় সে সম্পর্কে আরও জানতে, অনলাইন ডকুমেন্টেশন বা অন্যান্য রিসোর্স, যেমন ড্যানিয়েল কিপ দ্বারা শুরু করা এবং লুকাস ওয়ার্থ দ্বারা চালিত "The Little Book of Rust Macros" দেখুন।
অ্যাট্রিবিউট থেকে কোড জেনারেট করার জন্য প্রসিডিউরাল ম্যাক্রো
ম্যাক্রোর দ্বিতীয় রূপটি হলো প্রসিডিউরাল ম্যাক্রো, যা একটি ফাংশনের মতো কাজ করে (এবং এটি এক ধরণের প্রসিডিউর)। প্রসিডিউরাল ম্যাক্রো (Procedural macros) ইনপুট হিসাবে কিছু কোড গ্রহণ করে, সেই কোডের উপর কাজ করে এবং প্যাটার্নের সাথে ম্যাচ করে কোডকে অন্য কোড দিয়ে প্রতিস্থাপন করার পরিবর্তে আউটপুট হিসাবে কিছু কোড তৈরি করে, যেমনটা ডিক্লারেটিভ ম্যাক্রোগুলো করে। তিন ধরণের প্রসিডিউরাল ম্যাক্রো হলো কাস্টম derive
, অ্যাট্রিবিউট-লাইক এবং ফাংশন-লাইক, এবং সবগুলোই একই রকমভাবে কাজ করে।
প্রসিডিউরাল ম্যাক্রো তৈরি করার সময়, সংজ্ঞাগুলো অবশ্যই একটি বিশেষ ক্রেট টাইপ সহ তাদের নিজস্ব ক্রেটে থাকতে হবে। এটি জটিল প্রযুক্তিগত কারণে যা আমরা ভবিষ্যতে দূর করার আশা করি। লিস্টিং ২০-৩৬-এ, আমরা দেখাই কীভাবে একটি প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করতে হয়, যেখানে some_attribute
একটি নির্দিষ্ট ম্যাক্রো ভ্যারাইটি ব্যবহারের জন্য একটি প্লেসহোল্ডার।
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
যে ফাংশনটি একটি প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করে তা ইনপুট হিসাবে একটি TokenStream
নেয় এবং আউটপুট হিসাবে একটি TokenStream
তৈরি করে। TokenStream
টাইপটি proc_macro
ক্রেট দ্বারা সংজ্ঞায়িত করা হয়েছে যা রাস্টের সাথে অন্তর্ভুক্ত এবং টোকেনের একটি ক্রম প্রতিনিধিত্ব করে। এটি ম্যাক্রোর মূল: যে সোর্স কোডের উপর ম্যাক্রোটি কাজ করছে তা ইনপুট TokenStream
তৈরি করে, এবং ম্যাক্রো যে কোড তৈরি করে তা আউটপুট TokenStream
। ফাংশনটির সাথে একটি অ্যাট্রিবিউটও সংযুক্ত থাকে যা নির্দিষ্ট করে যে আমরা কোন ধরণের প্রসিডিউরাল ম্যাক্রো তৈরি করছি। আমরা একই ক্রেটে একাধিক ধরণের প্রসিডিউরাল ম্যাক্রো রাখতে পারি।
আসুন বিভিন্ন ধরণের প্রসিডিউরাল ম্যাক্রোগুলো দেখি। আমরা একটি কাস্টম derive
ম্যাক্রো দিয়ে শুরু করব এবং তারপর ছোটখাটো ভিন্নতাগুলো ব্যাখ্যা করব যা অন্যান্য রূপগুলোকে ভিন্ন করে তোলে।
কীভাবে একটি কাস্টম derive
ম্যাক্রো লিখবেন
আসুন hello_macro
নামে একটি ক্রেট তৈরি করি যা HelloMacro
নামে একটি trait সংজ্ঞায়িত করে যার একটি associated function hello_macro
আছে। আমাদের ব্যবহারকারীদের তাদের প্রতিটি টাইপের জন্য HelloMacro
trait ইমপ্লিমেন্ট করতে বাধ্য করার পরিবর্তে, আমরা একটি প্রসিডিউরাল ম্যাক্রো সরবরাহ করব যাতে ব্যবহারকারীরা #[derive(HelloMacro)]
দিয়ে তাদের টাইপকে অ্যানোটেট করে hello_macro
ফাংশনের একটি ডিফল্ট ইমপ্লিমেন্টেশন পেতে পারে। ডিফল্ট ইমপ্লিমেন্টেশনটি Hello, Macro! My name is TypeName!
প্রিন্ট করবে যেখানে TypeName
হলো সেই টাইপের নাম যার উপর এই trait-টি সংজ্ঞায়িত করা হয়েছে। অন্য কথায়, আমরা এমন একটি ক্রেট লিখব যা অন্য প্রোগ্রামারকে আমাদের ক্রেট ব্যবহার করে লিস্টিং ২০-৩৭-এর মতো কোড লিখতে সক্ষম করবে।
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
এই কোডটি যখন আমরা শেষ করব তখন Hello, Macro! My name is Pancakes!
প্রিন্ট করবে। প্রথম ধাপ হলো একটি নতুন লাইব্রেরি ক্রেট তৈরি করা, এইভাবে:
$ cargo new hello_macro --lib
এরপরে, লিস্টিং ২০-৩৮-এ, আমরা HelloMacro
trait এবং এর associated function সংজ্ঞায়িত করব।
pub trait HelloMacro {
fn hello_macro();
}
আমাদের একটি trait এবং তার ফাংশন আছে। এই মুহূর্তে, আমাদের ক্রেট ব্যবহারকারী কাঙ্ক্ষিত কার্যকারিতা অর্জনের জন্য trait-টি ইমপ্লিমেন্ট করতে পারে, যেমনটি লিস্টিং ২০-৩৯-এ দেখানো হয়েছে।
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
তবে, তাদের প্রতিটি টাইপের জন্য ইমপ্লিমেন্টেশন ব্লক লিখতে হতো যা তারা hello_macro
-এর সাথে ব্যবহার করতে চায়; আমরা তাদের এই কাজটি করা থেকে বাঁচাতে চাই।
উপরন্তু, আমরা এখনও hello_macro
ফাংশনটিকে ডিফল্ট ইমপ্লিমেন্টেশন দিয়ে সরবরাহ করতে পারি না যা trait-টি ইমপ্লিমেন্ট করা টাইপের নাম প্রিন্ট করবে: রাস্টের রিফ্লেকশন ক্ষমতা নেই, তাই এটি রানটাইমে টাইপের নাম দেখতে পারে না। আমাদের কম্পাইল টাইমে কোড জেনারেট করার জন্য একটি ম্যাক্রো প্রয়োজন।
পরবর্তী ধাপ হলো প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করা। এই লেখার সময়, প্রসিডিউরাল ম্যাক্রোগুলোকে তাদের নিজস্ব ক্রেটে থাকতে হবে। অবশেষে, এই সীমাবদ্ধতা তুলে নেওয়া হতে পারে। ক্রেট এবং ম্যাক্রো ক্রেট কাঠামো করার প্রথাটি নিম্নরূপ: foo
নামের একটি ক্রেটের জন্য, একটি কাস্টম derive
প্রসিডিউরাল ম্যাক্রো ক্রেটকে foo_derive
বলা হয়। চলুন আমাদের hello_macro
প্রজেক্টের ভিতরে hello_macro_derive
নামে একটি নতুন ক্রেট শুরু করি:
$ cargo new hello_macro_derive --lib
আমাদের দুটি ক্রেট ঘনিষ্ঠভাবে সম্পর্কিত, তাই আমরা আমাদের hello_macro
ক্রেটের ডিরেক্টরির মধ্যে প্রসিডিউরাল ম্যাক্রো ক্রেট তৈরি করি। যদি আমরা hello_macro
-তে trait সংজ্ঞা পরিবর্তন করি, আমাদের hello_macro_derive
-এ প্রসিডিউরাল ম্যাক্রোর ইমপ্লিমেন্টেশনও পরিবর্তন করতে হবে। দুটি ক্রেটকে আলাদাভাবে প্রকাশ করতে হবে, এবং এই ক্রেটগুলো ব্যবহারকারী প্রোগ্রামারদের উভয়কেই নির্ভরতা হিসাবে যোগ করতে হবে এবং উভয়কেই স্কোপে আনতে হবে। আমরা পরিবর্তে hello_macro
ক্রেটকে hello_macro_derive
-কে একটি নির্ভরতা হিসাবে ব্যবহার করতে এবং প্রসিডিউরাল ম্যাক্রো কোডটি পুনরায় এক্সপোর্ট করতে পারতাম। তবে, আমরা যেভাবে প্রজেক্টটি কাঠামো করেছি তা প্রোগ্রামারদের hello_macro
ব্যবহার করা সম্ভব করে তোলে এমনকি যদি তারা derive
কার্যকারিতা না চায়।
আমাদের hello_macro_derive
ক্রেটটিকে একটি প্রসিডিউরাল ম্যাক্রো ক্রেট হিসাবে ঘোষণা করতে হবে। আমাদের syn
এবং quote
ক্রেট থেকেও কার্যকারিতার প্রয়োজন হবে, যেমন আপনি এক মুহূর্তের মধ্যে দেখতে পাবেন, তাই আমাদের সেগুলোকে নির্ভরতা হিসাবে যোগ করতে হবে। hello_macro_derive
-এর জন্য Cargo.toml ফাইলে নিম্নলিখিতটি যোগ করুন:
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করা শুরু করতে, লিস্টিং ২০-৪০-এর কোডটি আপনার hello_macro_derive
ক্রেটের জন্য src/lib.rs ফাইলে রাখুন। লক্ষ্য করুন যে এই কোডটি যতক্ষণ না আমরা impl_hello_macro
ফাংশনের জন্য একটি সংজ্ঞা যোগ করি ততক্ষণ কম্পাইল হবে না।
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate.
let ast = syn::parse(input).unwrap();
// Build the trait implementation.
impl_hello_macro(&ast)
}
লক্ষ্য করুন যে আমরা কোডটিকে hello_macro_derive
ফাংশনে বিভক্ত করেছি, যা TokenStream
পার্স করার জন্য দায়ী, এবং impl_hello_macro
ফাংশনে, যা সিনট্যাক্স ট্রি রূপান্তর করার জন্য দায়ী: এটি একটি প্রসিডিউরাল ম্যাক্রো লেখা আরও সুবিধাজনক করে তোলে। বাইরের ফাংশনের কোড (hello_macro_derive
এই ক্ষেত্রে) প্রায় প্রতিটি প্রসিডিউরাল ম্যাক্রো ক্রেটের জন্য একই হবে যা আপনি দেখেন বা তৈরি করেন। ভিতরের ফাংশনের বডিতে আপনি যে কোড নির্দিষ্ট করেন (impl_hello_macro
এই ক্ষেত্রে) আপনার প্রসিডিউরাল ম্যাক্রোর উদ্দেশ্যের উপর নির্ভর করে ভিন্ন হবে।
আমরা তিনটি নতুন ক্রেট চালু করেছি: proc_macro
, syn
, এবং quote
। proc_macro
ক্রেটটি রাস্টের সাথে আসে, তাই আমাদের সেটিকে Cargo.toml-এর নির্ভরতাগুলিতে যোগ করার প্রয়োজন ছিল না। proc_macro
ক্রেটটি হলো কম্পাইলারের API যা আমাদের আমাদের কোড থেকে রাস্ট কোড পড়তে এবং ম্যানিপুলেট করতে দেয়।
syn
ক্রেটটি একটি স্ট্রিং থেকে রাস্ট কোডকে একটি ডেটা স্ট্রাকচারে পার্স করে যার উপর আমরা অপারেশন করতে পারি। quote
ক্রেটটি syn
ডেটা স্ট্রাকচারগুলোকে আবার রাস্ট কোডে পরিণত করে। এই ক্রেটগুলো আমাদের যে কোনো ধরণের রাস্ট কোড পার্স করা অনেক সহজ করে তোলে যা আমরা হ্যান্ডেল করতে চাইতে পারি: রাস্ট কোডের জন্য একটি সম্পূর্ণ পার্সার লেখা কোনো সহজ কাজ নয়।
hello_macro_derive
ফাংশনটি তখন কল করা হবে যখন আমাদের লাইব্রেরির একজন ব্যবহারকারী একটি টাইপের উপর #[derive(HelloMacro)]
নির্দিষ্ট করবে। এটি সম্ভব কারণ আমরা এখানে hello_macro_derive
ফাংশনটিকে proc_macro_derive
দিয়ে অ্যানোটেট করেছি এবং HelloMacro
নামটি নির্দিষ্ট করেছি, যা আমাদের trait নামের সাথে মেলে; এটি বেশিরভাগ প্রসিডিউরাল ম্যাক্রোর অনুসরণ করা প্রথা।
hello_macro_derive
ফাংশনটি প্রথমে input
-কে একটি TokenStream
থেকে একটি ডেটা স্ট্রাকচারে রূপান্তর করে যা আমরা তখন ব্যাখ্যা করতে এবং অপারেশন করতে পারি। এখানেই syn
কাজে আসে। syn
-এর parse
ফাংশনটি একটি TokenStream
নেয় এবং পার্স করা রাস্ট কোড প্রতিনিধিত্বকারী একটি DeriveInput
struct রিটার্ন করে। লিস্টিং ২০-৪১ struct Pancakes;
স্ট্রিং পার্স করে আমরা যে DeriveInput
struct পাই তার প্রাসঙ্গিক অংশগুলো দেখায়।
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
এই struct-এর ফিল্ডগুলো দেখায় যে আমরা যে রাস্ট কোডটি পার্স করেছি তা ident
(identifier
, অর্থাৎ নাম) Pancakes
সহ একটি ইউনিট struct। এই struct-এ সব ধরণের রাস্ট কোড বর্ণনা করার জন্য আরও ফিল্ড রয়েছে; আরও তথ্যের জন্য syn
ডকুমেন্টেশনে DeriveInput
দেখুন।
শীঘ্রই আমরা impl_hello_macro
ফাংশনটি সংজ্ঞায়িত করব, যেখানে আমরা যে নতুন রাস্ট কোডটি অন্তর্ভুক্ত করতে চাই তা তৈরি করব। কিন্তু তার আগে, লক্ষ্য করুন যে আমাদের derive
ম্যাক্রোর আউটপুটও একটি TokenStream
। ফেরত দেওয়া TokenStream
-টি আমাদের ক্রেট ব্যবহারকারীদের লেখা কোডে যোগ করা হয়, তাই যখন তারা তাদের ক্রেট কম্পাইল করে, তারা পরিবর্তিত TokenStream
-এ আমাদের সরবরাহ করা অতিরিক্ত কার্যকারিতা পাবে।
আপনি হয়তো লক্ষ্য করেছেন যে আমরা unwrap
কল করছি যাতে syn::parse
ফাংশনে কল ব্যর্থ হলে hello_macro_derive
ফাংশনটি প্যানিক করে। আমাদের প্রসিডিউরাল ম্যাক্রোর ত্রুটির উপর প্যানিক করা প্রয়োজন কারণ proc_macro_derive
ফাংশনগুলোকে প্রসিডিউরাল ম্যাক্রো API-এর সাথে সামঞ্জস্যপূর্ণ হওয়ার জন্য Result
-এর পরিবর্তে TokenStream
রিটার্ন করতে হবে। আমরা unwrap
ব্যবহার করে এই উদাহরণটি সহজ করেছি; প্রোডাকশন কোডে, আপনার panic!
বা expect
ব্যবহার করে কী ভুল হয়েছে সে সম্পর্কে আরও নির্দিষ্ট ত্রুটির বার্তা সরবরাহ করা উচিত।
এখন যেহেতু আমাদের কাছে অ্যানোটেটেড রাস্ট কোডটিকে একটি TokenStream
থেকে একটি DeriveInput
ইনস্ট্যান্সে পরিণত করার কোড রয়েছে, আসুন অ্যানোটেটেড টাইপের উপর HelloMacro
trait ইমপ্লিমেন্ট করে এমন কোড জেনারেট করি, যেমনটি লিস্টিং ২০-৪২-এ দেখানো হয়েছে।
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
generated.into()
}
আমরা ast.ident
ব্যবহার করে অ্যানোটেটেড টাইপের নাম (আইডেন্টিফায়ার) ধারণকারী একটি Ident
struct ইনস্ট্যান্স পাই। লিস্টিং ২০-৪১-এর structটি দেখায় যে যখন আমরা লিস্টিং ২০-৩৭-এর কোডের উপর impl_hello_macro
ফাংশনটি চালাই, তখন আমরা যে ident
পাব তার ident
ফিল্ডে "Pancakes"
মান থাকবে। সুতরাং লিস্টিং ২০-৪২-এর name
ভেরিয়েবলে একটি Ident
struct ইনস্ট্যান্স থাকবে যা প্রিন্ট করলে "Pancakes"
স্ট্রিংটি হবে, যা লিস্টিং ২০-৩৭-এর struct-এর নাম।
quote!
ম্যাক্রো আমাদের সেই রাস্ট কোড সংজ্ঞায়িত করতে দেয় যা আমরা রিটার্ন করতে চাই। কম্পাইলার quote!
ম্যাক্রোর এক্সিকিউশনের সরাসরি ফলাফলের চেয়ে ভিন্ন কিছু আশা করে, তাই আমাদের এটিকে একটি TokenStream
-এ রূপান্তর করতে হবে। আমরা এটি into
মেথড কল করে করি, যা এই মধ্যবর্তী উপস্থাপনাটি গ্রহণ করে এবং প্রয়োজনীয় TokenStream
টাইপের একটি মান রিটার্ন করে।
quote!
ম্যাক্রো কিছু খুব চমৎকার টেমপ্লেটিং মেকানিক্সও সরবরাহ করে: আমরা #name
প্রবেশ করাতে পারি, এবং quote!
এটিকে name
ভেরিয়েবলের মান দিয়ে প্রতিস্থাপন করবে। আপনি এমনকি নিয়মিত ম্যাক্রোগুলো যেভাবে কাজ করে তার মতো কিছু পুনরাবৃত্তিও করতে পারেন। একটি পুঙ্খানুপুঙ্খ পরিচিতির জন্য quote
ক্রেটের ডক্স দেখুন।
আমরা চাই আমাদের প্রসিডিউরাল ম্যাক্রো ব্যবহারকারীর অ্যানোটেট করা টাইপের জন্য আমাদের HelloMacro
trait-এর একটি ইমপ্লিমেন্টেশন জেনারেট করুক, যা আমরা #name
ব্যবহার করে পেতে পারি। trait ইমপ্লিমেন্টেশনের একটি ফাংশন hello_macro
আছে, যার বডিতে আমরা যে কার্যকারিতা সরবরাহ করতে চাই তা রয়েছে: Hello, Macro! My name is
প্রিন্ট করা এবং তারপর অ্যানোটেটেড টাইপের নাম।
এখানে ব্যবহৃত stringify!
ম্যাক্রোটি রাস্টের মধ্যে বিল্ট-ইন। এটি একটি রাস্ট এক্সপ্রেশন নেয়, যেমন 1 + 2
, এবং কম্পাইল টাইমে এক্সপ্রেশনটিকে একটি স্ট্রিং লিটারাল, যেমন "1 + 2"
-তে পরিণত করে। এটি format!
বা println!
ম্যাক্রোগুলো থেকে ভিন্ন, যা এক্সপ্রেশনটি মূল্যায়ন করে এবং তারপর ফলাফলটিকে একটি String
-এ পরিণত করে। এমন একটি সম্ভাবনা রয়েছে যে #name
ইনপুটটি আক্ষরিকভাবে প্রিন্ট করার জন্য একটি এক্সপ্রেশন হতে পারে, তাই আমরা stringify!
ব্যবহার করি। stringify!
ব্যবহার করা কম্পাইল টাইমে #name
-কে একটি স্ট্রিং লিটারালে রূপান্তর করে একটি অ্যালোকেশনও বাঁচায়।
এই মুহূর্তে, cargo build
hello_macro
এবং hello_macro_derive
উভয় ক্ষেত্রেই সফলভাবে সম্পন্ন হওয়া উচিত। চলুন এই ক্রেটগুলোকে লিস্টিং ২০-৩৭-এর কোডের সাথে সংযুক্ত করে প্রসিডিউরাল ম্যাক্রোটি কার্যকর দেখি! আপনার projects ডিরেক্টরিতে cargo new pancakes
ব্যবহার করে একটি নতুন বাইনারি প্রজেক্ট তৈরি করুন। আমাদের pancakes
ক্রেটের Cargo.toml-এ hello_macro
এবং hello_macro_derive
-কে নির্ভরতা হিসেবে যোগ করতে হবে। যদি আপনি আপনার hello_macro
এবং hello_macro_derive
-এর সংস্করণ crates.io-তে প্রকাশ করেন, তবে সেগুলি নিয়মিত নির্ভরতা হবে; যদি না হয়, আপনি সেগুলোকে path
নির্ভরতা হিসেবে নির্দিষ্ট করতে পারেন নিম্নরূপ:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
src/main.rs-এ লিস্টিং ২০-৩৭-এর কোডটি রাখুন, এবং cargo run
চালান: এটি Hello, Macro! My name is Pancakes!
প্রিন্ট করা উচিত। pancakes
ক্রেটকে ইমপ্লিমেন্ট করার প্রয়োজন ছাড়াই প্রসিডিউরাল ম্যাক্রো থেকে HelloMacro
trait-এর ইমপ্লিমেন্টেশন অন্তর্ভুক্ত করা হয়েছিল; #[derive(HelloMacro)]
trait ইমপ্লিমেন্টেশনটি যোগ করেছে।
এরপরে, চলুন দেখি অন্যান্য ধরণের প্রসিডিউরাল ম্যাক্রোগুলো কাস্টম derive
ম্যাক্রো থেকে কীভাবে ভিন্ন।
অ্যাট্রিবিউট-লাইক ম্যাক্রো (Attribute-Like Macros)
অ্যাট্রিবিউট-লাইক ম্যাক্রোগুলো কাস্টম derive
ম্যাক্রোর মতোই, কিন্তু derive
অ্যাট্রিবিউটের জন্য কোড জেনারেট করার পরিবর্তে, তারা আপনাকে নতুন অ্যাট্রিবিউট তৈরি করার সুযোগ দেয়। এগুলি আরও নমনীয়: derive
শুধুমাত্র struct এবং enum-এর জন্য কাজ করে; অ্যাট্রিবিউটগুলো অন্যান্য আইটেম, যেমন ফাংশন-এর উপরও প্রয়োগ করা যেতে পারে। এখানে একটি অ্যাট্রিবিউট-লাইক ম্যাক্রো ব্যবহারের একটি উদাহরণ। ধরুন আপনার route
নামে একটি অ্যাট্রিবিউট আছে যা একটি ওয়েব অ্যাপ্লিকেশন ফ্রেমওয়ার্ক ব্যবহার করার সময় ফাংশনগুলোকে অ্যানোটেট করে:
#[route(GET, "/")]
fn index() {
এই #[route]
অ্যাট্রিবিউটটি ফ্রেমওয়ার্ক দ্বারা একটি প্রসিডিউরাল ম্যাক্রো হিসাবে সংজ্ঞায়িত করা হবে। ম্যাক্রো সংজ্ঞা ফাংশনের সিগনেচারটি এইরকম দেখাবে:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
এখানে, আমাদের TokenStream
টাইপের দুটি প্যারামিটার রয়েছে। প্রথমটি অ্যাট্রিবিউটের বিষয়বস্তুর জন্য: GET, "/"
অংশ। দ্বিতীয়টি হলো সেই আইটেমের বডি যার সাথে অ্যাট্রিবিউটটি সংযুক্ত: এই ক্ষেত্রে, fn index() {}
এবং ফাংশনের বডির বাকি অংশ।
এছাড়া, অ্যাট্রিবিউট-লাইক ম্যাক্রোগুলো কাস্টম derive
ম্যাক্রোর মতোই কাজ করে: আপনি proc-macro
ক্রেট টাইপ সহ একটি ক্রেট তৈরি করেন এবং আপনি যে কোড জেনারেট করতে চান তার জন্য একটি ফাংশন ইমপ্লিমেন্ট করেন!
ফাংশন-লাইক ম্যাক্রো (Function-Like Macros)
ফাংশন-লাইক ম্যাক্রোগুলো এমন ম্যাক্রো সংজ্ঞায়িত করে যা ফাংশন কলের মতো দেখায়। macro_rules!
ম্যাক্রোর মতোই, এগুলি ফাংশনের চেয়ে বেশি নমনীয়; উদাহরণস্বরূপ, তারা একটি অজানা সংখ্যক আর্গুমেন্ট নিতে পারে। তবে, macro_rules!
ম্যাক্রোগুলো শুধুমাত্র ম্যাচ-লাইক সিনট্যাক্স ব্যবহার করে সংজ্ঞায়িত করা যেতে পারে যা আমরা আগে “Declarative Macros with macro_rules!
for General Metaprogramming”-এ আলোচনা করেছি। ফাংশন-লাইক ম্যাক্রোগুলো একটি TokenStream
প্যারামিটার নেয়, এবং তাদের সংজ্ঞা সেই TokenStream
-কে রাস্ট কোড ব্যবহার করে ম্যানিপুলেট করে যেমনটি অন্য দুটি ধরণের প্রসিডিউরাল ম্যাক্রো করে। একটি ফাংশন-লাইক ম্যাক্রোর উদাহরণ হলো একটি sql!
ম্যাক্রো যা এইভাবে কল করা হতে পারে:
let sql = sql!(SELECT * FROM posts WHERE id=1);
এই ম্যাক্রোটি এর ভেতরের SQL স্টেটমেন্টটি পার্স করবে এবং এটি সিনট্যাক্টিক্যালি সঠিক কিনা তা পরীক্ষা করবে, যা একটি macro_rules!
ম্যাক্রো যা করতে পারে তার চেয়ে অনেক বেশি জটিল প্রক্রিয়াকরণ। sql!
ম্যাক্রোটি এইভাবে সংজ্ঞায়িত করা হবে:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
এই সংজ্ঞাটি কাস্টম derive
ম্যাক্রোর সিগনেচারের মতোই: আমরা বন্ধনীর ভেতরের টোকেনগুলো গ্রহণ করি এবং আমরা যে কোড জেনারেট করতে চেয়েছিলাম তা রিটার্ন করি।
সারাংশ (Summary)
এখন আপনার টুলবক্সে কিছু রাস্ট ফিচার রয়েছে যা আপনি সম্ভবত প্রায়শই ব্যবহার করবেন না, তবে আপনি জানবেন যে খুব নির্দিষ্ট পরিস্থিতিতে সেগুলি উপলব্ধ রয়েছে। আমরা বেশ কয়েকটি জটিল বিষয় পরিচয় করিয়ে দিয়েছি যাতে আপনি যখন ত্রুটির বার্তার পরামর্শে বা অন্য লোকের কোডে সেগুলির সম্মুখীন হন, তখন আপনি এই ধারণা এবং সিনট্যাক্সগুলি চিনতে পারবেন। এই অধ্যায়টি আপনাকে সমাধানের দিকে পরিচালিত করার জন্য একটি রেফারেন্স হিসাবে ব্যবহার করুন।
এরপরে, আমরা বই জুড়ে যা কিছু আলোচনা করেছি তা অনুশীলনে প্রয়োগ করব এবং আরও একটি প্রজেক্ট করব!