Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Result দিয়ে পুনরুদ্ধারযোগ্য এরর (Recoverable Errors)

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

অধ্যায় ২-এর “Handling Potential Failure with Result থেকে মনে করুন যে Result enum-কে দুটি ভ্যারিয়েন্ট Ok এবং Err সহ সংজ্ঞায়িত করা হয়েছে, যা নিম্নরূপ:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

T এবং E হলো জেনেরিক টাইপ প্যারামিটার (generic type parameters): আমরা অধ্যায় ১০-এ জেনেরিক সম্পর্কে আরও বিস্তারিত আলোচনা করব। এখন আপনার যা জানা দরকার তা হলো, T সফল ক্ষেত্রে Ok ভ্যারিয়েন্টের মধ্যে ফেরত আসা ভ্যালুর টাইপকে প্রতিনিধিত্ব করে, এবং E ব্যর্থতার ক্ষেত্রে Err ভ্যারিয়েন্টের মধ্যে ফেরত আসা এররের টাইপকে প্রতিনিধিত্ব করে। যেহেতু Result-এর এই জেনেরিক টাইপ প্যারামিটারগুলো রয়েছে, তাই আমরা Result টাইপ এবং এর উপর সংজ্ঞায়িত ফাংশনগুলো বিভিন্ন পরিস্থিতিতে ব্যবহার করতে পারি যেখানে আমরা যে সফল ভ্যালু এবং এরর ভ্যালু ফেরত দিতে চাই তা ভিন্ন হতে পারে।

আসুন এমন একটি ফাংশন কল করি যা একটি Result ভ্যালু রিটার্ন করে কারণ ফাংশনটি ব্যর্থ হতে পারে। লিস্টিং ৯-৩-এ আমরা একটি ফাইল খোলার চেষ্টা করছি।

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

File::open-এর রিটার্ন টাইপ হলো একটি Result<T, E>। জেনেরিক প্যারামিটার T File::open-এর ইমপ্লিমেন্টেশনে সফল ভ্যালুর টাইপ std::fs::File দ্বারা পূর্ণ হয়েছে, যা একটি ফাইল হ্যান্ডেল (file handle)। এরর ভ্যালুতে ব্যবহৃত E-এর টাইপ হলো std::io::Error। এই রিটার্ন টাইপের মানে হলো File::open কলটি সফল হতে পারে এবং একটি ফাইল হ্যান্ডেল রিটার্ন করতে পারে যা থেকে আমরা পড়তে বা লিখতে পারি। ফাংশন কলটি ব্যর্থও হতে পারে: উদাহরণস্বরূপ, ফাইলটি নাও থাকতে পারে, অথবা আমাদের ফাইল অ্যাক্সেস করার অনুমতি নাও থাকতে পারে। File::open ফাংশনটির আমাদের জানানোর একটি উপায় থাকা দরকার যে এটি সফল হয়েছে নাকি ব্যর্থ হয়েছে এবং একই সাথে আমাদের ফাইল হ্যান্ডেল বা এররের তথ্য দেওয়া দরকার। Result enum ঠিক এই তথ্যই বহন করে।

যে ক্ষেত্রে File::open সফল হয়, greeting_file_result ভ্যারিয়েবলের ভ্যালুটি হবে Ok-এর একটি ইনস্ট্যান্স যা একটি ফাইল হ্যান্ডেল ধারণ করে। যে ক্ষেত্রে এটি ব্যর্থ হয়, greeting_file_result-এর ভ্যালুটি হবে Err-এর একটি ইনস্ট্যান্স যা কী ধরনের এরর ঘটেছে সে সম্পর্কে আরও তথ্য ধারণ করে।

File::open যে ভ্যালু রিটার্ন করে তার উপর নির্ভর করে বিভিন্ন পদক্ষেপ নেওয়ার জন্য আমাদের লিস্টিং ৯-৩-এর কোডে আরও কিছু যোগ করতে হবে। লিস্টিং ৯-৪ Result হ্যান্ডেল করার একটি উপায় দেখায়, যেখানে একটি বেসিক টুল, match এক্সপ্রেশন ব্যবহার করা হয়েছে যা আমরা অধ্যায় ৬-এ আলোচনা করেছি।

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {error:?}"),
    };
}

লক্ষ্য করুন যে, Option enum-এর মতো, Result enum এবং এর ভ্যারিয়েন্টগুলো prelude দ্বারা স্কোপে আনা হয়েছে, তাই আমাদের match arm-গুলোতে Ok এবং Err ভ্যারিয়েন্টের আগে Result:: নির্দিষ্ট করার প্রয়োজন নেই।

