match কন্ট্রোল ফ্লো কনস্ট্রাক্ট (The match Control Flow Construct)

Rust-এ match নামক একটি অত্যন্ত শক্তিশালী কন্ট্রোল ফ্লো কনস্ট্রাক্ট রয়েছে যা আপনাকে একটি মানকে একাধিক প্যাটার্নের সাথে তুলনা করতে এবং কোন প্যাটার্নটি মেলে তার উপর ভিত্তি করে কোড এক্সিকিউট করতে দেয়। প্যাটার্নগুলো লিটারেল মান, ভেরিয়েবলের নাম, ওয়াইল্ডকার্ড এবং আরও অনেক কিছু দিয়ে তৈরি হতে পারে; চ্যাপ্টার 19-এ সমস্ত ভিন্ন ধরনের প্যাটার্ন এবং সেগুলো কী করে তা কভার করা হয়েছে। match-এর ক্ষমতা আসে প্যাটার্নগুলোর এক্সপ্রেসিভনেস (expressiveness) থেকে এবং কম্পাইলার নিশ্চিত করে যে সমস্ত সম্ভাব্য ক্ষেত্র হ্যান্ডেল করা হয়েছে।

একটি match এক্সপ্রেশনকে একটি কয়েন-সর্টিং মেশিনের মতো ভাবতে পারেন: কয়েনগুলো বিভিন্ন আকারের গর্তযুক্ত একটি ট্র্যাকের নিচে গড়িয়ে পড়ে এবং প্রতিটি কয়েন প্রথম যে গর্তটিতে ফিট করে সেটিতে পড়ে যায়। একইভাবে, মানগুলো match-এর প্রতিটি প্যাটার্নের মধ্য দিয়ে যায় এবং প্রথম প্যাটার্নে যে মানটি "ফিট" করে, সেটি এক্সিকিউশনের সময় ব্যবহার করার জন্য সংশ্লিষ্ট কোড ব্লকে চলে যায়।

কয়েনের কথা যখন উঠলই, তখন আসুন match ব্যবহার করে সেগুলোকে একটি উদাহরণ হিসাবে ব্যবহার করি! আমরা এমন একটি ফাংশন লিখতে পারি যা একটি অজানা US কয়েন নেয় এবং গণনা মেশিনের মতোই নির্ধারণ করে যে এটি কোন কয়েন এবং সেন্টে এর মান রিটার্ন করে, যেমনটি Listing 6-3-তে দেখানো হয়েছে।

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-এর সাথে, কন্ডিশনটি একটি বুলিয়ান মান মূল্যায়ন করতে হবে, কিন্তু এখানে এটি যেকোনো টাইপের হতে পারে। এই উদাহরণে coin-এর টাইপ হল Coin এনাম যা আমরা প্রথম লাইনে সংজ্ঞায়িত করেছি।

এরপর রয়েছে match আর্মগুলো। একটি আর্মের দুটি অংশ রয়েছে: একটি প্যাটার্ন এবং কিছু কোড। এখানে প্রথম আর্মটিতে একটি প্যাটার্ন রয়েছে যা হল Coin::Penny মান এবং তারপর => অপারেটর যা প্যাটার্ন এবং চালানোর জন্য কোডকে আলাদা করে। এই ক্ষেত্রে কোডটি হল শুধুমাত্র 1 মান। প্রতিটি আর্মকে একটি কমা দিয়ে পরেরটি থেকে আলাদা করা হয়।

যখন match এক্সপ্রেশনটি এক্সিকিউট হয়, তখন এটি ফলাফলের মানটিকে প্রতিটি আর্মের প্যাটার্নের সাথে তুলনা করে, ক্রমানুসারে। যদি একটি প্যাটার্ন মানের সাথে মেলে, তাহলে সেই প্যাটার্নের সাথে সম্পর্কিত কোডটি এক্সিকিউট করা হয়। যদি সেই প্যাটার্নটি মানের সাথে না মেলে, তাহলে এক্সিকিউশন পরবর্তী আর্মে চলতে থাকে, অনেকটা কয়েন-সর্টিং মেশিনের মতো। আমাদের যতগুলো প্রয়োজন ততগুলো আর্ম থাকতে পারে: Listing 6-3-তে, আমাদের match-এর চারটি আর্ম রয়েছে।

প্রতিটি আর্মের সাথে সম্পর্কিত কোড হল একটি এক্সপ্রেশন এবং ম্যাচিং আর্মের এক্সপ্রেশনের ফলাফলের মান হল সেই মান যা সম্পূর্ণ match এক্সপ্রেশনের জন্য রিটার্ন করা হয়।

