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

মডুলারিটি এবং এরর হ্যান্ডলিং উন্নত করার জন্য রিফ্যাক্টরিং (Refactoring to Improve Modularity and Error Handling)

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

এই বিষয়টি দ্বিতীয় সমস্যার সাথেও জড়িত: যদিও query এবং file_path আমাদের প্রোগ্রামের কনফিগারেশন ভেরিয়েবল, কিন্তু contents-এর মতো ভেরিয়েবলগুলো প্রোগ্রামের লজিক সম্পাদনের জন্য ব্যবহৃত হয়। main ফাংশন যত দীর্ঘ হবে, আমাদের তত বেশি ভেরিয়েবল স্কোপে আনতে হবে; স্কোপে যত বেশি ভেরিয়েবল থাকবে, প্রতিটির উদ্দেশ্য মনে রাখা তত কঠিন হবে। কনফিগারেশন ভেরিয়েবলগুলোকে একটি স্ট্রাকচারে একত্রিত করে তাদের উদ্দেশ্য পরিষ্কার করে তোলাই শ্রেয়।

তৃতীয় সমস্যা হলো, ফাইল পড়তে ব্যর্থ হলে আমরা এরর মেসেজ প্রিন্ট করার জন্য expect ব্যবহার করেছি, কিন্তু এরর মেসেজটি শুধু Should have been able to read the file প্রিন্ট করে। একটি ফাইল পড়া বিভিন্ন কারণে ব্যর্থ হতে পারে: যেমন, ফাইলটি অনুপস্থিত থাকতে পারে, অথবা আমাদের কাছে এটি খোলার অনুমতি নাও থাকতে পারে। এখন, পরিস্থিতি যাই হোক না কেন, আমরা সবকিছুর জন্য একই এরর মেসেজ প্রিন্ট করব, যা ব্যবহারকারীকে কোনো তথ্য দেবে না!

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

চলুন আমাদের প্রজেক্ট রিফ্যাক্টর করে এই চারটি সমস্যা সমাধান করি।

বাইনারি প্রজেক্টের জন্য কাজের দায়িত্ব পৃথকীকরণ (Separation of Concerns for Binary Projects)

main ফাংশনে একাধিক কাজের দায়িত্ব অর্পণের সাংগঠনিক সমস্যাটি অনেক বাইনারি প্রজেক্টের জন্য সাধারণ। ফলস্বরূপ, অনেক Rust প্রোগ্রামার main ফাংশন বড় হতে শুরু করলে একটি বাইনারি প্রোগ্রামের পৃথক কাজগুলোকে বিভক্ত করা দরকারী বলে মনে করেন। এই প্রক্রিয়ার নিম্নলিখিত ধাপগুলো রয়েছে:

  • আপনার প্রোগ্রামকে একটি main.rs এবং একটি lib.rs ফাইলে বিভক্ত করুন এবং আপনার প্রোগ্রামের লজিক lib.rs-এ সরিয়ে নিন।
  • যতক্ষণ আপনার কমান্ড লাইন পার্সিং লজিক ছোট থাকে, ততক্ষণ এটি main ফাংশনে থাকতে পারে।
  • যখন কমান্ড লাইন পার্সিং লজিক জটিল হতে শুরু করে, তখন এটিকে main ফাংশন থেকে অন্য ফাংশন বা টাইপে এক্সট্র্যাক্ট করুন।

এই প্রক্রিয়ার পরে main ফাংশনে যে দায়িত্বগুলো থাকবে তা নিম্নলিখিতগুলির মধ্যে সীমাবদ্ধ থাকা উচিত:

  • আর্গুমেন্ট ভ্যালুগুলো দিয়ে কমান্ড লাইন পার্সিং লজিক কল করা
  • অন্যান্য যেকোনো কনফিগারেশন সেট আপ করা
  • lib.rs-এ একটি run ফাংশন কল করা
  • run ফাংশন এরর রিটার্ন করলে সেই এরর হ্যান্ডেল করা