যখন ফলাফল Ok হয়, এই কোডটি Ok ভ্যারিয়েন্ট থেকে ভেতরের file ভ্যালুটি রিটার্ন করবে, এবং আমরা তারপর সেই ফাইল হ্যান্ডেল ভ্যালুটি greeting_file ভ্যারিয়েবলে অ্যাসাইন করি। match-এর পরে, আমরা ফাইল হ্যান্ডেলটি পড়া বা লেখার জন্য ব্যবহার করতে পারি।

match-এর অন্য arm-টি সেই কেসটি হ্যান্ডেল করে যেখানে আমরা File::open থেকে একটি Err ভ্যালু পাই। এই উদাহরণে, আমরা panic! ম্যাক্রো কল করতে বেছে নিয়েছি। যদি আমাদের বর্তমান ডিরেক্টরিতে hello.txt নামে কোনো ফাইল না থাকে এবং আমরা এই কোডটি চালাই, আমরা panic! ম্যাক্রো থেকে নিম্নলিখিত আউটপুট দেখতে পাব:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`

thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

বরাবরের মতো, এই আউটপুটটি আমাদের ঠিক কী ভুল হয়েছে তা বলে দেয়।

বিভিন্ন এররের উপর ম্যাচিং (Matching on Different Errors)

লিস্টিং ৯-৪-এর কোডটি File::open কেন ব্যর্থ হয়েছে তা নির্বিশেষে panic! করবে। তবে, আমরা বিভিন্ন ব্যর্থতার কারণের জন্য বিভিন্ন পদক্ষেপ নিতে চাই। যদি ফাইলটি না থাকার কারণে File::open ব্যর্থ হয়, আমরা ফাইলটি তৈরি করতে এবং নতুন ফাইলের হ্যান্ডেল রিটার্ন করতে চাই। যদি File::open অন্য কোনো কারণে ব্যর্থ হয়—উদাহরণস্বরূপ, কারণ আমাদের ফাইল খোলার অনুমতি ছিল না—আমরা এখনও চাই কোডটি লিস্টিং ৯-৪-এর মতোই panic! করুক। এর জন্য, আমরা একটি অভ্যন্তরীণ match এক্সপ্রেশন যোগ করি, যা লিস্টিং ৯-৫-এ দেখানো হয়েছে।

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {e:?}"),
            },
            _ => {
                panic!("Problem opening the file: {error:?}");
            }
        },
    };
}

File::open Err ভ্যারিয়েন্টের ভিতরে যে ভ্যালুটি রিটার্ন করে তার টাইপ হলো io::Error, যা standard library দ্বারা সরবরাহ করা একটি struct। এই struct-টির একটি মেথড kind আছে যা আমরা একটি io::ErrorKind ভ্যালু পেতে কল করতে পারি। io::ErrorKind enum-টি standard library দ্বারা সরবরাহ করা হয় এবং এতে এমন ভ্যারিয়েন্ট রয়েছে যা একটি io অপারেশনের ফলে হতে পারে এমন বিভিন্ন ধরনের এররকে প্রতিনিধিত্ব করে। আমরা যে ভ্যারিয়েন্টটি ব্যবহার করতে চাই তা হলো ErrorKind::NotFound, যা নির্দেশ করে যে আমরা যে ফাইলটি খোলার চেষ্টা করছি তা এখনও বিদ্যমান নেই। তাই আমরা greeting_file_result-এর উপর ম্যাচ করি, কিন্তু আমাদের error.kind()-এর উপর একটি অভ্যন্তরীণ ম্যাচও রয়েছে।

অভ্যন্তরীণ ম্যাচে আমরা যে শর্তটি পরীক্ষা করতে চাই তা হলো error.kind() দ্বারা রিটার্ন করা ভ্যালুটি ErrorKind enum-এর NotFound ভ্যারিয়েন্ট কিনা। যদি তাই হয়, আমরা File::create দিয়ে ফাইলটি তৈরি করার চেষ্টা করি। তবে, যেহেতু File::create-ও ব্যর্থ হতে পারে, তাই আমাদের অভ্যন্তরীণ match এক্সপ্রেশনে একটি দ্বিতীয় arm দরকার। যখন ফাইলটি তৈরি করা যায় না, তখন একটি ভিন্ন এরর বার্তা প্রিন্ট করা হয়। বাইরের match-এর দ্বিতীয় arm-টি একই থাকে, তাই প্রোগ্রামটি ফাইল না পাওয়ার এরর ছাড়া অন্য যেকোনো এররের জন্য প্যানিক করে।

Result<T, E>-এর সাথে match ব্যবহারের বিকল্প

এখানে অনেক match ব্যবহার হয়েছে! match এক্সপ্রেশনটি খুব দরকারী কিন্তু এটি একটি বেশ আদিম (primitive) টুল। অধ্যায় ১৩-তে, আপনি closures সম্পর্কে শিখবেন, যা Result<T, E>-তে সংজ্ঞায়িত অনেক মেথডের সাথে ব্যবহৃত হয়। আপনার কোডে Result<T, E> ভ্যালু হ্যান্ডেল করার সময় এই মেথডগুলো match ব্যবহারের চেয়ে বেশি সংক্ষিপ্ত হতে পারে।

উদাহরণস্বরূপ, লিস্টিং ৯-৫-এর মতো একই লজিক লেখার আরেকটি উপায় এখানে দেওয়া হলো, এবার closures এবং unwrap_or_else মেথড ব্যবহার করে:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {error:?}");
            })
        } else {
            panic!("Problem opening the file: {error:?}");
        }
    });
}

যদিও এই কোডটির আচরণ লিস্টিং ৯-৫-এর মতোই, এতে কোনো match এক্সপ্রেশন নেই এবং এটি পড়তে আরও পরিষ্কার। অধ্যায় ১৩ পড়ার পরে এই উদাহরণে ফিরে আসুন, এবং standard library ডকুমেন্টেশনে unwrap_or_else মেথডটি দেখুন। এরর নিয়ে কাজ করার সময় এরকম আরও অনেক মেথড আছে যা বিশাল নেস্টেড match এক্সপ্রেশনকে পরিষ্কার করতে পারে।

এররের উপর প্যানিকের জন্য শর্টকাট: unwrap এবং expect

match ব্যবহার করা যথেষ্ট ভালো কাজ করে, তবে এটি কিছুটা দীর্ঘ হতে পারে এবং সবসময় উদ্দেশ্য ভালোভাবে বোঝাতে পারে না। Result<T, E> টাইপের উপর বিভিন্ন, আরও নির্দিষ্ট কাজ করার জন্য অনেক হেল্পার মেথড সংজ্ঞায়িত করা আছে। unwrap মেথডটি একটি শর্টকাট মেথড যা আমরা লিস্টিং ৯-৪-এ লেখা match এক্সপ্রেশনের মতোই প্রয়োগ করা হয়েছে। যদি Result ভ্যালুটি Ok ভ্যারিয়েন্ট হয়, unwrap Ok-এর ভিতরের ভ্যালুটি রিটার্ন করবে। যদি Result Err ভ্যারিয়েন্ট হয়, unwrap আমাদের জন্য panic! ম্যাক্রো কল করবে। এখানে unwrap-এর একটি উদাহরণ দেওয়া হলো:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

যদি আমরা hello.txt ফাইল ছাড়া এই কোডটি চালাই, আমরা unwrap মেথডের করা panic! কল থেকে একটি এরর বার্তা দেখতে পাব:

thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }

একইভাবে, expect মেথডটি আমাদের panic! এরর বার্তাও বেছে নিতে দেয়। unwrap-এর পরিবর্তে expect ব্যবহার করা এবং ভালো এরর বার্তা সরবরাহ করা আপনার উদ্দেশ্য বোঝাতে পারে এবং প্যানিকের উৎস খুঁজে বের করা সহজ করে তুলতে পারে। expect-এর সিনট্যাক্সটি এইরকম:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

আমরা expect unwrap-এর মতোই ব্যবহার করি: ফাইল হ্যান্ডেল রিটার্ন করতে বা panic! ম্যাক্রো কল করতে। expect-এর panic! কলে ব্যবহৃত এরর বার্তাটি হবে expect-এ পাস করা প্যারামিটার, unwrap-এর ব্যবহৃত ডিফল্ট panic! বার্তার পরিবর্তে। এটি দেখতে এইরকম:

thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }

প্রোডাকশন-মানের কোডে, বেশিরভাগ রাস্টেসিয়ান (Rustaceans) unwrap-এর পরিবর্তে expect বেছে নেয় এবং অপারেশনটি কেন সবসময় সফল হবে বলে আশা করা হচ্ছে সে সম্পর্কে আরও প্রসঙ্গ দেয়। এভাবে, যদি আপনার অনুমান কখনও ভুল প্রমাণিত হয়, আপনার ডিবাগিংয়ে ব্যবহার করার জন্য আরও তথ্য থাকবে।

এরর প্রচার করা (Propagating Errors)

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

উদাহরণস্বরূপ, লিস্টিং ৯-৬ একটি ফাংশন দেখায় যা একটি ফাইল থেকে একটি ব্যবহারকারীর নাম পড়ে। যদি ফাইলটি বিদ্যমান না থাকে বা পড়া না যায়, এই ফাংশনটি সেই এররগুলো ফাংশনটিকে কল করা কোডে ফেরত দেবে।

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}

এই ফাংশনটি অনেক ছোট করে লেখা যায়, কিন্তু আমরা এরর হ্যান্ডলিং অন্বেষণ করার জন্য প্রথমে এটি ম্যানুয়ালি অনেক কিছু করব; শেষে, আমরা ছোট উপায়টি দেখাব। আসুন প্রথমে ফাংশনের রিটার্ন টাইপটি দেখি: Result<String, io::Error>। এর মানে হলো ফাংশনটি Result<T, E> টাইপের একটি ভ্যালু রিটার্ন করছে, যেখানে জেনেরিক প্যারামিটার T কংক্রিট টাইপ String দিয়ে এবং জেনেরিক টাইপ E কংক্রিট টাইপ io::Error দিয়ে পূর্ণ করা হয়েছে।

যদি এই ফাংশনটি কোনো সমস্যা ছাড়াই সফল হয়, তবে এই ফাংশনটি কল করা কোডটি একটি Ok ভ্যালু পাবে যা একটি String ধারণ করে—এই ফাংশনটি ফাইল থেকে যে username পড়েছে। যদি এই ফাংশনটি কোনো সমস্যার সম্মুখীন হয়, তবে কলিং কোডটি একটি Err ভ্যালু পাবে যা io::Error-এর একটি ইনস্ট্যান্স ধারণ করে যা সমস্যাগুলো কী ছিল সে সম্পর্কে আরও তথ্য ধারণ করে। আমরা এই ফাংশনের রিটার্ন টাইপ হিসাবে io::Error বেছে নিয়েছি কারণ এই ফাংশনের বডিতে আমরা যে দুটি অপারেশন কল করছি যা ব্যর্থ হতে পারে—File::open ফাংশন এবং read_to_string মেথড—উভয় থেকেই রিটার্ন করা এরর ভ্যালুর টাইপ এটি।

ফাংশনের বডি File::open ফাংশন কল করে শুরু হয়। তারপর আমরা লিস্টিং ৯-৪-এর match-এর মতো একটি match দিয়ে Result ভ্যালুটি হ্যান্ডেল করি। যদি File::open সফল হয়, প্যাটার্ন ভ্যারিয়েবল file-এর ফাইল হ্যান্ডেলটি মিউটেবল ভ্যারিয়েবল username_file-এর ভ্যালু হয়ে যায় এবং ফাংশনটি চলতে থাকে। Err ক্ষেত্রে, panic! কল করার পরিবর্তে, আমরা return কীওয়ার্ড ব্যবহার করে ফাংশন থেকে পুরোপুরি আগেভাগে রিটার্ন করি এবং File::open থেকে এরর ভ্যালুটি, এখন প্যাটার্ন ভ্যারিয়েবল e-তে, এই ফাংশনের এরর ভ্যালু হিসাবে কলিং কোডে ফেরত পাঠাই।

সুতরাং, যদি আমাদের username_file-এ একটি ফাইল হ্যান্ডেল থাকে, ফাংশনটি তখন username ভ্যারিয়েবলে একটি নতুন String তৈরি করে এবং username_file-এর ফাইল হ্যান্ডেলের উপর read_to_string মেথড কল করে ফাইলের বিষয়বস্তু username-এ পড়ে। read_to_string মেথডটিও একটি Result রিটার্ন করে কারণ এটিও ব্যর্থ হতে পারে, যদিও File::open সফল হয়েছিল। তাই আমাদের সেই Result হ্যান্ডেল করার জন্য আরেকটি match দরকার: যদি read_to_string সফল হয়, তাহলে আমাদের ফাংশন সফল হয়েছে, এবং আমরা ফাইল থেকে পড়া ইউজারনেমটি, যা এখন username-এ আছে, একটি Ok-তে র‍্যাপ করে রিটার্ন করি। যদি read_to_string ব্যর্থ হয়, আমরা এরর ভ্যালুটি একইভাবে রিটার্ন করি যেভাবে আমরা File::open-এর রিটার্ন ভ্যালু হ্যান্ডেল করা match-এ এরর ভ্যালু রিটার্ন করেছিলাম। তবে, আমাদের স্পষ্টভাবে return বলার প্রয়োজন নেই, কারণ এটি ফাংশনের শেষ এক্সপ্রেশন।

এই কোডটি কল করা কোডটি তখন একটি Ok ভ্যালু যা একটি ইউজারনেম ধারণ করে বা একটি Err ভ্যালু যা একটি io::Error ধারণ করে তা হ্যান্ডেল করবে। সেই ভ্যালুগুলো দিয়ে কী করতে হবে তা সিদ্ধান্ত নেওয়া কলিং কোডের উপর নির্ভর করে। যদি কলিং কোড একটি Err ভ্যালু পায়, তবে এটি panic! কল করে প্রোগ্রাম ক্র্যাশ করতে পারে, একটি ডিফল্ট ইউজারনেম ব্যবহার করতে পারে, অথবা ফাইল ছাড়া অন্য কোথাও থেকে ইউজারনেম খুঁজতে পারে, উদাহরণস্বরূপ। কলিং কোড আসলে কী করার চেষ্টা করছে সে সম্পর্কে আমাদের কাছে পর্যাপ্ত তথ্য নেই, তাই আমরা সমস্ত সফলতা বা এররের তথ্য উপরে প্রচার করি যাতে এটি যথাযথভাবে হ্যান্ডেল করতে পারে।

এরর প্রচারের এই প্যাটার্নটি Rust-এ এতটাই সাধারণ যে Rust এটিকে সহজ করার জন্য প্রশ্নবোধক চিহ্ন অপারেটর ? সরবরাহ করে।

এরর প্রচারের জন্য একটি শর্টকাট: ? অপারেটর

লিস্টিং ৯-৭ read_username_from_file-এর একটি ইমপ্লিমেন্টেশন দেখায় যা লিস্টিং ৯-৬-এর মতোই কার্যকারিতা সম্পন্ন, কিন্তু এই ইমপ্লিমেন্টেশনটি ? অপারেটর ব্যবহার করে।

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}
}

একটি Result ভ্যালুর পরে রাখা ? অপারেটরটি প্রায় একইভাবে কাজ করার জন্য সংজ্ঞায়িত করা হয়েছে যেভাবে আমরা লিস্টিং ৯-৬-এ Result ভ্যালুগুলো হ্যান্ডেল করার জন্য match এক্সপ্রেশন সংজ্ঞায়িত করেছি। যদি Result-এর ভ্যালুটি একটি Ok হয়, তবে Ok-এর ভিতরের ভ্যালুটি এই এক্সপ্রেশন থেকে ফেরত আসবে, এবং প্রোগ্রামটি চলতে থাকবে। যদি ভ্যালুটি একটি Err হয়, তবে Err পুরো ফাংশন থেকে ফেরত আসবে যেন আমরা return কীওয়ার্ড ব্যবহার করেছি যাতে এরর ভ্যালুটি কলিং কোডে প্রচারিত হয়।

লিস্টিং ৯-৬-এর match এক্সপ্রেশন যা করে এবং ? অপারেটর যা করে তার মধ্যে একটি পার্থক্য রয়েছে: যে এরর ভ্যালুগুলোর উপর ? অপারেটর কল করা হয় সেগুলি standard library-এর From trait-এ সংজ্ঞায়িত from ফাংশনের মধ্য দিয়ে যায়, যা এক টাইপের ভ্যালুকে অন্য টাইপে রূপান্তর করতে ব্যবহৃত হয়। যখন ? অপারেটর from ফাংশনটি কল করে, তখন প্রাপ্ত এরর টাইপটি বর্তমান ফাংশনের রিটার্ন টাইপে সংজ্ঞায়িত এরর টাইপে রূপান্তরিত হয়। এটি দরকারী যখন একটি ফাংশন একটি এরর টাইপ রিটার্ন করে যা ফাংশনটি ব্যর্থ হওয়ার সমস্ত উপায়কে প্রতিনিধিত্ব করে, এমনকি যদি অংশগুলি বিভিন্ন কারণে ব্যর্থ হতে পারে।

উদাহরণস্বরূপ, আমরা লিস্টিং ৯-৭-এর read_username_from_file ফাংশনটি পরিবর্তন করে OurError নামের একটি কাস্টম এরর টাইপ রিটার্ন করতে পারি যা আমরা সংজ্ঞায়িত করি। যদি আমরা একটি io::Error থেকে OurError-এর একটি ইনস্ট্যান্স তৈরি করার জন্য impl From<io::Error> for OurError-ও সংজ্ঞায়িত করি, তবে read_username_from_file-এর বডিতে ? অপারেটর কলগুলো from কল করবে এবং ফাংশনে কোনো অতিরিক্ত কোড যোগ না করেই এরর টাইপগুলো রূপান্তর করবে।

লিস্টিং ৯-৭-এর প্রেক্ষাপটে, File::open কলের শেষে ? একটি Ok-এর ভিতরের ভ্যালুটি username_file ভ্যারিয়েবলে রিটার্ন করবে। যদি একটি এরর ঘটে, ? অপারেটরটি পুরো ফাংশন থেকে আগেভাগে রিটার্ন করবে এবং কলিং কোডকে যেকোনো Err ভ্যালু দেবে। একই জিনিস read_to_string কলের শেষে ?-এর ক্ষেত্রেও প্রযোজ্য।

? অপারেটরটি অনেক বয়লারপ্লেট (boilerplate) দূর করে এবং এই ফাংশনের ইমপ্লিমেন্টেশনকে সহজ করে তোলে। আমরা ?-এর ঠিক পরে মেথড কল চেইন করে এই কোডটিকে আরও ছোট করতে পারি, যেমনটি লিস্টিং ৯-৮-এ দেখানো হয়েছে।

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}
}

আমরা username-এ নতুন String তৈরি করাটা ফাংশনের শুরুতে নিয়ে এসেছি; সেই অংশটি পরিবর্তিত হয়নি। username_file ভ্যারিয়েবল তৈরি করার পরিবর্তে, আমরা read_to_string কলটি সরাসরি File::open("hello.txt")?-এর ফলাফলের সাথে চেইন করেছি। read_to_string কলের শেষে আমাদের এখনও একটি ? রয়েছে, এবং File::open এবং read_to_string উভয়ই সফল হলে আমরা এখনও এরর রিটার্ন করার পরিবর্তে username ধারণকারী একটি Ok ভ্যালু রিটার্ন করি। কার্যকারিতা আবার লিস্টিং ৯-৬ এবং লিস্টিং ৯-৭-এর মতোই; এটি লেখার একটি ভিন্ন, আরও সুবিধাজনক উপায়।

লিস্টিং ৯-৯ fs::read_to_string ব্যবহার করে এটিকে আরও ছোট করার একটি উপায় দেখায়।

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

একটি ফাইলকে একটি স্ট্রিং-এ পড়া একটি মোটামুটি সাধারণ অপারেশন, তাই standard library সুবিধাজনক fs::read_to_string ফাংশন সরবরাহ করে যা ফাইলটি খোলে, একটি নতুন String তৈরি করে, ফাইলের বিষয়বস্তু পড়ে, সেই String-এ বিষয়বস্তু রাখে এবং এটি রিটার্ন করে। অবশ্যই, fs::read_to_string ব্যবহার করা আমাদের সমস্ত এরর হ্যান্ডলিং ব্যাখ্যা করার সুযোগ দেয় না, তাই আমরা প্রথমে দীর্ঘ উপায়টি করেছি।

কোথায় ? অপারেটর ব্যবহার করা যেতে পারে

? অপারেটরটি শুধুমাত্র সেই ফাংশনগুলিতে ব্যবহার করা যেতে পারে যাদের রিটার্ন টাইপ ? যে ভ্যালুর উপর ব্যবহৃত হয় তার সাথে সামঞ্জস্যপূর্ণ। এটি কারণ ? অপারেটরটি একটি ফাংশন থেকে একটি ভ্যালুর আগেভাগে রিটার্ন করার জন্য সংজ্ঞায়িত করা হয়েছে, ঠিক যেমনটি আমরা লিস্টিং ৯-৬-এ সংজ্ঞায়িত match এক্সপ্রেশনের মতো। লিস্টিং ৯-৬-এ, match একটি Result ভ্যালু ব্যবহার করছিল, এবং আগেভাগে রিটার্ন করা arm-টি একটি Err(e) ভ্যালু রিটার্ন করেছিল। ফাংশনের রিটার্ন টাইপটি একটি Result হতে হবে যাতে এটি এই return-এর সাথে সামঞ্জস্যপূর্ণ হয়।

লিস্টিং ৯-১০-এ, আসুন দেখি আমরা যদি একটি main ফাংশনে ? অপারেটর ব্যবহার করি যার রিটার্ন টাইপ আমরা যে ভ্যালুর উপর ? ব্যবহার করি তার টাইপের সাথে অসামঞ্জস্যপূর্ণ হয় তবে আমরা কী এরর পাব।

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

এই কোডটি একটি ফাইল খোলে, যা ব্যর্থ হতে পারে। ? অপারেটরটি File::open দ্বারা রিটার্ন করা Result ভ্যালুটিকে অনুসরণ করে, কিন্তু এই main ফাংশনটির রিটার্ন টাইপ () , Result নয়। যখন আমরা এই কোডটি কম্পাইল করি, তখন আমরা নিম্নলিখিত এরর বার্তাটি পাই:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:48
  |
3 | fn main() {
  | --------- this function should return `Result` or `Option` to accept `?`
4 |     let greeting_file = File::open("hello.txt")?;
  |                                                ^ cannot use the `?` operator in a function that returns `()`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
help: consider adding return type
  |
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 |     let greeting_file = File::open("hello.txt")?;
5 +     Ok(())
  |

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

এই এররটি নির্দেশ করে যে আমরা শুধুমাত্র সেই ফাংশনে ? অপারেটর ব্যবহার করতে পারি যা Result, Option, বা FromResidual ইমপ্লিমেন্ট করে এমন অন্য কোনো টাইপ রিটার্ন করে।

এররটি ঠিক করার জন্য, আপনার দুটি বিকল্প রয়েছে। একটি বিকল্প হলো আপনার ফাংশনের রিটার্ন টাইপ পরিবর্তন করে আপনি যে ভ্যালুর উপর ? অপারেটর ব্যবহার করছেন তার সাথে সামঞ্জস্যপূর্ণ করা, যতক্ষণ না আপনার কোনো সীমাবদ্ধতা থাকে যা এটি প্রতিরোধ করে। অন্য বিকল্পটি হলো Result<T, E>-কে যেভাবে উপযুক্ত সেভাবে হ্যান্ডেল করার জন্য একটি match বা Result<T, E>-এর কোনো মেথড ব্যবহার করা।

এরর বার্তাটিতে আরও উল্লেখ করা হয়েছে যে ? Option<T> ভ্যালুগুলোর সাথেও ব্যবহার করা যেতে পারে। Result-এর উপর ? ব্যবহারের মতোই, আপনি শুধুমাত্র সেই ফাংশনে Option-এর উপর ? ব্যবহার করতে পারেন যা একটি Option রিটার্ন করে। Option<T>-এর উপর কল করা হলে ? অপারেটরের আচরণ Result<T, E>-এর উপর কল করা হলে তার আচরণের মতোই: যদি ভ্যালুটি None হয়, তবে সেই সময়ে ফাংশন থেকে None আগেভাগে রিটার্ন করা হবে। যদি ভ্যালুটি Some হয়, তবে Some-এর ভিতরের ভ্যালুটি এক্সপ্রেশনের ফলস্বরূপ ভ্যালু হয়, এবং ফাংশনটি চলতে থাকে। লিস্টিং ৯-১১-এ একটি ফাংশনের উদাহরণ রয়েছে যা প্রদত্ত টেক্সটের প্রথম লাইনের শেষ অক্ষরটি খুঁজে বের করে।

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Hello, world\nHow are you today?"),
        Some('d')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}```

