Result সহ পুনরুদ্ধারযোগ্য ত্রুটি (Recoverable Errors with Result)

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

চ্যাপ্টার ২-এর Result দিয়ে সম্ভাব্য ব্যর্থতা হ্যান্ডেল করা” থেকে স্মরণ করুন যে Result এনামটি দুটি ভেরিয়েন্ট, Ok এবং Err সহ সংজ্ঞায়িত করা হয়েছে, নিম্নরূপ:

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

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

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

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 দিয়ে পূরণ করা হয়েছে, যা একটি ফাইল হ্যান্ডেল। এরর মানে ব্যবহৃত E-এর টাইপ হল std::io::Error। এই রিটার্ন টাইপের অর্থ হল File::open-এর কলটি সফল হতে পারে এবং একটি ফাইল হ্যান্ডেল রিটার্ন করতে পারে যা থেকে আমরা পড়তে বা লিখতে পারি। ফাংশন কলটিও ব্যর্থ হতে পারে: উদাহরণস্বরূপ, ফাইলটি বিদ্যমান নাও থাকতে পারে, অথবা আমাদের ফাইলটি অ্যাক্সেস করার অনুমতি নাও থাকতে পারে। File::open ফাংশনটির আমাদের বলার একটি উপায় থাকতে হবে যে এটি সফল হয়েছে নাকি ব্যর্থ হয়েছে এবং একই সাথে আমাদের ফাইল হ্যান্ডেল বা এরর সম্পর্কিত তথ্য দিতে হবে। এই তথ্যটিই Result এনাম প্রকাশ করে।

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

আমাদের Listing 9-3-এর কোডে File::open যে মান রিটার্ন করে তার উপর নির্ভর করে ভিন্ন ভিন্ন পদক্ষেপ নিতে কোড যোগ করতে হবে। Listing 9-4 একটি বেসিক টুল ব্যবহার করে 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 এনামের মতো, Result এনাম এবং এর ভেরিয়েন্টগুলো প্রেলিউড (prelude) দ্বারা স্কোপে আনা হয়েছে, তাই আমাদের match আর্মগুলোতে Ok এবং Err ভেরিয়েন্টগুলোর আগে Result:: উল্লেখ করার প্রয়োজন নেই।

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

match-এর অন্য আর্মটি সেই ক্ষেত্রটি হ্যান্ডেল করে যেখানে আমরা 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)

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

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:?}"),
            },
            other_error => {
                panic!("Problem opening the file: {other_error:?}");
            }
        },
    };
}

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

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

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

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

উদাহরণস্বরূপ, Listing 9-5-এর মতোই একই লজিক লেখার আরেকটি উপায় এখানে দেওয়া হল, এবার ক্লোজার এবং 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:?}");
        }
    });
}

যদিও এই কোডটির Listing 9-5-এর মতোই আচরণ রয়েছে, তবে এতে কোনো match এক্সপ্রেশন নেই এবং এটি পড়া আরও সহজ। আপনি চ্যাপ্টার 13 পড়ার পরে এই উদাহরণটিতে ফিরে আসুন এবং স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশনে unwrap_or_else মেথডটি দেখুন। এরর নিয়ে কাজ করার সময় আরও অনেকগুলো মেথড বিশাল নেস্টেড match এক্সপ্রেশনগুলোকে পরিষ্কার করতে পারে।

এরর-এ প্যানিকের জন্য শর্টকাট: unwrap এবং expect (Shortcuts for Panic on Error: unwrap and expect)

match ব্যবহার করা যথেষ্ট ভাল কাজ করে, কিন্তু এটি কিছুটা শব্দবহুল হতে পারে এবং সর্বদাই অভিপ্রায়টি ভালভাবে প্রকাশ করে না। Result<T, E> টাইপে বিভিন্ন, আরও নির্দিষ্ট কাজ করার জন্য অনেকগুলি হেল্পার মেথড সংজ্ঞায়িত করা হয়েছে। unwrap মেথড হল একটি শর্টকাট মেথড যা আমরা Listing 9-4-এ লিখেছি এমন 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" }

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

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

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

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

#![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 ফাংশন কল করে শুরু হয়। তারপর আমরা Listing 9-4-এর match-এর মতোই একটি match দিয়ে Result মানটি হ্যান্ডেল করি। যদি File::open সফল হয়, তাহলে প্যাটার্ন ভেরিয়েবল file-এর ফাইল হ্যান্ডেলটি মিউটেবল ভেরিয়েবল username_file-এর মান হয়ে যায় এবং ফাংশনটি চলতে থাকে। Err কেসের ক্ষেত্রে, panic! কল করার পরিবর্তে, আমরা ফাংশন থেকে সম্পূর্ণরূপে তাড়াতাড়ি রিটার্ন করার জন্য return কীওয়ার্ড ব্যবহার করি এবং File::open থেকে এরর মানটি, এখন প্যাটার্ন ভেরিয়েবল e-তে, এই ফাংশনের এরর মান হিসাবে কলিং কোডে ফেরত পাঠাই।