এই প্যাটার্নটি হলো কাজগুলোকে আলাদা অংশে ভাগ করা (separating concerns): main.rs প্রোগ্রাম চালানো পরিচালনা করে এবং lib.rs হাতের কাজটির সমস্ত লজিক পরিচালনা করে। যেহেতু আপনি সরাসরি main ফাংশন টেস্ট করতে পারবেন না, তাই এই কাঠামোটি আপনাকে আপনার প্রোগ্রামের সমস্ত লজিক main ফাংশন থেকে বের করে এনে টেস্ট করার সুযোগ দেয়। main ফাংশনে যে কোড অবশিষ্ট থাকবে তা পড়ে এর সঠিকতা যাচাই করার জন্য যথেষ্ট ছোট হবে। চলুন এই প্রক্রিয়া অনুসরণ করে আমাদের প্রোগ্রামটি পুনরায় সাজাই।

আর্গুমেন্ট পার্সার এক্সট্র্যাক্ট করা

আমরা আর্গুমেন্ট পার্স করার ফাংশনালিটি একটি ফাংশনে এক্সট্র্যাক্ট করব যা main কল করবে। লিস্টিং ১২-৫ main ফাংশনের নতুন শুরু দেখাচ্ছে যা একটি নতুন ফাংশন parse_config কল করে, যা আমরা src/main.rs-এ ডিফাইন করব।

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}

আমরা এখনও কমান্ড লাইন আর্গুমেন্টগুলোকে একটি ভেক্টরে সংগ্রহ করছি, কিন্তু main ফাংশনের মধ্যে ইনডেক্স ১-এর আর্গুমেন্ট ভ্যালু query ভেরিয়েবলে এবং ইনডেক্স ২-এর আর্গুমেন্ট ভ্যালু file_path ভেরিয়েবলে অ্যাসাইন করার পরিবর্তে, আমরা পুরো ভেক্টরটি parse_config ফাংশনে পাস করছি। parse_config ফাংশনটি তখন সেই লজিক ধারণ করে যা নির্ধারণ করে কোন আর্গুমেন্ট কোন ভেরিয়েবলে যাবে এবং ভ্যালুগুলো main-এ ফেরত পাঠায়। আমরা এখনও main-এ query এবং file_path ভেরিয়েবল তৈরি করি, কিন্তু main-এর আর কমান্ড লাইন আর্গুমেন্ট এবং ভেরিয়েবলগুলো কীভাবে সম্পর্কিত তা নির্ধারণের দায়িত্ব নেই।

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

কনফিগারেশন ভ্যালুগুলোকে গ্রুপ করা

আমরা parse_config ফাংশনটিকে আরও উন্নত করতে আরও একটি ছোট পদক্ষেপ নিতে পারি। এই মুহূর্তে, আমরা একটি টাপল (tuple) রিটার্ন করছি, কিন্তু তারপরে আমরা অবিলম্বে সেই টাপলটিকে আবার পৃথক অংশে বিভক্ত করছি। এটি একটি লক্ষণ যে সম্ভবত আমাদের কাছে এখনও সঠিক অ্যাবস্ট্র্যাকশন নেই।

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

লিস্টিং ১২-৬ parse_config ফাংশনের উন্নতিগুলো দেখাচ্ছে।

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}

আমরা query এবং file_path নামে ফিল্ড থাকার জন্য ডিফাইন করা Config নামে একটি struct যোগ করেছি। parse_config-এর সিগনেচার এখন নির্দেশ করে যে এটি একটি Config ভ্যালু রিটার্ন করে। parse_config-এর বডিতে, যেখানে আমরা আগে args-এ String ভ্যালুগুলোকে রেফারেন্স করে এমন স্ট্রিং স্লাইস রিটার্ন করতাম, সেখানে আমরা এখন Config-কে নিজস্ব String ভ্যালু ধারণ করার জন্য ডিফাইন করেছি। main-এর args ভেরিয়েবলটি আর্গুমেন্ট ভ্যালুগুলোর মালিক এবং শুধুমাত্র parse_config ফাংশনকে সেগুলো ধার করতে দিচ্ছে, যার মানে হলো যদি Config args-এর ভ্যালুগুলোর মালিকানা নেওয়ার চেষ্টা করত তবে আমরা Rust-এর borrowing rule লঙ্ঘন করতাম।

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

