if let এবং let else দিয়ে সংক্ষিপ্ত কন্ট্রোল ফ্লো (Concise Control Flow with if let and let else)

if let সিনট্যাক্স আপনাকে if এবং let-কে একত্রিত করে কম শব্দ ব্যবহার করে এমন মানগুলো হ্যান্ডেল করতে দেয়, যেগুলো একটি প্যাটার্নের সাথে মেলে এবং বাকিগুলো উপেক্ষা করে। Listing 6-6-এর প্রোগ্রামটি বিবেচনা করুন, যেটি config_max ভেরিয়েবলের একটি Option<u8> মানের উপর ম্যাচ করে, কিন্তু শুধুমাত্র তখনই কোড এক্সিকিউট করতে চায় যদি মানটি Some ভেরিয়েন্ট হয়।

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}

যদি মানটি Some হয়, তাহলে আমরা প্যাটার্নের max ভেরিয়েবলের সাথে মান বাইন্ড করে Some ভেরিয়েন্টের মানটি প্রিন্ট করি। আমরা None মান নিয়ে কিছু করতে চাই না। match এক্সপ্রেশনটিকে সন্তুষ্ট করার জন্য, শুধুমাত্র একটি ভেরিয়েন্ট প্রক্রিয়া করার পরে আমাদের _ => () যোগ করতে হবে, যা যোগ করার জন্য বিরক্তিকর বয়লারপ্লেট কোড।

পরিবর্তে, আমরা if let ব্যবহার করে এটিকে আরও সংক্ষিপ্তভাবে লিখতে পারি। নিম্নলিখিত কোডটি Listing 6-6-এর match-এর মতোই আচরণ করে:

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

if let সিনট্যাক্স একটি প্যাটার্ন এবং একটি এক্সপ্রেশন নেয়, যা একটি সমান চিহ্ন দ্বারা পৃথক করা হয়। এটি match-এর মতোই কাজ করে, যেখানে এক্সপ্রেশনটি match-কে দেওয়া হয় এবং প্যাটার্নটি হল এর প্রথম আর্ম। এই ক্ষেত্রে, প্যাটার্নটি হল Some(max), এবং max Some-এর ভিতরের মানের সাথে বাইন্ড করে। তারপর আমরা if let ব্লকের বডিতে max ব্যবহার করতে পারি, একইভাবে আমরা সংশ্লিষ্ট match আর্মে max ব্যবহার করেছি। if let ব্লকের কোড শুধুমাত্র তখনই চলে যদি মানটি প্যাটার্নের সাথে মেলে।

if let ব্যবহার করার অর্থ হল কম টাইপিং, কম ইন্ডেন্টেশন এবং কম বয়লারপ্লেট কোড। তবে, আপনি match যে এক্সহস্টিভ চেকিং (exhaustive checking) প্রয়োগ করে সেটি হারাবেন। match এবং if let-এর মধ্যে বেছে নেওয়া আপনার নির্দিষ্ট পরিস্থিতিতে আপনি কী করছেন এবং এক্সহস্টিভ চেকিং হারানো সংক্ষিপ্ততা অর্জনের জন্য উপযুক্ত কিনা তার উপর নির্ভর করে।

অন্য কথায়, আপনি if let-কে একটি match-এর সিনট্যাক্স সুগার হিসাবে ভাবতে পারেন যা মানটি একটি প্যাটার্নের সাথে মিললে কোড চালায় এবং তারপর অন্য সমস্ত মান উপেক্ষা করে।

আমরা if let-এর সাথে একটি else অন্তর্ভুক্ত করতে পারি। else-এর সাথে থাকা কোডের ব্লকটি match এক্সপ্রেশনের _ কেসের সাথে থাকা কোড ব্লকের মতোই, যেটি if let এবং else-এর সমতুল্য। Listing 6-4-এর Coin এনামের সংজ্ঞাটি স্মরণ করুন, যেখানে Quarter ভেরিয়েন্টটিতে একটি UsState মানও ছিল। আমরা যদি কোয়ার্টারগুলোর রাজ্যের ঘোষণা করার পাশাপাশি সমস্ত নন-কোয়ার্টার কয়েন গণনা করতে চাই, তাহলে আমরা এটি একটি match এক্সপ্রেশন দিয়ে করতে পারি, এইভাবে:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

অথবা আমরা একটি if let এবং else এক্সপ্রেশন ব্যবহার করতে পারি, এইভাবে:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