যদি ম্যাচ আর্মের কোড ছোট হয় তবে আমরা সাধারণত কার্লি ব্র্যাকেট ব্যবহার করি না, যেমনটি Listing 6-3-তে রয়েছে যেখানে প্রতিটি আর্ম শুধুমাত্র একটি মান রিটার্ন করে। আপনি যদি একটি ম্যাচ আর্মে একাধিক লাইনের কোড চালাতে চান, তাহলে আপনাকে অবশ্যই কার্লি ব্র্যাকেট ব্যবহার করতে হবে এবং আর্মের পরে কমাটি তখন ঐচ্ছিক। উদাহরণস্বরূপ, নিম্নলিখিত কোডটি প্রতিবার 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() {}

মানগুলোর সাথে বাইন্ড করা প্যাটার্ন (Patterns That Bind to Values)

ম্যাচ আর্মগুলোর আরেকটি দরকারী ফিচার হল যে তারা প্যাটার্নের সাথে মেলে এমন মানগুলোর অংশের সাথে বাইন্ড করতে পারে। এইভাবে আমরা এনাম ভেরিয়েন্টগুলো থেকে মান বের করতে পারি।

উদাহরণ হিসাবে, আসুন আমাদের এনাম ভেরিয়েন্টগুলোর মধ্যে একটি পরিবর্তন করি যাতে এর ভিতরে ডেটা থাকে। ১৯৯৯ থেকে ২০০৮ সাল পর্যন্ত, মার্কিন যুক্তরাষ্ট্র প্রতিটি ৫০টি রাজ্যের জন্য একদিকে ভিন্ন ডিজাইন সহ কোয়ার্টার তৈরি করেছিল। অন্য কোনো কয়েনের স্টেট ডিজাইন ছিল না, তাই শুধুমাত্র কোয়ার্টারেই এই অতিরিক্ত মান রয়েছে। আমরা আমাদের enum-এ এই তথ্য যোগ করতে পারি Quarter ভেরিয়েন্টটিকে পরিবর্তন করে এর ভিতরে একটি UsState মান সংরক্ষণ করতে, যা আমরা Listing 6-4-এ করেছি।

#[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() {}

আসুন কল্পনা করি যে একজন বন্ধু সমস্ত ৫০টি রাজ্যের কোয়ার্টার সংগ্রহ করার চেষ্টা করছেন। আমরা যখন আমাদের খুচরো পয়সাগুলো কয়েনের ধরন অনুসারে বাছাই করি, তখন আমরা প্রতিটি কোয়ার্টারের সাথে সম্পর্কিত রাজ্যের নামও বলব যাতে যদি এটি এমন কোনো রাজ্য হয় যা আমাদের বন্ধুর নেই, তাহলে তারা সেটি তাদের সংগ্রহে যোগ করতে পারে।

এই কোডের ম্যাচ এক্সপ্রেশনে, আমরা Coin::Quarter ভেরিয়েন্টের মানগুলোর সাথে মেলে এমন প্যাটার্নে state নামক একটি ভেরিয়েবল যোগ করি। যখন একটি Coin::Quarter মেলে, তখন state ভেরিয়েবলটি সেই কোয়ার্টারের রাজ্যের মানের সাথে বাইন্ড হবে। তারপর আমরা সেই আর্মের কোডে state ব্যবহার করতে পারি, এইভাবে:

#[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)। যখন আমরা সেই মানটিকে প্রতিটি ম্যাচ আর্মের সাথে তুলনা করি, তখন সেগুলোর কোনোটিই মেলে না যতক্ষণ না আমরা Coin::Quarter(state)-এ পৌঁছাই। সেই সময়ে, state-এর জন্য বাইন্ডিং হবে UsState::Alaska মান। তারপর আমরা println! এক্সপ্রেশনে সেই বাইন্ডিংটি ব্যবহার করতে পারি, এইভাবে Quarter-এর জন্য Coin এনাম ভেরিয়েন্ট থেকে অভ্যন্তরীণ রাজ্যের মান পেতে পারি।

Option<T>-এর সাথে ম্যাচিং (Matching with Option<T>)

আগের বিভাগে, আমরা Option<T> ব্যবহার করার সময় Some কেস থেকে অভ্যন্তরীণ T মানটি পেতে চেয়েছিলাম; আমরা Coin এনামের সাথে যেভাবে করেছি, সেভাবে match ব্যবহার করে Option<T> হ্যান্ডেল করতে পারি! কয়েনগুলোর তুলনা করার পরিবর্তে, আমরা Option<T>-এর ভেরিয়েন্টগুলোর তুলনা করব, কিন্তু match এক্সপ্রেশন যেভাবে কাজ করে তা একই থাকে।