clone ব্যবহারের ট্রেড-অফ

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

আমরা main-কে আপডেট করেছি যাতে এটি parse_config দ্বারা রিটার্ন করা Config-এর ইনস্ট্যান্সটিকে config নামের একটি ভেরিয়েবলে রাখে, এবং আমরা আগের কোড যা পৃথক query এবং file_path ভেরিয়েবল ব্যবহার করত তা আপডেট করেছি যাতে এটি এখন Config স্ট্রাকটের ফিল্ডগুলো ব্যবহার করে।

এখন আমাদের কোড আরও পরিষ্কারভাবে বোঝায় যে query এবং file_path সম্পর্কিত এবং তাদের উদ্দেশ্য হলো প্রোগ্রামটি কীভাবে কাজ করবে তা কনফিগার করা। এই ভ্যালুগুলো ব্যবহার করে এমন যেকোনো কোড জানে যে তাদের config ইনস্ট্যান্সের মধ্যে তাদের উদ্দেশ্যের জন্য নামকরণ করা ফিল্ডগুলোতে খুঁজে পাওয়া যাবে।

Config-এর জন্য একটি কনস্ট্রাকটর (Constructor) তৈরি করা

এখন পর্যন্ত, আমরা main থেকে কমান্ড লাইন আর্গুমেন্ট পার্স করার জন্য দায়ী লজিকটি parse_config ফাংশনে এক্সট্র্যাক্ট করেছি। এটি করতে গিয়ে আমরা দেখতে পেয়েছি যে query এবং file_path ভ্যালুগুলো সম্পর্কিত ছিল এবং এই সম্পর্কটি আমাদের কোডে প্রকাশ করা উচিত। এরপর আমরা query এবং file_path-এর সম্পর্কিত উদ্দেশ্যকে নাম দেওয়ার জন্য এবং parse_config ফাংশন থেকে ভ্যালুগুলোর নাম স্ট্রাকট ফিল্ডের নাম হিসেবে রিটার্ন করতে সক্ষম হওয়ার জন্য একটি Config স্ট্রাকট যোগ করেছি।

এখন যেহেতু parse_config ফাংশনের উদ্দেশ্য একটি Config ইনস্ট্যান্স তৈরি করা, আমরা parse_config-কে একটি সাধারণ ফাংশন থেকে Config স্ট্রাকটের সাথে যুক্ত new নামের একটি ফাংশনে পরিবর্তন করতে পারি। এই পরিবর্তনটি কোডকে আরও ইডিওম্যাটিক (idiomatic) করে তুলবে। আমরা standard library-র টাইপের ইনস্ট্যান্স, যেমন String, String::new কল করে তৈরি করতে পারি। একইভাবে, parse_config-কে Config-এর সাথে যুক্ত একটি new ফাংশনে পরিবর্তন করে, আমরা Config::new কল করে Config-এর ইনস্ট্যান্স তৈরি করতে সক্ষম হব। লিস্টিং ১২-৭ দেখাচ্ছে আমাদের কী কী পরিবর্তন করতে হবে।

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");

    // --snip--
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

আমরা main-কে আপডেট করেছি যেখানে আমরা parse_config কল করছিলাম তার পরিবর্তে Config::new কল করার জন্য। আমরা parse_config-এর নাম পরিবর্তন করে new করেছি এবং এটিকে একটি impl ব্লকের মধ্যে সরিয়ে দিয়েছি, যা new ফাংশনটিকে Config-এর সাথে যুক্ত করে। এই কোডটি আবার কম্পাইল করে নিশ্চিত করুন যে এটি কাজ করে।

এরর হ্যান্ডলিং ঠিক করা

এখন আমরা আমাদের এরর হ্যান্ডলিং ঠিক করার কাজ করব। মনে রাখবেন যে args ভেক্টরের ইনডেক্স ১ বা ইনডেক্স ২-এর ভ্যালু অ্যাক্সেস করার চেষ্টা করলে প্রোগ্রামটি প্যানিক করবে যদি ভেক্টরে তিনটির কম আইটেম থাকে। কোনো আর্গুমেন্ট ছাড়াই প্রোগ্রামটি চালানোর চেষ্টা করুন; এটি দেখতে এমন হবে:

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

thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

index out of bounds: the len is 1 but the index is 1 লাইনটি প্রোগ্রামারদের জন্য একটি এরর মেসেজ। এটি আমাদের এন্ড-ইউজারদের বুঝতে সাহায্য করবে না যে তাদের পরিবর্তে কী করা উচিত। চলুন এখন এটি ঠিক করি।

এরর মেসেজ উন্নত করা

লিস্টিং ১২-৮-এ, আমরা new ফাংশনে একটি চেক যোগ করছি যা ইনডেক্স ১ এবং ইনডেক্স ২ অ্যাক্সেস করার আগে স্লাইসটি যথেষ্ট দীর্ঘ কিনা তা যাচাই করবে। যদি স্লাইসটি যথেষ্ট দীর্ঘ না হয়, প্রোগ্রামটি প্যানিক করে এবং একটি ভালো এরর মেসেজ প্রদর্শন করে।

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}

এই কোডটি লিস্টিং ৯-১৩-এ আমরা যে Guess::new ফাংশন লিখেছিলাম তার মতোই, যেখানে value আর্গুমেন্টটি বৈধ মানের সীমার বাইরে থাকলে আমরা panic! কল করেছিলাম। এখানে মানের একটি পরিসর পরীক্ষা করার পরিবর্তে, আমরা পরীক্ষা করছি যে args-এর দৈর্ঘ্য কমপক্ষে 3 এবং ফাংশনের বাকি অংশ এই শর্তটি পূরণ হয়েছে এই অনুমানের অধীনে কাজ করতে পারে। যদি args-এর তিনটি আইটেমের কম থাকে, এই শর্তটি true হবে এবং আমরা প্রোগ্রামটি অবিলম্বে শেষ করার জন্য panic! ম্যাক্রো কল করি।

new-তে এই অতিরিক্ত কয়েকটি লাইন কোড দিয়ে, চলুন কোনো আর্গুমেন্ট ছাড়াই প্রোগ্রামটি আবার চালাই এবং দেখি এররটি এখন কেমন দেখায়:

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

thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

এই আউটপুটটি ভালো: আমাদের এখন একটি যুক্তিসঙ্গত এরর মেসেজ আছে। যাইহোক, আমাদের কাছে অপ্রয়োজনীয় তথ্যও রয়েছে যা আমরা আমাদের ব্যবহারকারীদের দিতে চাই না। সম্ভবত লিস্টিং ৯-১৩-এ আমরা যে কৌশলটি ব্যবহার করেছি তা এখানে ব্যবহার করার জন্য সেরা নয়: একটি panic! কল একটি ব্যবহারের সমস্যার চেয়ে একটি প্রোগ্রামিং সমস্যার জন্য বেশি উপযুক্ত, যেমনটি অধ্যায় ৯-এ আলোচনা করা হয়েছে। পরিবর্তে, আমরা অধ্যায় ৯-এ আপনার শেখা অন্য কৌশলটি ব্যবহার করব—একটি Result রিটার্ন করা যা হয় সাফল্য বা একটি এরর নির্দেশ করে।

panic! কল করার পরিবর্তে Result রিটার্ন করা

আমরা পরিবর্তে একটি Result ভ্যালু রিটার্ন করতে পারি যা সফল ক্ষেত্রে একটি Config ইনস্ট্যান্স ধারণ করবে এবং এরর ক্ষেত্রে সমস্যাটি বর্ণনা করবে। আমরা ফাংশনের নাম new থেকে build-এ পরিবর্তন করতে যাচ্ছি কারণ অনেক প্রোগ্রামার আশা করেন যে new ফাংশনগুলো কখনই ব্যর্থ হবে না। যখন Config::build main-এর সাথে যোগাযোগ করছে, আমরা Result টাইপ ব্যবহার করে সংকেত দিতে পারি যে একটি সমস্যা ছিল। তারপরে আমরা main-কে একটি Err ভ্যারিয়েন্টকে আমাদের ব্যবহারকারীদের জন্য আরও ব্যবহারিক এররে রূপান্তর করতে পরিবর্তন করতে পারি, thread 'main' এবং RUST_BACKTRACE সম্পর্কিত পার্শ্ববর্তী টেক্সট ছাড়াই যা panic! কল করার কারণে ঘটে।

