ম্যাক্রো (Macros)
আমরা এই বই জুড়ে println!
এর মতো ম্যাক্রো ব্যবহার করেছি, কিন্তু ম্যাক্রো কী এবং এটি কীভাবে কাজ করে তা আমরা পুরোপুরি অনুসন্ধান করিনি। ম্যাক্রো শব্দটি Rust-এর একগুচ্ছ ফিচারকে বোঝায়: macro_rules!
দিয়ে ডিক্লেয়ারেটিভ ম্যাক্রো এবং তিন ধরনের প্রোসিডিউরাল ম্যাক্রো:
- কাস্টম
#[derive]
ম্যাক্রো, যা স্ট্রাক্ট এবং এনামে ব্যবহৃতderive
অ্যাট্রিবিউটের সাথে যোগ করা কোড নির্দিষ্ট করে। - অ্যাট্রিবিউট-এর মতো ম্যাক্রো, যা যেকোনো আইটেমে ব্যবহারযোগ্য কাস্টম অ্যাট্রিবিউট সংজ্ঞায়িত করে।
- ফাংশন-এর মতো ম্যাক্রো, যা ফাংশন কলের মতো দেখায় কিন্তু তাদের আর্গুমেন্ট হিসাবে নির্দিষ্ট টোকেনগুলির উপর কাজ করে।
আমরা এগুলির প্রতিটি নিয়ে একে একে আলোচনা করব, কিন্তু প্রথমে দেখা যাক, আমাদের ফাংশন থাকা সত্ত্বেও কেন ম্যাক্রোর প্রয়োজন।
ম্যাক্রো এবং ফাংশনের মধ্যে পার্থক্য
মৌলিকভাবে, ম্যাক্রো হল কোড লেখার একটি উপায় যা অন্য কোড লেখে, যা মেটাপ্রোগ্রামিং নামে পরিচিত। অ্যাপেন্ডিক্স C-তে, আমরা derive
অ্যাট্রিবিউট নিয়ে আলোচনা করি, যা আপনার জন্য বিভিন্ন ট্রেইটের ইমপ্লিমেন্টেশন তৈরি করে। আমরা বই জুড়ে println!
এবং vec!
ম্যাক্রোও ব্যবহার করেছি। এই সমস্ত ম্যাক্রো ম্যানুয়ালি লেখা কোডের চেয়ে বেশি কোড তৈরি করতে বিস্তৃত হয়।
মেটাপ্রোগ্রামিং আপনার লেখা এবং রক্ষণাবেক্ষণ করা কোডের পরিমাণ কমাতে দরকারী, যা ফাংশনেরও অন্যতম ভূমিকা। তবে, ম্যাক্রোগুলির কিছু অতিরিক্ত ক্ষমতা রয়েছে যা ফাংশনগুলির নেই।
একটি ফাংশন সিগনেচারকে অবশ্যই ফাংশনটির প্যারামিটারের সংখ্যা এবং টাইপ ঘোষণা করতে হবে। অন্যদিকে, ম্যাক্রোগুলি পরিবর্তনশীল সংখ্যক প্যারামিটার নিতে পারে: আমরা println!("hello")
কে একটি আর্গুমেন্ট সহ বা println!("hello {}", name)
কে দুটি আর্গুমেন্ট সহ কল করতে পারি। এছাড়াও, কম্পাইলার কোডের অর্থ ব্যাখ্যা করার আগেই ম্যাক্রোগুলি এক্সপান্ড করা হয়, তাই একটি ম্যাক্রো, উদাহরণস্বরূপ, একটি প্রদত্ত টাইপের উপর একটি ট্রেইট ইমপ্লিমেন্ট করতে পারে। একটি ফাংশন তা পারে না, কারণ এটি রানটাইমে কল করা হয় এবং একটি ট্রেইট কম্পাইল করার সময় ইমপ্লিমেন্ট করা প্রয়োজন।
একটি ফাংশনের পরিবর্তে একটি ম্যাক্রো ইমপ্লিমেন্ট করার অসুবিধা হল ম্যাক্রো সংজ্ঞাগুলি ফাংশন সংজ্ঞার চেয়ে বেশি জটিল কারণ আপনি Rust কোড লিখছেন যা Rust কোড লেখে। এই পরোক্ষতার কারণে, ম্যাক্রো সংজ্ঞাগুলি সাধারণত ফাংশন সংজ্ঞার চেয়ে পড়া, বোঝা এবং রক্ষণাবেক্ষণ করা আরও কঠিন।
ম্যাক্রো এবং ফাংশনের মধ্যে আরেকটি গুরুত্বপূর্ণ পার্থক্য হল, আপনি একটি ফাইলে ম্যাক্রো কল করার আগে আপনাকে অবশ্যই ম্যাক্রো ডিফাইন করতে হবে অথবা সেগুলিকে স্কোপের মধ্যে আনতে হবে, যেখানে ফাংশনের ক্ষেত্রে আপনি যেকোনো জায়গায় ডিফাইন করতে এবং যেকোনো জায়গা থেকে কল করতে পারেন।
সাধারণ মেটাপ্রোগ্রামিংয়ের জন্য macro_rules!
সহ ডিক্লেয়ারেটিভ ম্যাক্রো
Rust-এ ম্যাক্রোর সর্বাধিক ব্যবহৃত রূপটি হল ডিক্লেয়ারেটিভ ম্যাক্রো। এগুলিকে কখনও কখনও "ম্যাক্রোস বাই এক্সাম্পল," "macro_rules!
ম্যাক্রো," বা কেবল "ম্যাক্রো" হিসাবেও উল্লেখ করা হয়। তাদের মূলে, ডিক্লেয়ারেটিভ ম্যাক্রোগুলি আপনাকে Rust-এর match
এক্সপ্রেশনের মতো কিছু লিখতে দেয়। ষষ্ঠ অধ্যায়ে আলোচনা করা হয়েছে, match
এক্সপ্রেশন হল কন্ট্রোল স্ট্রাকচার যা একটি এক্সপ্রেশন নেয়, এক্সপ্রেশনের ফলস্বরূপ ভ্যালুটিকে প্যাটার্নের সাথে তুলনা করে এবং তারপর ম্যাচিং প্যাটার্নের সাথে অ্যাসোসিয়েটেড কোডটি চালায়। ম্যাক্রোও একটি ভ্যালুকে প্যাটার্নের সাথে তুলনা করে যা নির্দিষ্ট কোডের সাথে অ্যাসোসিয়েটেড: এই পরিস্থিতিতে, ভ্যালুটি হল ম্যাক্রোতে পাস করা আক্ষরিক Rust সোর্স কোড; প্যাটার্নগুলি সেই সোর্স কোডের কাঠামোর সাথে তুলনা করা হয়; এবং প্রতিটি প্যাটার্নের সাথে অ্যাসোসিয়েটেড কোড, যখন ম্যাচ করে, তখন ম্যাক্রোতে পাস করা কোডটিকে প্রতিস্থাপন করে। এই সব কম্পাইলেশনের সময় ঘটে।
একটি ম্যাক্রো সংজ্ঞায়িত করতে, আপনি 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
, এর পরে ম্যাক্রো সংজ্ঞার বডি নির্দেশ করে কোঁকড়া ধনুর্বন্ধনী রয়েছে।
vec!
বডির গঠনটি match
এক্সপ্রেশনের গঠনের অনুরূপ। এখানে আমাদের একটি আর্ম রয়েছে যার প্যাটার্ন ( $( $x:expr ),* )
, তার পরে =>
এবং এই প্যাটার্নের সাথে অ্যাসোসিয়েটেড কোডের ব্লক রয়েছে। যদি প্যাটার্নটি ম্যাচ করে, তাহলে অ্যাসোসিয়েটেড কোডের ব্লকটি নির্গত হবে। যেহেতু এই ম্যাক্রোতে এটিই একমাত্র প্যাটার্ন, তাই ম্যাচ করার কেবল একটি বৈধ উপায় রয়েছে; অন্য কোনো প্যাটার্ন এরর দেবে। আরও জটিল ম্যাক্রোগুলির একাধিক আর্ম থাকবে।
ম্যাক্রো সংজ্ঞায় বৈধ প্যাটার্ন সিনট্যাক্স ঊনবিংশ অধ্যায়ে আলোচিত প্যাটার্ন সিনট্যাক্সের চেয়ে আলাদা কারণ ম্যাক্রো প্যাটার্নগুলি ভ্যালুগুলির পরিবর্তে Rust কোড কাঠামোর সাথে মেলানো হয়। আসুন লিস্টিং ২০-২৯-এর প্যাটার্নের অংশগুলির অর্থ কী তা নিয়ে আলোচনা করি; সম্পূর্ণ ম্যাক্রো প্যাটার্ন সিনট্যাক্সের জন্য, Rust রেফারেন্স দেখুন।
প্রথমে, আমরা সম্পূর্ণ প্যাটার্নটিকে আবদ্ধ করতে এক সেট প্যারেন্থেসিস ব্যবহার করি। ম্যাক্রো সিস্টেমে একটি ভেরিয়েবল ঘোষণা করতে আমরা একটি ডলার চিহ্ন ($
) ব্যবহার করি, যেখানে প্যাটার্নের সাথে ম্যাচ করা Rust কোড থাকবে। ডলার চিহ্নটি স্পষ্ট করে দেয় যে এটি একটি সাধারণ Rust ভেরিয়েবলের পরিবর্তে একটি ম্যাক্রো ভেরিয়েবল। এরপরে প্যারেন্থেসিসের একটি সেট আসে যা প্রতিস্থাপন কোডে ব্যবহারের জন্য প্যারেন্থেসিসের মধ্যে থাকা প্যাটার্নের সাথে মেলে এমন মানগুলিকে ক্যাপচার করে। $()
-এর মধ্যে $x:expr
রয়েছে, যা যেকোনো Rust এক্সপ্রেশনের সাথে মেলে এবং এক্সপ্রেশনটিকে $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”।
অ্যাট্রিবিউট থেকে কোড তৈরির জন্য প্রোসিডিউরাল ম্যাক্রো
ম্যাক্রোর দ্বিতীয় রূপটি হল প্রোসিডিউরাল ম্যাক্রো, যা একটি ফাংশনের মতো আরও কাজ করে (এবং এটি এক ধরনের procedure)। প্রোসিডিউরাল ম্যাক্রোগুলি কিছু কোডকে ইনপুট হিসাবে গ্রহণ করে, সেই কোডের উপর কাজ করে এবং কিছু কোডকে আউটপুট হিসাবে তৈরি করে, যেখানে ডিক্লেয়ারেটিভ ম্যাক্রোগুলি প্যাটার্নের সাথে মেলে এবং কোডটিকে অন্য কোড দিয়ে প্রতিস্থাপন করে। তিন ধরনের প্রোসিডিউরাল ম্যাক্রো হল কাস্টম ডিরাইভ, অ্যাট্রিবিউট-এর মতো এবং ফাংশন-এর মতো, এবং সবই একইভাবে কাজ করে।
প্রোসিডিউরাল ম্যাক্রো তৈরি করার সময়, সংজ্ঞাগুলি অবশ্যই তাদের নিজস্ব ক্রেটে একটি বিশেষ ক্রেট টাইপ সহ থাকতে হবে। এটি জটিল প্রযুক্তিগত কারণে, যা আমরা ভবিষ্যতে দূর করার আশা করি। লিস্টিং ২০-৩০-এ, আমরা দেখাই কিভাবে একটি প্রোসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করতে হয়, যেখানে some_attribute
হল একটি নির্দিষ্ট ম্যাক্রো বৈচিত্র্য ব্যবহারের জন্য একটি প্লেসহোল্ডার।
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
যে ফাংশনটি একটি প্রোসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করে সেটি একটি TokenStream
কে ইনপুট হিসাবে নেয় এবং একটি TokenStream
কে আউটপুট হিসাবে তৈরি করে। TokenStream
টাইপটি proc_macro
ক্রেট দ্বারা সংজ্ঞায়িত করা হয়েছে যা Rust-এর সাথে অন্তর্ভুক্ত এবং টোকেনগুলির একটি ক্রম উপস্থাপন করে। এটি ম্যাক্রোর মূল: ম্যাক্রো যে সোর্স কোডের উপর কাজ করছে সেটি ইনপুট TokenStream
তৈরি করে এবং ম্যাক্রো যে কোড তৈরি করে তা হল আউটপুট TokenStream
। ফাংশনটির সাথে একটি অ্যাট্রিবিউটও সংযুক্ত রয়েছে যা নির্দিষ্ট করে যে আমরা কোন ধরনের প্রোসিডিউরাল ম্যাক্রো তৈরি করছি। আমরা একই ক্রেটে একাধিক ধরনের প্রোসিডিউরাল ম্যাক্রো রাখতে পারি।
আসুন বিভিন্ন ধরনের প্রোসিডিউরাল ম্যাক্রো দেখি। আমরা একটি কাস্টম ডিরাইভ ম্যাক্রো দিয়ে শুরু করব এবং তারপরে ছোটখাটো অমিলগুলি ব্যাখ্যা করব যা অন্য ফর্মগুলিকে আলাদা করে তোলে।
কীভাবে একটি কাস্টম derive
ম্যাক্রো লিখবেন
আসুন hello_macro
নামে একটি ক্রেট তৈরি করি যা HelloMacro
নামে একটি ট্রেইট সংজ্ঞায়িত করে, যার সাথে hello_macro
নামে একটি অ্যাসোসিয়েটেড ফাংশন রয়েছে। আমাদের ব্যবহারকারীদের তাদের প্রতিটি টাইপের জন্য HelloMacro
ট্রেইট ইমপ্লিমেন্ট করার পরিবর্তে, আমরা একটি প্রোসিডিউরাল ম্যাক্রো সরবরাহ করব যাতে ব্যবহারকারীরা তাদের টাইপকে #[derive(HelloMacro)]
দিয়ে অ্যানোটেট করতে পারে এবং hello_macro
ফাংশনের একটি ডিফল্ট ইমপ্লিমেন্টেশন পেতে পারে। ডিফল্ট ইমপ্লিমেন্টেশনটি Hello, Macro! My name is TypeName!
প্রিন্ট করবে, যেখানে TypeName
হল সেই টাইপের নাম যার উপর এই ট্রেইটটি সংজ্ঞায়িত করা হয়েছে। অন্য কথায়, আমরা একটি ক্রেট লিখব যা অন্য একজন প্রোগ্রামারকে আমাদের ক্রেট ব্যবহার করে লিস্টিং ২০-৩১-এর মতো কোড লিখতে সক্ষম করে।
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
ট্রেইট এবং এর অ্যাসোসিয়েটেড ফাংশন সংজ্ঞায়িত করব:
pub trait HelloMacro {
fn hello_macro();
}
আমাদের একটি ট্রেইট এবং এর ফাংশন রয়েছে। এই মুহুর্তে, আমাদের ক্রেট ব্যবহারকারী পছন্দসই কার্যকারিতা অর্জনের জন্য ট্রেইটটি ইমপ্লিমেন্ট করতে পারে, এইভাবে:
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
ফাংশনটিকে ডিফল্ট ইমপ্লিমেন্টেশন সহ সরবরাহ করতে পারি না যা সেই টাইপের নাম প্রিন্ট করবে যার উপর ট্রেইটটি ইমপ্লিমেন্ট করা হয়েছে: Rust-এর রিফ্লেকশন ক্ষমতা নেই, তাই এটি রানটাইমে টাইপের নাম দেখতে পারে না। আমাদের কম্পাইল করার সময় কোড তৈরি করার জন্য একটি ম্যাক্রো প্রয়োজন।
পরবর্তী ধাপ হল প্রোসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করা। এই লেখার সময়, প্রোসিডিউরাল ম্যাক্রোগুলি তাদের নিজস্ব ক্রেটে থাকা দরকার। অবশেষে, এই সীমাবদ্ধতা তুলে নেওয়া হতে পারে। ক্রেট এবং ম্যাক্রো ক্রেট গঠনের নিয়ম নিম্নরূপ: foo
নামের একটি ক্রেটের জন্য, একটি কাস্টম ডিরাইভ প্রোসিডিউরাল ম্যাক্রো ক্রেটকে foo_derive
বলা হয়। আসুন আমাদের hello_macro
প্রকল্পের ভিতরে hello_macro_derive
নামে একটি নতুন ক্রেট শুরু করি:
$ cargo new hello_macro_derive --lib
আমাদের দুটি ক্রেট ঘনিষ্ঠভাবে সম্পর্কিত, তাই আমরা আমাদের hello_macro
ক্রেটের ডিরেক্টরির মধ্যে প্রোসিডিউরাল ম্যাক্রো ক্রেট তৈরি করি। যদি আমরা hello_macro
-তে ট্রেইট সংজ্ঞা পরিবর্তন করি, তাহলে আমাদের 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
ক্রেটটি Rust-এর সাথে আসে, তাই আমাদের এটিকে Cargo.toml-এ নির্ভরতাগুলিতে যুক্ত করার দরকার ছিল না। proc_macro
ক্রেটটি হল কম্পাইলারের API যা আমাদের কোড থেকে Rust কোড পড়তে এবং ম্যানিপুলেট করার অনুমতি দেয়।
syn
ক্রেট একটি স্ট্রিং থেকে Rust কোডকে একটি ডেটা স্ট্রাকচারে পার্স করে যার উপর আমরা অপারেশন করতে পারি। quote
ক্রেট syn
ডেটা স্ট্রাকচারগুলিকে আবার Rust কোডে পরিণত করে। এই ক্রেটগুলি আমাদের হ্যান্ডেল করতে হতে পারে এমন যেকোনো ধরনের Rust কোড পার্স করা অনেক সহজ করে তোলে: Rust কোডের জন্য একটি সম্পূর্ণ পার্সার লেখা কোনো সহজ কাজ নয়।
যখন আমাদের লাইব্রেরির একজন ব্যবহারকারী একটি টাইপের উপর #[derive(HelloMacro)]
নির্দিষ্ট করে তখন hello_macro_derive
ফাংশনটিকে কল করা হবে। এটি সম্ভব কারণ আমরা এখানে hello_macro_derive
ফাংশনটিকে proc_macro_derive
দিয়ে অ্যানোটেট করেছি এবং HelloMacro
নামটি নির্দিষ্ট করেছি, যা আমাদের ট্রেইটের নামের সাথে মেলে; এটি সেই নিয়ম যা বেশিরভাগ প্রোসিডিউরাল ম্যাক্রো অনুসরণ করে।
hello_macro_derive
ফাংশন প্রথমে input
কে TokenStream
থেকে এমন একটি ডেটা স্ট্রাকচারে রূপান্তর করে যা আমরা ব্যাখ্যা করতে এবং অপারেশন করতে পারি। এখানেই syn
কাজে আসে। syn
-এর parse
ফাংশনটি একটি TokenStream
নেয় এবং পার্স করা Rust কোডকে উপস্থাপন করে এমন একটি DeriveInput
স্ট্রাক্ট রিটার্ন করে। লিস্টিং ২০-৩৩ struct Pancakes;
স্ট্রিং পার্স করে আমরা যে DeriveInput
স্ট্রাক্ট পাই তার প্রাসঙ্গিক অংশগুলি দেখায়:
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
এই স্ট্রাক্টের ফিল্ডগুলি দেখায় যে আমরা যে Rust কোডটি পার্স করেছি সেটি হল Pancakes
-এর ident
(আইডেন্টিফায়ার, অর্থাৎ নাম) সহ একটি ইউনিট স্ট্রাক্ট। এই স্ট্রাক্টের উপর সব ধরনের Rust কোড বর্ণনা করার জন্য আরও ফিল্ড রয়েছে; আরও তথ্যের জন্য syn
ডকুমেন্টেশন DeriveInput
-এর জন্য দেখুন।
শীঘ্রই আমরা impl_hello_macro
ফাংশনটি সংজ্ঞায়িত করব, যেখানে আমরা নতুন Rust কোড তৈরি করব যা আমরা অন্তর্ভুক্ত করতে চাই। কিন্তু তার আগে, মনে রাখবেন যে আমাদের ডিরাইভ ম্যাক্রোর আউটপুটও একটি TokenStream
। রিটার্ন করা TokenStream
আমাদের ক্রেট ব্যবহারকারীদের লেখা কোডে যোগ করা হয়, তাই যখন তারা তাদের ক্রেট কম্পাইল করে, তখন তারা অতিরিক্ত কার্যকারিতা পাবে যা আমরা পরিবর্তিত TokenStream
-এ সরবরাহ করি।
আপনি হয়তো লক্ষ্য করেছেন যে আমরা syn::parse
ফাংশনে কল ব্যর্থ হলে hello_macro_derive
ফাংশনটিকে প্যানিক করার জন্য unwrap
কল করছি। প্রোসিডিউরাল ম্যাক্রো API-এর সাথে সঙ্গতি রাখতে proc_macro_derive
ফাংশনগুলিকে Result
-এর পরিবর্তে TokenStream
রিটার্ন করতে হবে বলে এরর-এ আমাদের প্রোসিডিউরাল ম্যাক্রো প্যানিক করা প্রয়োজন। আমরা unwrap
ব্যবহার করে এই উদাহরণটিকে সরল করেছি; প্রোডাকশন কোডে, আপনার panic!
বা expect
ব্যবহার করে কী ভুল হয়েছে সে সম্পর্কে আরও নির্দিষ্ট এরর মেসেজ সরবরাহ করা উচিত।
এখন আমাদের কাছে অ্যানোটেটেড Rust কোডকে TokenStream
থেকে DeriveInput
ইন্সট্যান্সে পরিণত করার কোড রয়েছে, আসুন সেই কোডটি তৈরি করি যা অ্যানোটেটেড টাইপের উপর HelloMacro
ট্রেইট ইমপ্লিমেন্ট করে, যেমনটি লিস্টিং ২০-৩৪-এ দেখানো হয়েছে।
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
স্ট্রাক্ট ইন্সট্যান্স পাই। লিস্টিং ২০-৩৩-এর স্ট্রাক্টটি দেখায় যে যখন আমরা লিস্টিং ২০-৩১-এর কোডে impl_hello_macro
ফাংশনটি চালাই, তখন আমরা যে ident
পাব তার ident
ফিল্ডে "Pancakes"
-এর একটি ভ্যালু থাকবে। এইভাবে, লিস্টিং ২০-৩৪-এর name
ভেরিয়েবলটিতে একটি Ident
স্ট্রাক্ট ইন্সট্যান্স থাকবে যা প্রিন্ট করার সময় "Pancakes"
স্ট্রিং হবে, লিস্টিং ২০-৩১-এর স্ট্রাক্টের নাম।
quote!
ম্যাক্রো আমাদের Rust কোড সংজ্ঞায়িত করতে দেয় যা আমরা রিটার্ন করতে চাই। কম্পাইলার quote!
ম্যাক্রোর সরাসরি ফলাফলের থেকে আলাদা কিছু আশা করে, তাই আমাদের এটিকে একটি TokenStream
-এ রূপান্তর করতে হবে। আমরা এটি into
মেথড কল করে করি, যা এই মধ্যবর্তী উপস্থাপনাকে গ্রাস করে এবং প্রয়োজনীয় TokenStream
টাইপের একটি ভ্যালু রিটার্ন করে।
quote!
ম্যাক্রো কিছু খুব সুন্দর টেমপ্লেটিং মেকানিক্সও সরবরাহ করে: আমরা #name
লিখতে পারি, এবং quote!
এটিকে name
ভেরিয়েবলের ভ্যালু দিয়ে প্রতিস্থাপন করবে। আপনি নিয়মিত ম্যাক্রোর মতো একইভাবে কিছু পুনরাবৃত্তিও করতে পারেন। একটি পুঙ্খানুপুঙ্খ ভূমিকার জন্য quote
ক্রেটের ডক্স দেখুন।
আমরা চাই আমাদের প্রোসিডিউরাল ম্যাক্রো ব্যবহারকারীর অ্যানোটেটেড টাইপের জন্য আমাদের HelloMacro
ট্রেইটের একটি ইমপ্লিমেন্টেশন তৈরি করুক, যা আমরা #name
ব্যবহার করে পেতে পারি। ট্রেইট ইমপ্লিমেন্টেশনে hello_macro
নামে একটি ফাংশন রয়েছে, যার বডিতে আমরা যে কার্যকারিতা সরবরাহ করতে চাই তা রয়েছে: Hello, Macro! My name is
এবং তারপর অ্যানোটেটেড টাইপের নাম প্রিন্ট করা।
এখানে ব্যবহৃত stringify!
ম্যাক্রোটি Rust-এ বিল্ট-ইন। এটি 1 + 2
-এর মতো একটি Rust এক্সপ্রেশন নেয় এবং কম্পাইল করার সময় এক্সপ্রেশনটিকে "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
যোগ করতে হবে। আপনি যদি crates.io-তে hello_macro
এবং hello_macro_derive
-এর আপনার সংস্করণ প্রকাশ করেন, তাহলে সেগুলি নিয়মিত নির্ভরতা হবে; যদি না হয়, আপনি সেগুলিকে path
নির্ভরতা হিসাবে নিম্নরূপ নির্দিষ্ট করতে পারেন:
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!
প্রিন্ট করবে। প্রোসিডিউরাল ম্যাক্রো থেকে HelloMacro
ট্রেইটের ইমপ্লিমেন্টেশনটি pancakes
ক্রেটকে এটি ইমপ্লিমেন্ট করার প্রয়োজন ছাড়াই অন্তর্ভুক্ত করা হয়েছিল; #[derive(HelloMacro)]
ট্রেইট ইমপ্লিমেন্টেশন যোগ করেছে।
এরপরে, আসুন অন্বেষণ করি কিভাবে অন্যান্য ধরনের প্রোসিডিউরাল ম্যাক্রোগুলি কাস্টম ডিরাইভ ম্যাক্রোগুলি থেকে আলাদা।
অ্যাট্রিবিউট-এর মতো ম্যাক্রো
অ্যাট্রিবিউট-এর মতো ম্যাক্রো কাস্টম ডিরাইভ ম্যাক্রোর মতোই, কিন্তু derive
অ্যাট্রিবিউটের জন্য কোড তৈরি করার পরিবর্তে, তারা আপনাকে নতুন অ্যাট্রিবিউট তৈরি করতে দেয়। এগুলি আরও flexible: derive
শুধুমাত্র স্ট্রাক্ট এবং এনামের জন্য কাজ করে; অ্যাট্রিবিউটগুলি অন্যান্য আইটেমগুলিতেও প্রয়োগ করা যেতে পারে, যেমন ফাংশন। অ্যাট্রিবিউট-এর মতো ম্যাক্রো ব্যবহারের একটি উদাহরণ এখানে দেওয়া হল: ধরুন আপনার কাছে route
নামে একটি অ্যাট্রিবিউট রয়েছে যা একটি ওয়েব অ্যাপ্লিকেশন ফ্রেমওয়ার্ক ব্যবহার করার সময় ফাংশনগুলিকে অ্যানোটেট করে:
#[route(GET, "/")]
fn index() {
এই #[route]
অ্যাট্রিবিউটটি ফ্রেমওয়ার্ক দ্বারা একটি প্রোসিডিউরাল ম্যাক্রো হিসাবে সংজ্ঞায়িত করা হবে। ম্যাক্রো সংজ্ঞা ফাংশনের স্বাক্ষরটি এইরকম দেখতে হবে:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
এখানে, আমাদের TokenStream
টাইপের দুটি প্যারামিটার রয়েছে। প্রথমটি অ্যাট্রিবিউটের কনটেন্টের জন্য: GET, "/"
অংশ। দ্বিতীয়টি অ্যাট্রিবিউটটি যে আইটেমের সাথে সংযুক্ত তার বডির জন্য: এই ক্ষেত্রে, fn index() {}
এবং ফাংশনের বডির বাকি অংশ।
এছাড়া, অ্যাট্রিবিউট-এর মতো ম্যাক্রো কাস্টম ডিরাইভ ম্যাক্রোর মতোই কাজ করে: আপনি proc-macro
ক্রেট টাইপ সহ একটি ক্রেট তৈরি করেন এবং একটি ফাংশন ইমপ্লিমেন্ট করেন যা আপনার ইচ্ছামতো কোড তৈরি করে!
ফাংশন-এর মতো ম্যাক্রো
ফাংশন-এর মতো ম্যাক্রো সেই ম্যাক্রোগুলিকে সংজ্ঞায়িত করে যা ফাংশন কলের মতো দেখায়। macro_rules!
ম্যাক্রোর মতোই, এগুলি ফাংশনের চেয়ে বেশি flexible; উদাহরণস্বরূপ, তারা অজানা সংখ্যক আর্গুমেন্ট নিতে পারে। যাইহোক, macro_rules!
ম্যাক্রোগুলি শুধুমাত্র সেই ম্যাচ-এর মতো সিনট্যাক্স ব্যবহার করে সংজ্ঞায়িত করা যেতে পারে যা আমরা আগে “সাধারণ মেটাপ্রোগ্রামিংয়ের জন্য macro_rules!
সহ ডিক্লেয়ারেটিভ ম্যাক্রো” তে আলোচনা করেছি। ফাংশন-এর মতো ম্যাক্রোগুলি একটি TokenStream
প্যারামিটার নেয় এবং তাদের সংজ্ঞা অন্য দুটি ধরণের প্রোসিডিউরাল ম্যাক্রোর মতোই Rust কোড ব্যবহার করে সেই TokenStream
-কে ম্যানিপুলেট করে। একটি ফাংশন-এর মতো ম্যাক্রোর একটি উদাহরণ হল একটি sql!
ম্যাক্রো, যাকে এইভাবে কল করা যেতে পারে:
let sql = sql!(SELECT * FROM posts WHERE id=1);
এই ম্যাক্রোটি এর ভিতরের SQL স্টেটমেন্টটিকে পার্স করবে এবং এটি সিনট্যাক্টিকভাবে সঠিক কিনা তা পরীক্ষা করবে, যা macro_rules!
ম্যাক্রো যা করতে পারে তার চেয়ে অনেক বেশি জটিল প্রসেসিং। sql!
ম্যাক্রোটিকে এইভাবে সংজ্ঞায়িত করা হবে:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
এই সংজ্ঞাটি কাস্টম ডিরাইভ ম্যাক্রোর স্বাক্ষরের মতোই: আমরা প্যারেন্থেসিসের ভিতরের টোকেনগুলি পাই এবং আমরা যে কোড তৈরি করতে চেয়েছিলাম তা রিটার্ন করি।
সারসংক্ষেপ
বাহ! এখন আপনার টুলবক্সে কিছু Rust ফিচার রয়েছে যা আপনি সম্ভবত প্রায়শই ব্যবহার করবেন না, তবে আপনি জানবেন যে সেগুলি খুব বিশেষ পরিস্থিতিতে উপলব্ধ। আমরা বেশ কয়েকটি জটিল বিষয় উপস্থাপন করেছি যাতে আপনি যখন এরর মেসেজের সাজেশনগুলিতে বা অন্য লোকেদের কোডে এগুলির মুখোমুখি হন, তখন আপনি এই ধারণা এবং সিনট্যাক্সগুলি চিনতে পারবেন। সমাধানগুলিতে আপনাকে গাইড করতে এই অধ্যায়টিকে একটি রেফারেন্স হিসাবে ব্যবহার করুন।
এরপরে, আমরা বই জুড়ে যা আলোচনা করেছি তার সবকিছু অনুশীলনে প্রয়োগ করব এবং আরও একটি প্রোজেক্ট করব!