ধরুন আমরা এমন একটি ফাংশন লিখতে চাই যা একটি Option<i32> নেয় এবং যদি ভিতরে একটি মান থাকে তবে সেই মানের সাথে 1 যোগ করে। যদি ভিতরে কোনো মান না থাকে, তাহলে ফাংশনটির None মান রিটার্ন করা উচিত এবং কোনো অপারেশন করার চেষ্টা করা উচিত নয়।

match-এর কারণে এই ফাংশনটি লেখা খুব সহজ এবং এটি Listing 6-5-এর মতো দেখতে হবে।

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)। তারপর আমরা এটিকে প্রতিটি ম্যাচ আর্মের সাথে তুলনা করি:

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 প্যাটার্নের সাথে মেলে না, তাই আমরা পরবর্তী আর্মে চালিয়ে যাই:

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 মান নেয়। তারপর ম্যাচ আর্মের কোডটি এক্সিকিউট করা হয়, তাই আমরা i-এর মানের সাথে 1 যোগ করি এবং আমাদের মোট 6 সহ একটি নতুন Some মান তৈরি করি।

এবার Listing 6-5-এ plus_one-এর দ্বিতীয় কলটি বিবেচনা করি, যেখানে x হল 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);
}

এটি মিলে যায়! যোগ করার মতো কোনো মান নেই, তাই প্রোগ্রামটি বন্ধ হয়ে যায় এবং =>-এর ডানদিকের None মানটি রিটার্ন করে। যেহেতু প্রথম আর্মটি মিলে গেছে, তাই অন্য কোনো আর্ম তুলনা করা হয় না।

অনেক পরিস্থিতিতে match এবং এনামগুলোকে একত্রিত করা দরকারী। আপনি Rust কোডে এই প্যাটার্নটি অনেক দেখতে পাবেন: একটি এনামের বিপরীতে match, ভিতরের ডেটার সাথে একটি ভেরিয়েবল বাইন্ড করা এবং তারপর এর উপর ভিত্তি করে কোড এক্সিকিউট করা। এটি প্রথমে একটু জটিল, কিন্তু একবার আপনি এতে অভ্যস্ত হয়ে গেলে, আপনি চাইবেন যে এটি সমস্ত ভাষায় থাকুক। এটি ধারাবাহিকভাবে ব্যবহারকারীর প্রিয়।

ম্যাচগুলো এক্সহস্টিভ (Matches Are Exhaustive)

match-এর আরেকটি দিক রয়েছে যা আমাদের আলোচনা করতে হবে: আর্মগুলোর প্যাটার্নগুলোকে অবশ্যই সমস্ত সম্ভাবনা কভার করতে হবে। আমাদের 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 কীভাবে ধরতে হয় তা জানে। আমরা যদি এই কোডটি কম্পাইল করার চেষ্টা করি, তাহলে আমরা এই এররটি পাব:

$ 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
   --> file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/option.rs:572:1
    |
572 | pub enum Option<T> {
    | ^^^^^^^^^^^^^^^^^^
...
576 |     None,
    |     ---- 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 কেসটি স্পষ্টভাবে হ্যান্ডেল করতে ভোলা থেকে বিরত রাখে, তখন এটি আমাদের এমন একটি মান আছে বলে ধরে নেওয়া থেকে রক্ষা করে যখন আমাদের কাছে নাল থাকতে পারে, এইভাবে পূর্বে আলোচিত বিলিয়ন-ডলারের ভুলটিকে অসম্ভব করে তোলে।

ক্যাচ-অল প্যাটার্ন এবং _ প্লেসহোল্ডার (Catch-All Patterns and the _ Placeholder)

এনামগুলো ব্যবহার করে, আমরা কয়েকটি নির্দিষ্ট মানের জন্য বিশেষ অ্যাকশন নিতে পারি, কিন্তু অন্য সমস্ত মানের জন্য একটি ডিফল্ট অ্যাকশন নিতে পারি। কল্পনা করুন যে আমরা একটি গেম ইমপ্লিমেন্ট করছি যেখানে, আপনি যদি ডাইস রোলে 3 পান, তাহলে আপনার প্লেয়ার সরবে না, কিন্তু পরিবর্তে একটি নতুন অভিনব টুপি পায়। আপনি যদি 7 রোল করেন, তাহলে আপনার প্লেয়ার একটি অভিনব টুপি হারায়। অন্য সমস্ত মানের জন্য, আপনার প্লেয়ার গেম বোর্ডে সেই সংখ্যক স্পেস সরবে। এখানে একটি 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) {}
}