লিস্টিং ১২-৯ দেখাচ্ছে যে ফাংশনের রিটার্ন ভ্যালুতে আমাদের কী কী পরিবর্তন করতে হবে, যাকে আমরা এখন Config::build বলছি, এবং ফাংশনের বডিতে Result রিটার্ন করার জন্য কী প্রয়োজন। মনে রাখবেন যে এটি main আপডেট না করা পর্যন্ত কম্পাইল হবে না, যা আমরা পরবর্তী লিস্টিং-এ করব।

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

আমাদের build ফাংশন সফল ক্ষেত্রে একটি Config ইনস্ট্যান্স সহ একটি Result এবং এরর ক্ষেত্রে একটি স্ট্রিং লিটারেল রিটার্ন করে। আমাদের এরর ভ্যালুগুলো সবসময় স্ট্রিং লিটারেল হবে যার 'static লাইফটাইম আছে।

আমরা ফাংশনের বডিতে দুটি পরিবর্তন করেছি: ব্যবহারকারী পর্যাপ্ত আর্গুমেন্ট পাস না করলে panic! কল করার পরিবর্তে, আমরা এখন একটি Err ভ্যালু রিটার্ন করি, এবং আমরা Config রিটার্ন ভ্যালুটিকে একটি Ok-এর মধ্যে র‍্যাপ করেছি। এই পরিবর্তনগুলো ফাংশনটিকে তার নতুন টাইপ সিগনেচারের সাথে সঙ্গতিপূর্ণ করে তোলে।

Config::build থেকে একটি Err ভ্যালু রিটার্ন করা main ফাংশনকে build ফাংশন থেকে রিটার্ন করা Result ভ্যালুটি হ্যান্ডেল করতে এবং এরর ক্ষেত্রে প্রসেসটি আরও পরিষ্কারভাবে প্রস্থান করতে দেয়।

Config::build কল করা এবং এরর হ্যান্ডেল করা

এরর কেসটি হ্যান্ডেল করতে এবং একটি ব্যবহারকারী-বান্ধব মেসেজ প্রিন্ট করতে, আমাদের Config::build দ্বারা রিটার্ন করা Result-কে হ্যান্ডেল করার জন্য main-কে আপডেট করতে হবে, যেমনটি লিস্টিং ১২-১০-এ দেখানো হয়েছে। আমরা একটি নন-জিরো এরর কোড দিয়ে কমান্ড লাইন টুল থেকে প্রস্থান করার দায়িত্বটি panic! থেকে সরিয়ে নেব এবং পরিবর্তে এটি হাতে-কলমে বাস্তবায়ন করব। একটি নন-জিরো এক্সিট স্ট্যাটাস হলো আমাদের প্রোগ্রাম কল করা প্রসেসকে সংকেত দেওয়ার একটি কনভেনশন যে প্রোগ্রামটি একটি এরর স্টেট দিয়ে প্রস্থান করেছে।

use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

এই লিস্টিং-এ, আমরা এমন একটি মেথড ব্যবহার করেছি যা আমরা এখনও বিস্তারিতভাবে কভার করিনি: unwrap_or_else, যা standard library দ্বারা Result<T, E>-এর উপর ডিফাইন করা হয়েছে। unwrap_or_else ব্যবহার করে আমরা কিছু কাস্টম, নন-panic! এরর হ্যান্ডলিং ডিফাইন করতে পারি। যদি Result একটি Ok ভ্যালু হয়, এই মেথডের আচরণ unwrap-এর মতোই: এটি Ok-এর মধ্যে থাকা অভ্যন্তরীণ ভ্যালুটি রিটার্ন করে। যাইহোক, যদি ভ্যালুটি একটি Err ভ্যালু হয়, এই মেথডটি ক্লোজার (closure)-এর কোড কল করে, যা একটি অ্যানোনিমাস ফাংশন যা আমরা ডিফাইন করি এবং unwrap_or_else-এর আর্গুমেন্ট হিসেবে পাস করি। আমরা অধ্যায় ১৩-তে ক্লোজার সম্পর্কে আরও বিস্তারিতভাবে আলোচনা করব। আপাতত, আপনাকে শুধু জানতে হবে যে unwrap_or_else Err-এর অভ্যন্তরীণ ভ্যালুটি, যা এই ক্ষেত্রে লিস্টিং ১২-৯-এ যোগ করা স্ট্যাটিক স্ট্রিং "not enough arguments", আমাদের ক্লোজারে ভার্টিকাল পাইপের মধ্যে থাকা err আর্গুমেন্টে পাস করবে। ক্লোজারের কোডটি তখন চলার সময় err ভ্যালুটি ব্যবহার করতে পারে।