সুতরাং, যদি আমাদের username_file-এ একটি ফাইল হ্যান্ডেল থাকে, তাহলে ফাংশনটি ভেরিয়েবল username-এ একটি নতুন String তৈরি করে এবং ফাইল হ্যান্ডেলে 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 এটিকে সহজ করার জন্য প্রশ্নবোধক চিহ্ন অপারেটর ? সরবরাহ করে।

এরর প্রচার করার জন্য একটি শর্টকাট: ? অপারেটর (A Shortcut for Propagating Errors: the ? Operator)

Listing 9-7 read_username_from_file-এর একটি ইমপ্লিমেন্টেশন দেখায় যা Listing 9-6-এর মতোই একই কার্যকারিতা সম্পন্ন করে, কিন্তু এই ইমপ্লিমেন্টেশনটি ? অপারেটর ব্যবহার করে।

#![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 মানের পরে স্থাপিত ? প্রায় Listing 9-6-এ Result মানগুলো হ্যান্ডেল করার জন্য আমরা যে match এক্সপ্রেশনগুলো সংজ্ঞায়িত করেছি তার মতোই কাজ করার জন্য সংজ্ঞায়িত করা হয়েছে। যদি Result-এর মানটি একটি Ok হয়, তাহলে Ok-এর ভিতরের মানটি এই এক্সপ্রেশন থেকে রিটার্ন করা হবে এবং প্রোগ্রামটি চলতে থাকবে। যদি মানটি একটি Err হয়, তাহলে Err টি সম্পূর্ণ ফাংশন থেকে রিটার্ন করা হবে যেন আমরা return কীওয়ার্ড ব্যবহার করেছি যাতে এরর মানটি কলিং কোডে প্রচারিত হয়।

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

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

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

? অপারেটর অনেক বয়লারপ্লেট দূর করে এবং এই ফাংশনের ইমপ্লিমেন্টেশনকে সহজ করে তোলে। আমরা Listing 9-8-এ দেখানো ?-এর পরে অবিলম্বে মেথড কল চেইন করে এই কোডটিকে আরও ছোট করতে পারি।

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)
}

fn main() {
    let username = read_username_from_file().expect("Unable to get username");
}

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

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

use std::fs;
use std::io;

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

fn main() {
    let username = read_username_from_file().expect("Unable to get username");
}

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

? অপারেটর কোথায় ব্যবহার করা যেতে পারে (Where The ? Operator Can Be Used)

? অপারেটরটি শুধুমাত্র এমন ফাংশনগুলোতে ব্যবহার করা যেতে পারে যাদের রিটার্ন টাইপ সেই মানের সাথে সামঞ্জস্যপূর্ণ যেখানে ? ব্যবহার করা হয়েছে। এর কারণ হল ? অপারেটরটি Listing 9-6-এ সংজ্ঞায়িত match এক্সপ্রেশনের মতোই ফাংশন থেকে তাড়াতাড়ি একটি মান রিটার্ন করার জন্য সংজ্ঞায়িত করা হয়েছে। Listing 9-6-এ, match একটি Result মান ব্যবহার করছিল এবং আর্লি রিটার্ন আর্ম একটি Err(e) মান রিটার্ন করছিল। ফাংশনের রিটার্ন টাইপটিকে অবশ্যই একটি Result হতে হবে যাতে এটি এই return-এর সাথে সামঞ্জস্যপূর্ণ হয়।

Listing 9-10-এ, আসুন আমরা একটি 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 ইমপ্লিমেন্ট করে।

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

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

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);
}

এই ফাংশনটি 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> রিটার্ন করতে পারে। Listing 9-12-এ Listing 9-10-এর কোড রয়েছে, কিন্তু আমরা main-এর রিটার্ন টাইপ পরিবর্তন করে Result<(), Box<dyn Error>> করেছি এবং শেষে একটি রিটার্ন ভ্যালু Ok(()) যোগ করেছি। এই কোডটি এখন কম্পাইল হবে।

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), যা নিয়ে আমরা চ্যাপ্টার 18-এর “ভিন্ন টাইপের মানের জন্য অনুমতি দেয় এমন ট্রেইট অবজেক্ট ব্যবহার করা”-তে কথা বলব। আপাতত, আপনি Box<dyn Error>-কে "যেকোনো ধরনের এরর" হিসাবে পড়তে পারেন। এরর টাইপ Box<dyn Error> সহ একটি main ফাংশনে Result মানের উপর ? ব্যবহার করার অনুমতি রয়েছে কারণ এটি যেকোনো Err মানকে তাড়াতাড়ি রিটার্ন করার অনুমতি দেয়। যদিও এই main ফাংশনের বডি শুধুমাত্র std::io::Error টাইপের এরর রিটার্ন করবে, Box<dyn Error> নির্দিষ্ট করে, এই সিগনেচারটি সঠিক থাকবে এমনকী যদি main-এর বডিতে আরও কোড যোগ করা হয় যা অন্যান্য এরর রিটার্ন করে।

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

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

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