প্রথম দুটি আর্মের জন্য, প্যাটার্নগুলো হল আক্ষরিক মান 3 এবং 7। শেষ আর্মের জন্য যা অন্য সমস্ত সম্ভাব্য মান কভার করে, প্যাটার্নটি হল other নামক ভেরিয়েবল। other আর্মের জন্য যে কোডটি চলে সেটি move_player ফাংশনে ভেরিয়েবলটি পাস করে সেটি ব্যবহার করে।

এই কোডটি কম্পাইল হয়, যদিও আমরা u8 যে সমস্ত সম্ভাব্য মান নিতে পারে তা তালিকাভুক্ত করিনি, কারণ শেষ প্যাটার্নটি বিশেষভাবে তালিকাভুক্ত নয় এমন সমস্ত মানের সাথে মিলবে। এই ক্যাচ-অল প্যাটার্নটি match অবশ্যই এক্সহস্টিভ হতে হবে সেই প্রয়োজনীয়তা পূরণ করে। মনে রাখবেন যে আমাদের ক্যাচ-অল আর্মটিকে শেষে রাখতে হবে কারণ প্যাটার্নগুলো ক্রমানুসারে মূল্যায়ন করা হয়। আমরা যদি ক্যাচ-অল আর্মটিকে আগে রাখি, তাহলে অন্য আর্মগুলো কখনই চলবে না, তাই আমরা যদি একটি ক্যাচ-অলের পরে আর্ম যোগ করি তবে Rust আমাদের সতর্ক করবে!

Rust-এ একটি প্যাটার্নও রয়েছে যা আমরা ব্যবহার করতে পারি যখন আমরা একটি ক্যাচ-অল চাই কিন্তু ক্যাচ-অল প্যাটার্নের মানটি ব্যবহার করতে চাই না: _ হল একটি বিশেষ প্যাটার্ন যা যেকোনো মানের সাথে মেলে এবং সেই মানের সাথে বাইন্ড করে না। এটি Rust-কে বলে যে আমরা মানটি ব্যবহার করতে যাচ্ছি না, তাই Rust আমাদের একটি অব্যবহৃত ভেরিয়েবল সম্পর্কে সতর্ক করবে না।

আসুন গেমের নিয়ম পরিবর্তন করি: এখন, আপনি যদি 3 বা 7 ছাড়া অন্য কিছু রোল করেন, তাহলে আপনাকে অবশ্যই আবার রোল করতে হবে। আমাদের আর ক্যাচ-অল মানটি ব্যবহার করার প্রয়োজন নেই, তাই আমরা আমাদের কোড পরিবর্তন করে 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() {}
}

এই উদাহরণটিও এক্সহস্টিভনেসের প্রয়োজনীয়তা পূরণ করে কারণ আমরা স্পষ্টভাবে শেষ আর্মে অন্য সমস্ত মান উপেক্ষা করছি; আমরা কোনো কিছু ভুলে যাইনি।

অবশেষে, আমরা গেমের নিয়মগুলো আরও একবার পরিবর্তন করব যাতে আপনি যদি 3 বা 7 ছাড়া অন্য কিছু রোল করেন তাহলে আপনার টার্নে আর কিছুই ঘটবে না। আমরা _ আর্মের সাথে থাকা কোড হিসাবে ইউনিট মান (খালি টাপল টাইপ যা আমরা “টাপল টাইপ” বিভাগে উল্লেখ করেছি) ব্যবহার করে তা প্রকাশ করতে পারি:

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-কে স্পষ্টভাবে বলছি যে আমরা অন্য কোনো মান ব্যবহার করতে যাচ্ছি না যা আগের কোনো আর্মে একটি প্যাটার্নের সাথে মেলে না এবং আমরা এই ক্ষেত্রে কোনো কোড চালাতে চাই না।

প্যাটার্ন এবং ম্যাচিং সম্পর্কে আরও অনেক কিছু রয়েছে যা আমরা চ্যাপ্টার 19-এ কভার করব। আপাতত, আমরা if let সিনট্যাক্সে চলে যাচ্ছি, যা এমন পরিস্থিতিতে দরকারী হতে পারে যেখানে match এক্সপ্রেশনটি একটু শব্দবহুল।