আমরা standard library থেকে process স্কোপে আনার জন্য একটি নতুন use লাইন যোগ করেছি। এরর ক্ষেত্রে যে ক্লোজারটি চালানো হবে তার কোডটি মাত্র দুই লাইনের: আমরা err ভ্যালুটি প্রিন্ট করি এবং তারপর process::exit কল করি। process::exit ফাংশনটি প্রোগ্রামটি অবিলম্বে বন্ধ করে দেবে এবং এক্সিট স্ট্যাটাস কোড হিসেবে পাস করা নম্বরটি রিটার্ন করবে। এটি লিস্টিং ১২-৮-এ আমরা ব্যবহৃত panic!-ভিত্তিক হ্যান্ডলিংয়ের মতোই, কিন্তু আমরা আর সমস্ত অতিরিক্ত আউটপুট পাই না। চলুন এটি চেষ্টা করি:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

চমৎকার! এই আউটপুটটি আমাদের ব্যবহারকারীদের জন্য অনেক বেশি বন্ধুত্বপূর্ণ।

main ফাংশন থেকে লজিক এক্সট্র্যাক্ট করা

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

লিস্টিং ১২-১১ একটি run ফাংশন এক্সট্র্যাক্ট করার ছোট, ক্রমবর্ধমান উন্নতি দেখাচ্ছে।

use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

run ফাংশনটি এখন ফাইল পড়া থেকে শুরু করে main থেকে বাকি সমস্ত লজিক ধারণ করে। run ফাংশনটি Config ইনস্ট্যান্সটিকে একটি আর্গুমেন্ট হিসেবে নেয়।

run ফাংশন থেকে এরর রিটার্ন করা

বাকি প্রোগ্রাম লজিক run ফাংশনে পৃথক করার সাথে সাথে, আমরা এরর হ্যান্ডলিং উন্নত করতে পারি, যেমনটি আমরা লিস্টিং ১২-৯-এ Config::build-এর সাথে করেছিলাম। expect কল করে প্রোগ্রামকে প্যানিক করার অনুমতি দেওয়ার পরিবর্তে, run ফাংশনটি কিছু ভুল হলে একটি Result<T, E> রিটার্ন করবে। এটি আমাদের এরর হ্যান্ডলিং সম্পর্কিত লজিককে main-এ আরও ব্যবহারকারী-বান্ধব উপায়ে একত্রিত করতে দেবে। লিস্টিং ১২-১২ দেখাচ্ছে যে run-এর সিগনেচার এবং বডিতে আমাদের কী কী পরিবর্তন করতে হবে।

use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

আমরা এখানে তিনটি উল্লেখযোগ্য পরিবর্তন করেছি। প্রথমত, আমরা run ফাংশনের রিটার্ন টাইপ পরিবর্তন করে Result<(), Box<dyn Error>> করেছি। এই ফাংশনটি আগে ইউনিট টাইপ, () রিটার্ন করত, এবং আমরা এটিকে Ok ক্ষেত্রে রিটার্ন করা ভ্যালু হিসেবে রাখি।

