match
কন্ট্রোল ফ্লো কনস্ট্রাক্ট
Rust-এ match
নামে একটি অত্যন্ত শক্তিশালী কন্ট্রোল ফ্লো কনস্ট্রাক্ট আছে যা আপনাকে একটি মানকে একাধিক প্যাটার্নের সাথে তুলনা করতে এবং কোন প্যাটার্নটি মেলে তার উপর ভিত্তি করে কোড এক্সিকিউট করতে দেয়। প্যাটার্নগুলো লিটারেল ভ্যালু, ভেরিয়েবলের নাম, ওয়াইল্ডকার্ড এবং আরও অনেক কিছু দিয়ে তৈরি হতে পারে; Chapter 19-এ বিভিন্ন ধরণের প্যাটার্ন এবং তাদের কাজ সম্পর্কে আলোচনা করা হয়েছে। match
-এর আসল শক্তি হলো এর প্যাটার্নগুলোর প্রকাশক্ষমতা (expressiveness) এবং compiler এটা নিশ্চিত করে যে সমস্ত সম্ভাব্য কেস হ্যান্ডেল করা হয়েছে।
match
এক্সপ্রেশনকে একটি কয়েন বাছাই করার মেশিনের মতো ভাবুন: কয়েনগুলো বিভিন্ন আকারের ছিদ্রযুক্ত একটি ট্র্যাকের উপর দিয়ে স্লাইড করে, এবং প্রতিটি কয়েন প্রথম যে ছিদ্রের সাথে ফিট করে সেটির মধ্যে পড়ে যায়। একইভাবে, match
-এর প্রতিটি প্যাটার্নের মধ্য দিয়ে মানগুলো যায়, এবং প্রথম যে প্যাটার্নের সাথে মানটি "ফিট" করে, সেই মানটি সংশ্লিষ্ট কোড ব্লকে পড়ে যায় এবং এক্সিকিউশনের সময় ব্যবহৃত হয়।
কয়েনের কথা যেহেতু উঠলই, চলুন match
ব্যবহার করে কয়েনকে উদাহরণ হিসাবে ব্যবহার করি! আমরা এমন একটি ফাংশন লিখতে পারি যা একটি অজানা US কয়েন নেয় এবং কাউন্টিং মেশিনের মতো নির্ধারণ করে যে এটি কোন কয়েন এবং সেন্টে এর মান ফেরত দেয়, যেমনটি লিস্টিং ৬-৩ এ দেখানো হয়েছে।
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
চলুন value_in_cents
ফাংশনের match
অংশটি ভেঙে দেখি। প্রথমে আমরা match
কীওয়ার্ডটি লিখি, যার পরে একটি এক্সপ্রেশন থাকে, এক্ষেত্রে coin
ভ্যালুটি। এটি if
-এর সাথে ব্যবহৃত কন্ডিশনাল এক্সপ্রেশনের মতোই মনে হতে পারে, কিন্তু একটি বড় পার্থক্য আছে: if
-এর ক্ষেত্রে কন্ডিশনটিকে একটি বুলিয়ান (boolean) ভ্যালু হতে হয়, কিন্তু এখানে এটি যেকোনো type-এর হতে পারে। এই উদাহরণে coin
-এর type হলো Coin
enum, যা আমরা প্রথম লাইনে ডিফাইন করেছি।
এরপর আসে match
arm। একটি arm-এর দুটি অংশ থাকে: একটি প্যাটার্ন এবং কিছু কোড। এখানের প্রথম arm-টির প্যাটার্ন হলো Coin::Penny
এবং তারপরে =>
অপারেটর যা প্যাটার্ন এবং চালানোর জন্য কোডকে আলাদা করে। এই ক্ষেত্রে কোডটি শুধু 1
ভ্যালুটি। প্রতিটি arm কমা দ্বারা পরবর্তী arm থেকে পৃথক করা হয়।
যখন match
এক্সপ্রেশনটি এক্সিকিউট হয়, এটি তার ফলাফলের মানকে প্রতিটি arm-এর প্যাটার্নের সাথে ক্রমানুসারে তুলনা করে। যদি একটি প্যাটার্ন মানের সাথে মিলে যায়, তবে সেই প্যাটার্নের সাথে যুক্ত কোডটি এক্সিকিউট হয়। যদি সেই প্যাটার্নটি মানের সাথে না মেলে, এক্সিকিউশন পরবর্তী arm-এ চলে যায়, অনেকটা কয়েন বাছাই করার মেশিনের মতো। আমাদের যতগুলো প্রয়োজন ততগুলো arm থাকতে পারে: লিস্টিং ৬-৩-এ, আমাদের match
-এর চারটি arm আছে।
প্রতিটি arm-এর সাথে যুক্ত কোড একটি এক্সপ্রেশন, এবং ম্যাচিং arm-এর এক্সপ্রেশনের ফলাফলই পুরো match
এক্সপ্রেশনের রিটার্ন ভ্যালু হিসাবে ফেরত আসে।
যখন ম্যাচ arm-এর কোড ছোট হয়, যেমন লিস্টিং ৬-৩-এ যেখানে প্রতিটি arm শুধু একটি ভ্যালু রিটার্ন করে, তখন আমরা সাধারণত কার্লি ব্র্যাকেট ব্যবহার করি না। যদি আপনি একটি ম্যাচ arm-এ একাধিক লাইন কোড চালাতে চান, তবে আপনাকে অবশ্যই কার্লি ব্র্যাকেট ব্যবহার করতে হবে, এবং সেক্ষেত্রে arm-এর পরে কমা দেওয়াটা ঐচ্ছিক। উদাহরণস্বরূপ, নিচের কোডটি যখনই একটি Coin::Penny
দিয়ে মেথডটি কল করা হয়, তখন "Lucky penny!" প্রিন্ট করে, কিন্তু ব্লকের শেষ মান, 1
, রিটার্ন করে:
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
ভ্যালুর সাথে বাইন্ড হওয়া প্যাটার্ন
match
arm-এর আরেকটি দরকারী বৈশিষ্ট্য হলো যে তারা প্যাটার্নের সাথে মিলে যাওয়া মানগুলোর অংশে বাইন্ড হতে পারে। এভাবেই আমরা enum ভ্যারিয়েন্ট থেকে মান বের করতে পারি।
উদাহরণস্বরূপ, চলুন আমাদের enum ভ্যারিয়েন্টগুলোর একটিকে পরিবর্তন করে তার ভিতরে ডেটা রাখি। ১৯৯৯ থেকে ২০০৮ সাল পর্যন্ত, মার্কিন যুক্তরাষ্ট্র ৫০টি রাজ্যের জন্য একপাশে বিভিন্ন ডিজাইন সহ কোয়ার্টার তৈরি করেছিল। অন্য কোনো কয়েনে রাজ্যের ডিজাইন ছিল না, তাই শুধুমাত্র কোয়ার্টারেই এই অতিরিক্ত মানটি রয়েছে। আমরা আমাদের enum
-এ এই তথ্য যোগ করতে পারি Quarter
ভ্যারিয়েন্টটিকে পরিবর্তন করে এর ভিতরে একটি UsState
ভ্যালু অন্তর্ভুক্ত করে, যা আমরা লিস্টিং ৬-৪-এ করেছি।
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() {}
আসুন কল্পনা করি যে একজন বন্ধু ৫০টি রাজ্যের সমস্ত কোয়ার্টার সংগ্রহ করার চেষ্টা করছে। আমরা যখন আমাদের খুচরা পয়সাগুলো কয়েনের ধরন অনুযায়ী সাজাব, তখন আমরা প্রতিটি কোয়ার্টারের সাথে যুক্ত রাজ্যের নামও ঘোষণা করব যাতে যদি আমাদের বন্ধুর সংগ্রহে সেটি না থাকে, তবে সে তার সংগ্রহে এটি যোগ করতে পারে।
এই কোডের match
এক্সপ্রেশনে, আমরা Coin::Quarter
ভ্যারিয়েন্টের মানের সাথে মিলে যাওয়া প্যাটার্নে state
নামে একটি ভেরিয়েবল যোগ করি। যখন একটি Coin::Quarter
মেলে, state
ভেরিয়েবলটি সেই কোয়ার্টারের রাজ্যের মানের সাথে বাইন্ড হবে। তারপর আমরা সেই state
ভেরিয়েবলটি সেই arm-এর কোডে ব্যবহার করতে পারি, এভাবে:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {state:?}!"); 25 } } } fn main() { value_in_cents(Coin::Quarter(UsState::Alaska)); }
যদি আমরা value_in_cents(Coin::Quarter(UsState::Alaska))
কল করি, coin
-এর মান হবে Coin::Quarter(UsState::Alaska)
। যখন আমরা সেই মানটি প্রতিটি match
arm-এর সাথে তুলনা করি, Coin::Quarter(state)
-এ না পৌঁছানো পর্যন্ত কোনোটিই মেলে না। সেই সময়ে, state
-এর জন্য বাইন্ডিং হবে UsState::Alaska
মানটি। আমরা তখন সেই বাইন্ডিংটি println!
এক্সপ্রেশনে ব্যবহার করতে পারি, যার ফলে Coin
enum-এর Quarter
ভ্যারিয়েন্ট থেকে ভিতরের রাজ্যের মানটি বের করে আনা যায়।
Option<T>
এর সাথে ম্যাচিং
আগের বিভাগে, আমরা Option<T>
ব্যবহার করার সময় Some
কেস থেকে ভিতরের T
মানটি বের করতে চেয়েছিলাম; আমরা Option<T>
-কেও match
ব্যবহার করে হ্যান্ডেল করতে পারি, যেমনটি আমরা Coin
enum-এর সাথে করেছিলাম! কয়েন তুলনা করার পরিবর্তে, আমরা Option<T>
-এর ভ্যারিয়েন্টগুলো তুলনা করব, কিন্তু match
এক্সপ্রেশনের কাজ করার পদ্ধতি একই থাকে।
ধরুন আমরা একটি ফাংশন লিখতে চাই যা একটি Option<i32>
নেয় এবং যদি ভিতরে একটি মান থাকে, তবে সেই মানের সাথে ১ যোগ করে। যদি ভিতরে কোনো মান না থাকে, ফাংশনটি None
মান ফেরত দেবে এবং কোনো অপারেশন করার চেষ্টা করবে না।
match
-এর সৌজন্যে এই ফাংশনটি লেখা খুব সহজ, এবং এটি লিস্টিং ৬-৫-এর মতো দেখাবে।
fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); }
আসুন plus_one
-এর প্রথম এক্সিকিউশনটি আরও বিস্তারিতভাবে পরীক্ষা করি। যখন আমরা plus_one(five)
কল করি, plus_one
-এর বডিতে x
ভেরিয়েবলের মান হবে Some(5)
। তারপর আমরা সেটিকে প্রতিটি match
arm-এর সাথে তুলনা করি:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5)
মানটি None
প্যাটার্নের সাথে মেলে না, তাই আমরা পরবর্তী arm-এ যাই:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5)
কি Some(i)
-এর সাথে মেলে? হ্যাঁ, মেলে! আমাদের একই ভ্যারিয়েন্ট আছে। i
ভেরিয়েবলটি Some
-এর ভিতরের মানের সাথে বাইন্ড হয়, তাই i
-এর মান 5
হয়। ম্যাচ arm-এর কোডটি তখন এক্সিকিউট হয়, তাই আমরা i
-এর মানের সাথে ১ যোগ করি এবং আমাদের মোট 6
-কে ভিতরে নিয়ে একটি নতুন Some
মান তৈরি করি।
এবার লিস্টিং ৬-৫-এ plus_one
-এর দ্বিতীয় কলটি বিবেচনা করা যাক, যেখানে x
হলো None
। আমরা match
-এ প্রবেশ করি এবং প্রথম arm-এর সাথে তুলনা করি:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
এটা মিলে গেছে! যোগ করার জন্য কোনো মান নেই, তাই প্রোগ্রামটি থেমে যায় এবং =>
-এর ডান পাশের None
মানটি রিটার্ন করে। যেহেতু প্রথম arm-টি মিলে গেছে, অন্য কোনো arm-এর সাথে আর তুলনা করা হয় না।
match
এবং enum একত্রিত করা অনেক পরিস্থিতিতে দরকারী। আপনি Rust কোডে এই প্যাটার্নটি প্রায়শই দেখবেন: একটি enum-এর উপর match
করা, ভিতরের ডেটার সাথে একটি ভেরিয়েবল বাইন্ড করা, এবং তারপর তার উপর ভিত্তি করে কোড চালানো। প্রথমে এটি কিছুটা জটিল মনে হতে পারে, কিন্তু একবার আপনি এতে অভ্যস্ত হয়ে গেলে, আপনার মনে হবে যদি সব ভাষাতেই এটি থাকত। এটি ব্যবহারকারীদের মধ্যে ধারাবাহিকভাবে একটি প্রিয় ফিচার।
ম্যাচগুলো সম্পূর্ণ (Exhaustive) হতে হয়
match
-এর আরও একটি দিক নিয়ে আমাদের আলোচনা করতে হবে: arm-এর প্যাটার্নগুলোকে অবশ্যই সমস্ত সম্ভাবনাকে কভার করতে হবে। আমাদের plus_one
ফাংশনের এই সংস্করণটি বিবেচনা করুন, যাতে একটি বাগ আছে এবং এটি কম্পাইল হবে না:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
আমরা None
কেসটি হ্যান্ডেল করিনি, তাই এই কোডটি একটি বাগ তৈরি করবে। সৌভাগ্যবশত, এটি এমন একটি বাগ যা Rust ধরতে জানে। যদি আমরা এই কোডটি কম্পাইল করার চেষ্টা করি, আমরা এই error পাব:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:572:1
::: /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:576:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust জানে যে আমরা প্রতিটি সম্ভাব্য কেস কভার করিনি, এবং এমনকি কোন প্যাটার্নটি আমরা ভুলে গেছি সেটাও জানে! Rust-এর ম্যাচগুলো exhaustive বা সম্পূর্ণ: কোডটি বৈধ হওয়ার জন্য আমাদের অবশ্যই প্রতিটি সম্ভাবনাকে শেষ করতে হবে। বিশেষ করে Option<T>
-এর ক্ষেত্রে, যখন Rust আমাদের None
কেসটি স্পষ্টভাবে হ্যান্ডেল করতে ভুলে যাওয়া থেকে বিরত রাখে, তখন এটি আমাদের এমন একটি মান আছে বলে ধরে নেওয়া থেকে রক্ষা করে যখন আমাদের কাছে null
থাকতে পারে, যার ফলে আগে আলোচিত বিলিয়ন-ডলারের ভুলটি অসম্ভব হয়ে যায়।
ক্যাচ-অল প্যাটার্ন এবং _
প্লেসহোল্ডার
enum ব্যবহার করে, আমরা কয়েকটি নির্দিষ্ট মানের জন্য বিশেষ পদক্ষেপ নিতে পারি, কিন্তু অন্য সব মানের জন্য একটি ডিফল্ট পদক্ষেপ নিতে পারি। কল্পনা করুন আমরা একটি গেম তৈরি করছি যেখানে, যদি আপনি একটি ডাইস রোলে ৩ পান, আপনার প্লেয়ার নড়াচড়া করে না, বরং একটি নতুন সুন্দর টুপি পায়। যদি আপনি ৭ পান, আপনার প্লেয়ার একটি সুন্দর টুপি হারায়। অন্য সব মানের জন্য, আপনার প্লেয়ার গেম বোর্ডে সেই সংখ্যক ঘর সরে যায়। এখানে একটি match
রয়েছে যা সেই যুক্তিটি বাস্তবায়ন করে, যেখানে ডাইস রোলের ফলাফল একটি র্যান্ডম মানের পরিবর্তে হার্ডকোড করা হয়েছে, এবং অন্য সমস্ত যুক্তি বডি ছাড়া ফাংশন দ্বারা প্রতিনিধিত্ব করা হয়েছে কারণ সেগুলির বাস্তবায়ন এই উদাহরণের আওতার বাইরে:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {} }
প্রথম দুটি arm-এর জন্য, প্যাটার্নগুলি হলো লিটারেল মান 3
এবং 7
। শেষ arm-টির জন্য যা অন্য সব সম্ভাব্য মানকে কভার করে, প্যাটার্নটি হলো other
নামে একটি ভেরিয়েবল। other
arm-এর জন্য যে কোডটি চলে তা move_player
ফাংশনে ভেরিয়েবলটি পাস করে ব্যবহার করে।
এই কোডটি কম্পাইল হয়, যদিও আমরা u8
-এর সমস্ত সম্ভাব্য মান তালিকাভুক্ত করিনি, কারণ শেষ প্যাটার্নটি বিশেষভাবে তালিকাভুক্ত নয় এমন সমস্ত মানকে ম্যাচ করবে। এই ক্যাচ-অল প্যাটার্নটি match
-এর সম্পূর্ণ (exhaustive) হওয়ার প্রয়োজনীয়তা পূরণ করে। মনে রাখবেন যে আমাদের ক্যাচ-অল arm-টি শেষে রাখতে হবে কারণ প্যাটার্নগুলো ক্রমানুসারে মূল্যায়ন করা হয়। যদি আমরা ক্যাচ-অল arm-টি আগে রাখি, অন্য arm-গুলো কখনওই চলবে না, তাই Rust আমাদের সতর্ক করবে যদি আমরা একটি ক্যাচ-অল-এর পরে arm যোগ করি!
Rust-এর আরও একটি প্যাটার্ন আছে যা আমরা ব্যবহার করতে পারি যখন আমরা একটি ক্যাচ-অল চাই কিন্তু ক্যাচ-অল প্যাটার্নের মানটি ব্যবহার করতে চাই না: _
একটি বিশেষ প্যাটার্ন যা যেকোনো মানকে ম্যাচ করে এবং সেই মানের সাথে বাইন্ড হয় না। এটি Rust-কে বলে যে আমরা মানটি ব্যবহার করতে যাচ্ছি না, তাই Rust আমাদের একটি অব্যবহৃত ভেরিয়েবল সম্পর্কে সতর্ক করবে না।
আসুন গেমের নিয়ম পরিবর্তন করি: এখন, যদি আপনি ৩ বা ৭ ছাড়া অন্য কিছু রোল করেন, আপনাকে আবার রোল করতে হবে। আমাদের আর ক্যাচ-অল মানটি ব্যবহার করার প্রয়োজন নেই, তাই আমরা আমাদের কোডটি পরিবর্তন করে other
ভেরিয়েবলের পরিবর্তে _
ব্যবহার করতে পারি:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {} }
এই উদাহরণটিও সম্পূর্ণতার (exhaustiveness) প্রয়োজনীয়তা পূরণ করে কারণ আমরা শেষ arm-এ অন্য সব মানকে স্পষ্টভাবে উপেক্ষা করছি; আমরা কিছুই ভুলে যাইনি।
অবশেষে, আমরা গেমের নিয়ম আরও একবার পরিবর্তন করব যাতে আপনি যদি ৩ বা ৭ ছাড়া অন্য কিছু রোল করেন তবে আপনার টার্নে আর কিছুই হবে না। আমরা _
arm-এর সাথে যুক্ত কোড হিসাবে ইউনিট ভ্যালু (খালি টাপল টাইপ যা আমরা "The Tuple Type" বিভাগে উল্লেখ করেছি) ব্যবহার করে এটি প্রকাশ করতে পারি:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {} }
এখানে, আমরা Rust-কে স্পষ্টভাবে বলছি যে আমরা আগের arm-গুলোর কোনো প্যাটার্নের সাথে মেলে না এমন অন্য কোনো মান ব্যবহার করতে যাচ্ছি না, এবং আমরা এই ক্ষেত্রে কোনো কোড চালাতে চাই না।
প্যাটার্ন এবং ম্যাচিং সম্পর্কে আরও অনেক কিছু আছে যা আমরা Chapter 19-এ আলোচনা করব। আপাতত, আমরা if let
সিনট্যাক্সে চলে যাচ্ছি, যা এমন পরিস্থিতিতে কার্যকর হতে পারে যেখানে match
এক্সপ্রেশনটি কিছুটা শব্দবহুল হয়।