মডুলারিটি এবং এরর হ্যান্ডলিং উন্নত করার জন্য রিফ্যাক্টরিং (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-এ করা হবে।
চলুন এই নতুন মডুলারিটির সুবিধা নিয়ে এমন কিছু করি যা পুরোনো কোড দিয়ে করা কঠিন ছিল কিন্তু নতুন কোড দিয়ে সহজ: আমরা কিছু টেস্ট লিখব!