এরর টাইপের জন্য, আমরা ট্রেইট অবজেক্ট Box<dyn Error> ব্যবহার করেছি (এবং আমরা উপরে একটি use স্টেটমেন্ট দিয়ে std::error::Error-কে স্কোপে নিয়ে এসেছি)। আমরা অধ্যায় ১৮-তে ট্রেইট অবজেক্ট নিয়ে আলোচনা করব। আপাতত, শুধু জেনে রাখুন যে Box<dyn Error> মানে ফাংশনটি এমন একটি টাইপ রিটার্ন করবে যা Error ট্রেইট ইমপ্লিমেন্ট করে, কিন্তু আমাদের নির্দিষ্ট করতে হবে না যে রিটার্ন ভ্যালুটি কোন নির্দিষ্ট টাইপের হবে। এটি আমাদের বিভিন্ন এরর ক্ষেত্রে বিভিন্ন টাইপের এরর ভ্যালু রিটার্ন করার নমনীয়তা দেয়। dyn কীওয়ার্ডটি ডাইনামিক (dynamic)-এর সংক্ষিপ্ত রূপ।

দ্বিতীয়ত, আমরা expect কলটি সরিয়ে ? অপারেটরের পক্ষে নিয়েছি, যেমনটি আমরা অধ্যায় ৯-এ আলোচনা করেছি। একটি এররে panic! করার পরিবর্তে, ? বর্তমান ফাংশন থেকে এরর ভ্যালুটি কলারের কাছে হ্যান্ডেল করার জন্য রিটার্ন করবে।

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

আপনি যখন এই কোডটি চালাবেন, এটি কম্পাইল হবে কিন্তু একটি সতর্কতা প্রদর্শন করবে:

$ cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
19 |     let _ = run(config);
   |     +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust আমাদের বলছে যে আমাদের কোড Result ভ্যালুটিকে উপেক্ষা করেছে এবং Result ভ্যালুটি নির্দেশ করতে পারে যে একটি এরর ঘটেছে। কিন্তু আমরা পরীক্ষা করছি না যে কোনো এরর ছিল কি না, এবং কম্পাইলার আমাদের মনে করিয়ে দেয় যে আমরা সম্ভবত এখানে কিছু এরর-হ্যান্ডলিং কোড রাখতে চেয়েছিলাম! চলুন এখন সেই সমস্যাটি সমাধান করি।

main-এ run থেকে রিটার্ন করা এরর হ্যান্ডেল করা

আমরা এরর পরীক্ষা করব এবং লিস্টিং ১২-১০-এ Config::build-এর সাথে ব্যবহৃত কৌশলের মতো একটি কৌশল ব্যবহার করে সেগুলো হ্যান্ডেল করব, কিন্তু সামান্য পার্থক্য সহ:

ফাইলের নাম: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

আমরা unwrap_or_else-এর পরিবর্তে if let ব্যবহার করি run একটি Err ভ্যালু রিটার্ন করেছে কিনা তা পরীক্ষা করতে এবং যদি করে তবে process::exit(1) কল করতে। run ফাংশনটি এমন কোনো ভ্যালু রিটার্ন করে না যা আমরা unwrap করতে চাই, যেভাবে Config::build Config ইনস্ট্যান্স রিটার্ন করে। যেহেতু run সফল ক্ষেত্রে () রিটার্ন করে, আমরা শুধুমাত্র একটি এরর সনাক্ত করতে আগ্রহী, তাই আমাদের unwrap_or_else-এর প্রয়োজন নেই আনর‍্যাপ করা ভ্যালু রিটার্ন করার জন্য, যা শুধুমাত্র () হবে।

if let এবং unwrap_or_else ফাংশনের বডি উভয় ক্ষেত্রেই একই: আমরা এরর প্রিন্ট করি এবং প্রস্থান করি।

কোডকে একটি লাইব্রেরি ক্রেটে বিভক্ত করা

আমাদের minigrep প্রজেক্টটি এখন পর্যন্ত বেশ ভালো দেখাচ্ছে! এখন আমরা src/main.rs ফাইলটি বিভক্ত করব এবং কিছু কোড src/lib.rs ফাইলে রাখব। এইভাবে, আমরা কোডটি টেস্ট করতে পারব এবং একটি src/main.rs ফাইল রাখতে পারব যার দায়িত্ব কম।