</Listing>

এই ফাংশনটি `Option<char>` রিটার্ন করে কারণ এটি সম্ভব যে সেখানে একটি অক্ষর আছে, কিন্তু এটিও সম্ভব যে সেখানে নেই। এই কোডটি `text` স্ট্রিং স্লাইস আর্গুমেন্টটি নেয় এবং এর উপর `lines` মেথড কল করে, যা স্ট্রিং-এর লাইনগুলোর উপর একটি ইটারেটর রিটার্ন করে। যেহেতু এই ফাংশনটি প্রথম লাইনটি পরীক্ষা করতে চায়, এটি ইটারেটরের প্রথম ভ্যালুটি পেতে ইটারেটরের উপর `next` কল করে। যদি `text` একটি খালি স্ট্রিং হয়, তবে এই `next` কলটি `None` রিটার্ন করবে, সেক্ষেত্রে আমরা `?` ব্যবহার করে `last_char_of_first_line` থেকে `None` রিটার্ন করে থেমে যাই। যদি `text` একটি খালি স্ট্রিং না হয়, `next` একটি `Some` ভ্যালু রিটার্ন করবে যা `text`-এর প্রথম লাইনের একটি স্ট্রিং স্লাইস ধারণ করে।

`?` স্ট্রিং স্লাইসটি এক্সট্র্যাক্ট করে, এবং আমরা সেই স্ট্রিং স্লাইসের উপর `chars` কল করে এর অক্ষরগুলোর একটি ইটারেটর পেতে পারি। আমরা এই প্রথম লাইনের শেষ অক্ষরে আগ্রহী, তাই আমরা ইটারেটরের শেষ আইটেমটি রিটার্ন করার জন্য `last` কল করি। এটি একটি `Option` কারণ এটি সম্ভব যে প্রথম লাইনটি একটি খালি স্ট্রিং; উদাহরণস্বরূপ, যদি `text` একটি ফাঁকা লাইন দিয়ে শুরু হয় কিন্তু অন্য লাইনে অক্ষর থাকে, যেমন `"\nhi"`। তবে, যদি প্রথম লাইনে একটি শেষ অক্ষর থাকে, তবে এটি `Some` ভ্যারিয়েন্টে রিটার্ন করা হবে। মাঝখানে `?` অপারেটরটি আমাদের এই লজিকটি সংক্ষিপ্তভাবে প্রকাশ করার একটি উপায় দেয়, যা আমাদের ফাংশনটি এক লাইনে ইমপ্লিমেন্ট করতে দেয়। যদি আমরা `Option`-এর উপর `?` অপারেটর ব্যবহার করতে না পারতাম, তবে আমাদের এই লজিকটি আরও মেথড কল বা একটি `match` এক্সপ্রেশন ব্যবহার করে ইমপ্লিমেন্ট করতে হতো।