"হ্যাপি পাথ"-এ থাকা let else দিয়ে ("Staying on the “happy path” with let else)

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

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

তারপর আমরা কয়েনের টাইপের উপর ম্যাচ করার জন্য if let ব্যবহার করতে পারি, Listing 6-7-এর মতো, কন্ডিশনের বডিতে একটি state ভেরিয়েবল প্রবর্তন করতে পারি।

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

এতে কাজটি সম্পন্ন হয়, কিন্তু এটি কাজটিকে if let স্টেটমেন্টের বডিতে ঠেলে দিয়েছে এবং যদি কাজটি আরও জটিল হয়, তাহলে টপ-লেভেল ব্রাঞ্চগুলো কীভাবে সম্পর্কিত তা অনুসরণ করা কঠিন হতে পারে। আমরা এই বিষয়টিও বিবেচনায় নিতে পারি যে এক্সপ্রেশনগুলো একটি মান তৈরি করে, if let থেকে state তৈরি করতে বা তাড়াতাড়ি রিটার্ন করতে, যেমনটি Listing 6-8-এ রয়েছে। (আপনি একটি match দিয়েও একই কাজ করতে পারেন, অবশ্যই!)

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

এটি তার নিজস্ব উপায়ে অনুসরণ করা কিছুটা বিরক্তিকর! if let-এর একটি শাখা একটি মান তৈরি করে এবং অন্যটি সম্পূর্ণরূপে ফাংশন থেকে রিটার্ন করে।

এই সাধারণ প্যাটার্নটিকে আরও সুন্দরভাবে প্রকাশ করার জন্য, Rust-এর let-else রয়েছে। let-else সিনট্যাক্স বাম দিকে একটি প্যাটার্ন এবং ডানদিকে একটি এক্সপ্রেশন নেয়, if let-এর মতোই, কিন্তু এটির কোনো if শাখা নেই, শুধুমাত্র একটি else শাখা রয়েছে। যদি প্যাটার্নটি মেলে, তাহলে এটি বাইরের স্কোপে প্যাটার্ন থেকে মানটিকে বাইন্ড করবে। যদি প্যাটার্নটি মেলে না, তাহলে প্রোগ্রামটি else আর্মের মধ্যে চলে যাবে, যেটিকে অবশ্যই ফাংশন থেকে রিটার্ন করতে হবে।

Listing 6-9-এ, আপনি দেখতে পারেন কিভাবে Listing 6-8 if let-এর পরিবর্তে let-else ব্যবহার করলে কেমন দেখায়। লক্ষ্য করুন যে এটি ফাংশনের মূল বডিতে "হ্যাপি পাথে" থাকে, দুটি শাখার জন্য উল্লেখযোগ্যভাবে ভিন্ন কন্ট্রোল ফ্লো না রেখে যেভাবে if let করেছিল।

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

যদি আপনার এমন পরিস্থিতি থাকে যেখানে আপনার প্রোগ্রামের লজিক একটি match ব্যবহার করে প্রকাশ করা খুব শব্দবহুল হয়, তাহলে মনে রাখবেন যে if let এবং let else আপনার Rust টুলবক্সে রয়েছে।

সারসংক্ষেপ (Summary)

আমরা এখন এনামগুলো ব্যবহার করে কীভাবে কাস্টম টাইপ তৈরি করতে হয় তা কভার করেছি, যেগুলো গণনাকৃত মানগুলোর একটি সেটের মধ্যে একটি হতে পারে। আমরা দেখেছি কিভাবে স্ট্যান্ডার্ড লাইব্রেরির Option<T> টাইপ আপনাকে এরর প্রতিরোধ করতে টাইপ সিস্টেম ব্যবহার করতে সাহায্য করে। যখন এনাম মানগুলোর ভিতরে ডেটা থাকে, তখন আপনি কতগুলো ক্ষেত্র হ্যান্ডেল করতে হবে তার উপর নির্ভর করে সেই মানগুলো বের করতে এবং ব্যবহার করতে match বা if let ব্যবহার করতে পারেন।

আপনার Rust প্রোগ্রামগুলো এখন স্ট্রাকট এবং এনাম ব্যবহার করে আপনার ডোমেনে ধারণাগুলো প্রকাশ করতে পারে। আপনার API-তে ব্যবহার করার জন্য কাস্টম টাইপ তৈরি করা টাইপ নিরাপত্তা নিশ্চিত করে: কম্পাইলার নিশ্চিত করবে যে আপনার ফাংশনগুলো শুধুমাত্র সেই টাইপের মানগুলো পাবে যা প্রতিটি ফাংশন আশা করে।

আপনার ব্যবহারকারীদের কাছে একটি সুসংগঠিত API সরবরাহ করার জন্য যা ব্যবহার করা সহজ এবং শুধুমাত্র আপনার ব্যবহারকারীদের যা প্রয়োজন সেটাই প্রকাশ করে, আসুন এবার Rust-এর মডিউলগুলোর দিকে যাই।