চলুন টেক্সট সার্চ করার জন্য দায়ী কোডটি src/main.rs-এর পরিবর্তে src/lib.rs-এ ডিফাইন করি, যা আমাদের (বা আমাদের minigrep লাইব্রেরি ব্যবহারকারী অন্য যে কাউকে) আমাদের minigrep বাইনারি ছাড়াও আরও অনেক কনটেক্সট থেকে সার্চিং ফাংশনটি কল করতে দেবে।

প্রথমে, চলুন src/lib.rs-এ search ফাংশনের সিগনেচার ডিফাইন করি যেমনটি লিস্টিং ১২-১৩-এ দেখানো হয়েছে, যার বডিতে unimplemented! ম্যাক্রো কল করা হয়েছে। আমরা ইমপ্লিমেন্টেশন পূরণ করার সময় সিগনেচারটি আরও বিস্তারিতভাবে ব্যাখ্যা করব।

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}

আমরা search-কে আমাদের লাইব্রেরি ক্রেটের পাবলিক API-এর অংশ হিসেবে চিহ্নিত করার জন্য ফাংশন ডেফিনিশনে pub কীওয়ার্ড ব্যবহার করেছি। আমাদের এখন একটি লাইব্রেরি ক্রেট আছে যা আমরা আমাদের বাইনারি ক্রেট থেকে ব্যবহার করতে পারি এবং যা আমরা টেস্ট করতে পারি!

এখন আমাদের src/lib.rs-এ ডিফাইন করা কোডটিকে src/main.rs-এর বাইনারি ক্রেটের স্কোপে আনতে হবে এবং এটিকে কল করতে হবে, যেমনটি লিস্টিং ১২-১৪-এ দেখানো হয়েছে।

use std::env;
use std::error::Error;
use std::fs;
use std::process;

// --snip--
use minigrep::search;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

// --snip--


struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

আমরা লাইব্রেরি ক্রেট থেকে search ফাংশনটিকে বাইনারি ক্রেটের স্কোপে আনার জন্য একটি use minigrep::search লাইন যোগ করি। তারপর, run ফাংশনে, ফাইলের বিষয়বস্তু প্রিন্ট করার পরিবর্তে, আমরা search ফাংশনটি কল করি এবং config.query ভ্যালু এবং contents আর্গুমেন্ট হিসেবে পাস করি। তারপর run একটি for লুপ ব্যবহার করে search থেকে রিটার্ন করা প্রতিটি লাইন যা কোয়েরির সাথে মিলেছে তা প্রিন্ট করবে। এটি main ফাংশনে থাকা println! কলগুলো যা কোয়েরি এবং ফাইল পাথ প্রদর্শন করত তা সরিয়ে ফেলারও একটি ভালো সময়, যাতে আমাদের প্রোগ্রাম শুধুমাত্র সার্চ ফলাফল প্রিন্ট করে (যদি কোনো এরর না ঘটে)।

মনে রাখবেন যে search ফাংশনটি কোনো প্রিন্টিং হওয়ার আগে সমস্ত ফলাফল একটি ভেক্টরে সংগ্রহ করে রিটার্ন করবে। বড় ফাইল সার্চ করার সময় ফলাফল প্রদর্শন করতে এই ইমপ্লিমেন্টেশনটি ধীর হতে পারে কারণ ফলাফলগুলো খুঁজে পাওয়ার সাথে সাথে প্রিন্ট হয় না; আমরা অধ্যায় ১৩-এ ইটারেটর ব্যবহার করে এটি ঠিক করার একটি সম্ভাব্য উপায় নিয়ে আলোচনা করব।

অনেক কাজ হয়ে গেল! কিন্তু আমরা ভবিষ্যতের সাফল্যের জন্য নিজেদের প্রস্তুত করেছি। এখন এরর হ্যান্ডেল করা অনেক সহজ, এবং আমরা কোডকে আরও মডুলার করেছি। এখন থেকে আমাদের প্রায় সমস্ত কাজ src/lib.rs-এ করা হবে।

চলুন এই নতুন মডুলারিটির সুবিধা নিয়ে এমন কিছু করি যা পুরোনো কোড দিয়ে করা কঠিন ছিল কিন্তু নতুন কোড দিয়ে সহজ: আমরা কিছু টেস্ট লিখব!