মনে রাখবেন যে আপনি একটি ফাংশনে `Result`-এর উপর `?` অপারেটর ব্যবহার করতে পারেন যা `Result` রিটার্ন করে, এবং আপনি একটি ফাংশনে `Option`-এর উপর `?` অপারেটর ব্যবহার করতে পারেন যা `Option` রিটার্ন করে, কিন্তু আপনি মিশ্রণ করতে পারবেন না। `?` অপারেটরটি স্বয়ংক্রিয়ভাবে একটি `Result`-কে একটি `Option`-এ বা তার বিপরীতে রূপান্তর করবে না; সেই ক্ষেত্রে, আপনি স্পষ্টভাবে রূপান্তর করার জন্য `Result`-এর উপর `ok` মেথড বা `Option`-এর উপর `ok_or` মেথডের মতো মেথড ব্যবহার করতে পারেন।

এখন পর্যন্ত, আমরা যে সমস্ত `main` ফাংশন ব্যবহার করেছি সেগুলি `()` রিটার্ন করে। `main` ফাংশনটি বিশেষ কারণ এটি একটি এক্সিকিউটেবল প্রোগ্রামের প্রবেশ এবং প্রস্থান বিন্দু, এবং প্রোগ্রামটি প্রত্যাশিতভাবে আচরণ করার জন্য এর রিটার্ন টাইপের উপর বিধিনিষেধ রয়েছে।

সৌভাগ্যবশত, `main` একটি `Result<(), E>`-ও রিটার্ন করতে পারে। লিস্টিং ৯-১২-এ লিস্টিং ৯-১০-এর কোড রয়েছে, কিন্তু আমরা `main`-এর রিটার্ন টাইপ পরিবর্তন করে `Result<(), Box<dyn Error>>` করেছি এবং শেষে একটি রিটার্ন ভ্যালু `Ok(())` যোগ করেছি। এই কোডটি এখন কম্পাইল হবে।

<Listing number="9-12" file-name="src/main.rs" caption="`main`-কে `Result<(), E>` রিটার্ন করার জন্য পরিবর্তন করা `Result` ভ্যালুগুলোর উপর `?` অপারেটর ব্যবহারের অনুমতি দেয়।">

```rust,ignore
use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

Box<dyn Error> টাইপটি একটি trait object, যা আমরা অধ্যায় ১৮-এর “Using Trait Objects That Allow for Values of Different Types” বিভাগে আলোচনা করব। আপাতত, আপনি Box<dyn Error>-কে “যেকোনো ধরনের এরর” হিসাবে পড়তে পারেন। main ফাংশনে Box<dyn Error> এরর টাইপসহ একটি Result ভ্যালুর উপর ? ব্যবহার করা অনুমোদিত কারণ এটি যেকোনো Err ভ্যালুকে আগেভাগে রিটার্ন করার অনুমতি দেয়। যদিও এই main ফাংশনের বডি শুধুমাত্র std::io::Error টাইপের এরর রিটার্ন করবে, Box<dyn Error> নির্দিষ্ট করার মাধ্যমে, এই সিগনেচারটি সঠিক থাকবে এমনকি যদি main-এর বডিতে অন্য এরর রিটার্ন করে এমন আরও কোড যোগ করা হয়।

যখন একটি main ফাংশন একটি Result<(), E> রিটার্ন করে, তখন এক্সিকিউটেবলটি 0 ভ্যালু দিয়ে প্রস্থান করবে যদি main Ok(()) রিটার্ন করে এবং একটি নন-জিরো ভ্যালু দিয়ে প্রস্থান করবে যদি main একটি Err ভ্যালু রিটার্ন করে। C-তে লেখা এক্সিকিউটেবলগুলো প্রস্থান করার সময় ইন্টিজার রিটার্ন করে: যে প্রোগ্রামগুলো সফলভাবে প্রস্থান করে সেগুলি 0 ইন্টিজার রিটার্ন করে, এবং যে প্রোগ্রামগুলো এরর করে সেগুলি 0 ছাড়া অন্য কোনো ইন্টিজার রিটার্ন করে। Rust এই কনভেনশনের সাথে সামঞ্জস্যপূর্ণ হওয়ার জন্য এক্সিকিউটেবল থেকে ইন্টিজার রিটার্ন করে।

main ফাংশনটি যেকোনো টাইপ রিটার্ন করতে পারে যা std::process::Termination trait ইমপ্লিমেন্ট করে, যা একটি report ফাংশন ধারণ করে যা একটি ExitCode রিটার্ন করে। আপনার নিজের টাইপের জন্য Termination trait ইমপ্লিমেন্ট করার বিষয়ে আরও তথ্যের জন্য standard library ডকুমেন্টেশন দেখুন।

এখন যেহেতু আমরা panic! কল করা বা Result রিটার্ন করার বিস্তারিত আলোচনা করেছি, আসুন আমরা কোন ক্ষেত্রে কোনটি ব্যবহার করা উপযুক্ত তা সিদ্ধান্ত নেওয়ার বিষয়ে ফিরে যাই।