Modularity এবং Error Handling উন্নত করার জন্য Refactoring
আমাদের প্রোগ্রামটিকে উন্নত করার জন্য, আমরা চারটি সমস্যা সমাধান করব যেগুলির প্রোগ্রামের structure এবং এটি কীভাবে potential error গুলি handle করছে তার সাথে সম্পর্ক রয়েছে। প্রথমত, আমাদের main
function এখন দুটি কাজ করে: এটি argument parse করে এবং file read করে। আমাদের প্রোগ্রাম যত বাড়বে, main
function-এর handle করা আলাদা কাজের সংখ্যাও বাড়বে। একটি function-এর responsibility যত বাড়ে, সেটি সম্পর্কে reasoning করা, test করা এবং সেটির কোনো একটি অংশ break না করে পরিবর্তন করা তত কঠিন হয়ে পড়ে। Functionality আলাদা করা সবচেয়ে ভালো যাতে প্রতিটি function একটি কাজের জন্য responsible হয়।
এই সমস্যাটি দ্বিতীয় সমস্যার সাথেও জড়িত: যদিও query
এবং file_path
আমাদের প্রোগ্রামের configuration variable, contents
-এর মতো variable গুলো প্রোগ্রামের logic perform করার জন্য ব্যবহৃত হয়। main
যত দীর্ঘ হবে, তত বেশি variable আমাদের scope-এ আনতে হবে; আমাদের scope-এ যত বেশি variable থাকবে, প্রতিটির purpose ট্র্যাক রাখা তত কঠিন হবে। Configuration variable গুলোকে একটি structure-এ group করা সবচেয়ে ভালো যাতে তাদের purpose স্পষ্ট হয়।
তৃতীয় সমস্যাটি হল, file read fail করলে আমরা একটি error message print করার জন্য expect
ব্যবহার করেছি, কিন্তু error message টি শুধুমাত্র Should have been able to read the file
প্রিন্ট করে। File read করার ক্ষেত্রে বিভিন্নভাবে fail হতে পারে: উদাহরণস্বরূপ, file টি missing থাকতে পারে, অথবা আমাদের এটি open করার permission নাও থাকতে পারে। এখন, পরিস্থিতি যাই হোক না কেন, আমরা সবকিছুর জন্য একই error message প্রিন্ট করব, যা user-কে কোনো information দেবে না!
চতুর্থত, আমরা একটি error handle করার জন্য expect
ব্যবহার করি, এবং যদি user পর্যাপ্ত argument specify না করে আমাদের প্রোগ্রাম run করে, তাহলে তারা Rust-এর কাছ থেকে একটি index out of bounds
error পাবে যা সমস্যাটি স্পষ্ট ভাবে ব্যাখ্যা করে না। Error-handling code-গুলো এক জায়গায় থাকলে সবচেয়ে ভালো হবে, যাতে future-এ maintainer-দের error-handling logic পরিবর্তন করার প্রয়োজন হলে code-এর শুধুমাত্র একটি জায়গাতেই consult করতে হয়। সমস্ত error-handling code এক জায়গায় থাকলে এটাও নিশ্চিত হবে যে আমরা এমন message প্রিন্ট করছি যা আমাদের end user-দের কাছে অর্থপূর্ণ হবে।
আসুন আমাদের project refactor করে এই চারটি সমস্যার সমাধান করি।
বাইনারি প্রোজেক্টের জন্য Separation of Concerns
main
function-এ একাধিক কাজের responsibility allocate করার সাংগঠনিক সমস্যাটি অনেক binary project-এর ক্ষেত্রে common। ফলস্বরূপ, Rust community একটি binary program-এর আলাদা concern গুলোকে split করার জন্য guidelines develop করেছে, যখন main
বড় হতে শুরু করে। এই process-টিতে নিম্নলিখিত step গুলো রয়েছে:
- আপনার প্রোগ্রামকে একটি main.rs file এবং একটি lib.rs file-এ split করুন এবং আপনার প্রোগ্রামের logic-কে lib.rs-এ move করুন।
- যতক্ষণ আপনার command line parsing logic ছোট থাকে, ততক্ষণ এটি main.rs-এ থাকতে পারে।
- যখন command line parsing logic জটিল হতে শুরু করে, তখন এটিকে main.rs থেকে extract করুন এবং lib.rs-এ move করুন।
এই process-এর পরে main
function-এ যে responsibility গুলো থাকা উচিত সেগুলো নিম্নলিখিতগুলির মধ্যে limited হওয়া উচিত:
- Argument value-গুলো দিয়ে command line parsing logic call করা
- অন্যান্য configuration set up করা
- lib.rs-এ একটি
run
function call করা - যদি
run
কোনো error return করে তাহলে error handle করা
এই pattern-টি concern গুলোকে আলাদা করার বিষয়ে: main.rs প্রোগ্রাম run করা handle করে এবং lib.rs বর্তমান task-এর সমস্ত logic handle করে। যেহেতু আপনি সরাসরি main
function test করতে পারবেন না, তাই এই structure আপনাকে lib.rs-এর function গুলোতে move করে আপনার প্রোগ্রামের সমস্ত logic test করতে দেয়। main.rs-এ যে code অবশিষ্ট থাকে তা এতটাই ছোট হবে যে এটি পড়ে এর সঠিকতা verify করা যাবে। আসুন এই process টি follow করে আমাদের প্রোগ্রামটিকে পুনরায় কাজ করি।
Argument Parser-কে Extract করা
আমরা argument parse করার functionality-টিকে একটি function-এ extract করব যাকে main
call করবে command line parsing logic-কে src/lib.rs-এ move করার জন্য প্রস্তুত করতে। Listing 12-5 main
-এর নতুন start দেখায় যা parse_config
নামক একটি নতুন function call করে, যেটি আমরা আপাতত src/main.rs-এ define করব।
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)
}
আমরা এখনও command line argument-গুলোকে একটি vector-এ collect করছি, কিন্তু main
function-এর মধ্যে index 1-এ থাকা argument value-টিকে query
variable-এ এবং index 2-এ থাকা argument value-টিকে file_path
variable-এ assign করার পরিবর্তে, আমরা পুরো vector-টিকে parse_config
function-এ pass করি। parse_config
function-টিতে এরপর সেই logic থাকে যা নির্ধারণ করে কোন argument কোন variable-এ যাবে এবং value-গুলো main
-এ ফেরত পাঠায়। আমরা এখনও main
-এ query
এবং file_path
variable create করি, কিন্তু command line argument এবং variable গুলো কীভাবে correspond করে তা নির্ধারণ করার responsibility আর main
-এর নেই।
আমাদের ছোট প্রোগ্রামের জন্য এই rework টি overkill মনে হতে পারে, কিন্তু আমরা ছোট, incremental step-এ refactor করছি। এই পরিবর্তনটি করার পরে, argument parsing এখনও কাজ করছে কিনা তা verify করার জন্য প্রোগ্রামটি আবার run করুন। আপনার progress প্রায়শই check করা ভালো, যাতে সমস্যা দেখা দিলে তার কারণ সনাক্ত করতে সুবিধা হয়।
Configuration Value গুলো Grouping করা
আমরা parse_config
function-টিকে আরও উন্নত করার জন্য আরেকটি ছোট step নিতে পারি। এখন, আমরা একটি tuple return করছি, কিন্তু তারপরে আমরা সেই tuple-টিকে আবার individual part-এ ভেঙে দিচ্ছি। এটি একটি লক্ষণ যে সম্ভবত আমাদের এখনও সঠিক abstraction নেই।
আরেকটি indicator যা দেখায় যে উন্নতির জায়গা রয়েছে তা হল parse_config
-এর config
অংশটি, যা বোঝায় যে আমরা যে দুটি value return করি সেগুলি related এবং উভয়ই একটি configuration value-এর অংশ। আমরা বর্তমানে data-র structure-এ এই অর্থটি প্রকাশ করছি না, শুধুমাত্র দুটি value-কে একটি tuple-এ group করে; এর পরিবর্তে আমরা দুটি value-কে একটি struct-এ রাখব এবং struct-এর প্রতিটি field-কে একটি অর্থপূর্ণ নাম দেব। এটি করলে future-এ এই code-এর maintainer-দের জন্য এটা বোঝা সহজ হবে যে কীভাবে বিভিন্ন value একে অপরের সাথে related এবং তাদের purpose কী।
Listing 12-6 parse_config
function-এর improvement গুলো দেখায়।
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 }
}
আমরা Config
নামের একটি struct যোগ করেছি যার field-গুলোর নাম query
এবং file_path
হিসেবে define করা হয়েছে। parse_config
-এর signature এখন নির্দেশ করে যে এটি একটি Config
value return করে। parse_config
-এর body-তে, যেখানে আমরা args
-এ String
value-গুলোকে reference করা string slice return করতাম, সেখানে এখন আমরা Config
-কে define করি owned String
value ধারণ করার জন্য। main
-এর args
variable-টি argument value-গুলোর owner এবং parse_config
function-কে শুধুমাত্র সেগুলো borrow করতে দিচ্ছে, যার অর্থ হল যদি Config
, args
-এর value-গুলোর ownership নেওয়ার চেষ্টা করত তাহলে আমরা Rust-এর borrowing rule violate করতাম।
আমরা String
data manage করার জন্য বেশ কয়েকটি উপায় অবলম্বন করতে পারি; সবচেয়ে সহজ, যদিও কিছুটা inefficient, উপায় হল value-গুলোর উপর clone
method call করা। এটি Config
instance-এর own করার জন্য data-র একটি full copy তৈরি করবে, যা string data-র একটি reference store করার চেয়ে বেশি সময় এবং memory নেয়। যাইহোক, data clone করা আমাদের code-কে আরও straightforward করে তোলে কারণ আমাদের reference-গুলোর lifetime manage করতে হয় না; এই পরিস্থিতিতে, simplicity অর্জনের জন্য performance-এর সামান্য ত্যাগ একটি worthwhile trade-off।
clone
ব্যবহারের Trade-Offঅনেক Rustaceans-দের মধ্যে ownership-এর সমস্যা সমাধানের জন্য
clone
ব্যবহার করা এড়িয়ে যাওয়ার প্রবণতা রয়েছে কারণ এর runtime cost আছে। Chapter 13-এ, আপনি শিখবেন কীভাবে এই ধরনের পরিস্থিতিতে আরও efficient method ব্যবহার করতে হয়। কিন্তু আপাতত, progress চালিয়ে যাওয়ার জন্য কয়েকটি string copy করা ঠিক আছে কারণ আপনি এই copy গুলো শুধুমাত্র একবার করবেন এবং আপনার file path এবং query string খুব ছোট। প্রথম চেষ্টাতেই code hyperoptimize করার চেষ্টা করার চেয়ে একটি working প্রোগ্রাম থাকা ভালো যা কিছুটা inefficient। আপনি Rust-এর সাথে আরও experienced হওয়ার সাথে সাথে, সবচেয়ে efficient solution দিয়ে শুরু করা আরও সহজ হবে, কিন্তু আপাতত,clone
call করা perfectly acceptable।
আমরা main
update করেছি যাতে এটি parse_config
দ্বারা returned Config
-এর instance-টিকে config
নামের একটি variable-এ রাখে, এবং আমরা সেই code update করেছি যেটি আগে আলাদা query
এবং file_path
variable ব্যবহার করত যাতে এটি এখন পরিবর্তে Config
struct-এর field গুলো ব্যবহার করে।
এখন আমাদের code আরও স্পষ্টভাবে প্রকাশ করে যে query
এবং file_path
related এবং তাদের purpose হল প্রোগ্রামটি কীভাবে কাজ করবে তা configure করা। এই value গুলো ব্যবহার করে এমন যেকোনো code জানে যে সেগুলিকে config
instance-এর মধ্যে তাদের purpose-এর জন্য named field-গুলোতে খুঁজতে হবে।
Config
-এর জন্য একটি Constructor তৈরি করা
এখনও পর্যন্ত, আমরা command line argument parse করার জন্য responsible logic-টিকে main
থেকে extract করে parse_config
function-এ রেখেছি। এটি করতে গিয়ে আমরা দেখতে পেলাম যে query
এবং file_path
value গুলো related ছিল, এবং সেই relationship আমাদের code-এ প্রকাশ করা উচিত। তারপরে আমরা query
এবং file_path
-এর related purpose-টির নাম দেওয়ার জন্য এবং parse_config
function থেকে value-গুলোর নাম struct field name হিসেবে return করতে সক্ষম হওয়ার জন্য একটি Config
struct যোগ করেছি।
সুতরাং এখন যেহেতু parse_config
function-টির purpose হল একটি Config
instance create করা, তাই আমরা parse_config
-কে একটি plain function থেকে Config
struct-এর সাথে associated new
নামের একটি function-এ পরিবর্তন করতে পারি। এই পরিবর্তনটি code-টিকে আরও idiomatic করে তুলবে। আমরা standard library-তে type-গুলোর instance create করতে পারি, যেমন String
, String::new
call করে। একইভাবে, parse_config
-কে Config
-এর সাথে associated একটি new
function-এ পরিবর্তন করে, আমরা Config::new
call করে Config
-এর instance create করতে পারব। Listing 12-7 আমাদের যে পরিবর্তনগুলো করতে হবে সেগুলো দেখায়।
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
update করেছি যেখানে আমরা parse_config
call করছিলাম, পরিবর্তে Config::new
call করার জন্য। আমরা parse_config
-এর নাম পরিবর্তন করে new
করেছি এবং এটিকে একটি impl
block-এর মধ্যে move করেছি, যা new
function-টিকে Config
-এর সাথে associate করে। এই code টি আবার compile করে দেখুন এটা নিশ্চিত করতে যে এটি কাজ করছে।
Error Handling ঠিক করা
এখন আমরা আমাদের error handling ঠিক করার জন্য কাজ করব। মনে রাখবেন যে vector-এ তিনটির কম item থাকলে args
vector-এর index 1 বা index 2-এ value access করার চেষ্টা করলে প্রোগ্রামটি panic করবে। কোনো argument ছাড়া প্রোগ্রামটি run করার চেষ্টা করুন; এটি এইরকম দেখাবে:
$ 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
লাইনটি programmers-দের জন্য উদ্দিষ্ট একটি error message। এটি আমাদের end user-দের বুঝতে সাহায্য করবে না যে পরিবর্তে তাদের কী করা উচিত। আসুন এখন সেটা ঠিক করি।
Error Message-এর উন্নতি
Listing 12-8-এ, আমরা new
function-এ একটি check যোগ করি যা index 1 এবং index 2 access করার আগে verify করবে যে slice-টি যথেষ্ট long কিনা। যদি slice-টি যথেষ্ট long না হয়, তাহলে প্রোগ্রামটি panic করে এবং একটি better error message প্রদর্শন করে।
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 }
}
}
এই code-টি Listing 9-13-এ লেখা আমাদের Guess::new
function-এর মতো, যেখানে value
argument-টি valid value-গুলোর range-এর বাইরে থাকলে আমরা panic!
call করেছিলাম। এখানে value-গুলোর একটি range check করার পরিবর্তে, আমরা check করছি যে args
-এর length কমপক্ষে 3
কিনা এবং function-এর বাকি অংশ এই assumption-এর অধীনে operate করতে পারে যে এই condition পূরণ হয়েছে। যদি args
-এ তিনটির কম item থাকে, তাহলে এই condition-টি true
হবে, এবং আমরা প্রোগ্রামটিকে immediately end করার জন্য panic!
macro call করি।
new
-তে এই কয়েকটি অতিরিক্ত code line সহ, আসুন আবার কোনো argument ছাড়াই প্রোগ্রামটি run করি এটা দেখতে যে error-টি এখন কেমন দেখায়:
$ 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
এই output টি আরও ভালো: আমাদের এখন একটি reasonable error message রয়েছে। যাইহোক, আমাদের কাছে extraneous information-ও রয়েছে যা আমরা আমাদের user-দের দিতে চাই না। সম্ভবত Listing 9-13-এ আমরা যে technique ব্যবহার করেছি সেটি এখানে ব্যবহার করার জন্য সেরা নয়: panic!
-এ call করা usage problem-এর চেয়ে programming problem-এর জন্য বেশি উপযুক্ত, যেমন Chapter 9-এ আলোচনা করা হয়েছে। পরিবর্তে, আমরা Chapter 9-এ শেখা অন্য technique টি ব্যবহার করব—একটি Result
return করা যা success বা error indicate করে।
panic!
Call করার পরিবর্তে একটি Result
Return করা
আমরা পরিবর্তে একটি Result
value return করতে পারি যাতে successful case-এ একটি Config
instance থাকবে এবং error case-এ problem টি describe করবে। আমরা function-টির নামও new
থেকে build
-এ পরিবর্তন করতে যাচ্ছি কারণ অনেক programmer আশা করেন new
function গুলো কখনই fail করবে না। যখন Config::build
, main
-এর সাথে communicate করছে, তখন আমরা Result
type ব্যবহার করে signal দিতে পারি যে একটি problem ছিল। তারপরে আমরা main
পরিবর্তন করে Err
variant-কে আমাদের user-দের জন্য আরও practical error-এ convert করতে পারি, thread 'main'
এবং RUST_BACKTRACE
সম্পর্কে আশেপাশের text ছাড়াই যা panic!
-এ call করার কারণে ঘটে।
Listing 12-9-এ আমরা এখন যে function টিকে Config::build
বলছি, তার return value এবং একটি Result
return করার জন্য function-এর body-তে যে পরিবর্তনগুলো করতে হবে সেগুলো দেখানো হলো। মনে রাখবেন যে যতক্ষণ না আমরা main
update করি, ততক্ষণ পর্যন্ত এটি compile হবে না, যা আমরা পরবর্তী listing-এ করব।
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
function success case-এ একটি Config
instance এবং error case-এ একটি string literal সহ একটি Result
return করে। আমাদের error value গুলো সব সময় string literal হবে যাদের 'static
lifetime আছে।
আমরা function-এর body-তে দুটি পরিবর্তন করেছি: user পর্যাপ্ত argument pass না করলে panic!
call করার পরিবর্তে, আমরা এখন একটি Err
value return করি, এবং আমরা Config
return value-টিকে একটি Ok
-এ wrap করেছি। এই পরিবর্তনগুলো function-টিকে এর new type signature-এর সাথে সঙ্গতিপূর্ণ করে তোলে।
Config::build
থেকে একটি Err
value return করা main
function-কে build
function থেকে returned Result
value handle করতে এবং error case-এ আরও cleanly process exit করতে দেয়।
Config::build
কল করা এবং Error হ্যান্ডেল করা
Error case হ্যান্ডেল করতে এবং একটি user-friendly message প্রিন্ট করতে, আমাদের main
update করতে হবে যাতে এটি Config::build
দ্বারা returned Result
হ্যান্ডেল করতে পারে, যেমনটি Listing 12-10-এ দেখানো হয়েছে। আমরা panic!
থেকে nonzero error code সহ command line tool exit করার responsibility-ও সরিয়ে নেব এবং পরিবর্তে এটি নিজে implement করব। একটি nonzero exit status হল এমন একটি convention যা আমাদের প্রোগ্রাম call করা process-কে signal দেয় যে প্রোগ্রামটি একটি error state-এর সাথে exit করেছে।
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 })
}
}
এই listing-এ, আমরা এমন একটি method ব্যবহার করেছি যা আমরা এখনও বিস্তারিতভাবে আলোচনা করিনি: unwrap_or_else
, যেটি standard library দ্বারা Result<T, E>
-তে define করা হয়েছে। unwrap_or_else
ব্যবহার করা আমাদের কিছু custom, non-panic!
error handling define করার সুযোগ দেয়। যদি Result
একটি Ok
value হয়, তাহলে এই method-টির behavior unwrap
-এর মতোই: এটি Ok
যে inner value-টিকে wrap করে রেখেছে সেটি return করে। যাইহোক, যদি value-টি একটি Err
value হয়, তাহলে এই method টি closure-এর মধ্যে থাকা code call করে, যেটি হল একটি anonymous function যা আমরা define করি এবং unwrap_or_else
-এ argument হিসেবে pass করি। আমরা Chapter 13-এ closure সম্পর্কে আরও বিস্তারিত আলোচনা করব। আপাতত, আপনার শুধু এটা জানলেই চলবে যে unwrap_or_else
, Err
-এর inner value, যেটি এই ক্ষেত্রে Listing 12-9-এ যোগ করা static string "not enough arguments"
, সেটিকে vertical pipe-গুলোর মধ্যে থাকা err
argument-এর মাধ্যমে আমাদের closure-এ pass করবে। Closure-এর ভেতরের code তারপর run করার সময় err
value-টিকে ব্যবহার করতে পারবে।
আমরা standard library থেকে process
কে scope-এ আনার জন্য একটি নতুন use
লাইন যোগ করেছি। Error case-এ যে closure-টি run করবে তার code-এ শুধুমাত্র দুটি লাইন রয়েছে: আমরা err
value-টি print করি এবং তারপর process::exit
call করি। process::exit
function প্রোগ্রামটিকে immediately stop করবে এবং exit status code হিসেবে যে number টি pass করা হয়েছিল সেটি return করবে। এটি Listing 12-8-এ ব্যবহৃত panic!
-ভিত্তিক handling-এর মতোই, কিন্তু আমরা আর সমস্ত extra output পাচ্ছি না। আসুন চেষ্টা করা যাক:
$ 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
দারুণ! এই output টি আমাদের user-দের জন্য অনেক বেশি friendly।
main
থেকে Logic Extract করা
এখন যেহেতু আমরা configuration parsing refactor করা শেষ করেছি, আসুন প্রোগ্রামের logic-এর দিকে মনোযোগ দিই। আমরা যেমন “বাইনারি প্রোজেক্টের জন্য Separation of Concerns”-এ উল্লেখ করেছি, আমরা run
নামের একটি function extract করব যেটি বর্তমানে main
function-এ থাকা সমস্ত logic ধারণ করবে যা configuration set up করা বা error handle করার সাথে জড়িত নয়। যখন আমাদের কাজ শেষ হবে, main
সংক্ষিপ্ত হবে এবং inspection-এর মাধ্যমে সহজেই verify করা যাবে, এবং আমরা অন্যান্য সমস্ত logic-এর জন্য test লিখতে পারব।
Listing 12-11-এ extract করা run
function দেখানো হয়েছে। আপাতত, আমরা শুধুমাত্র function extract করার ছোট, incremental improvement করছি। আমরা এখনও src/main.rs-এ function-টি define করছি।
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
function-এ এখন file read করা থেকে শুরু করে main
-এর সমস্ত অবশিষ্ট logic রয়েছে। run
function টি Config
instance-টিকে argument হিসেবে নেয়।
run
Function থেকে Error Return করা
প্রোগ্রামের অবশিষ্ট logic run
function-এ আলাদা করার সাথে, আমরা error handling-এর উন্নতি করতে পারি, যেমনটি আমরা Listing 12-9-এ Config::build
-এর সাথে করেছিলাম। expect
call করে প্রোগ্রামটিকে panic করার অনুমতি দেওয়ার পরিবর্তে, run
function টি কোনো কিছু ভুল হলে একটি Result<T, E>
return করবে। এটি আমাদেরকে user-friendly উপায়ে error handle করার জন্য logic-কে main
-এ আরও consolidate করার সুযোগ দেবে। Listing 12-12 run
-এর signature এবং body-তে আমাদের যে পরিবর্তনগুলো করতে হবে সেগুলো দেখায়।
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
function-এর return type পরিবর্তন করে Result<(), Box<dyn Error>>
করেছি। এই function টি আগে unit type, ()
, return করত, এবং আমরা Ok
case-এ returned value হিসেবে সেটিই রাখছি।
Error type-এর জন্য, আমরা trait object Box<dyn Error>
ব্যবহার করেছি (এবং আমরা উপরের দিকে একটি use
statement দিয়ে std::error::Error
কে scope-এ এনেছি)। আমরা Chapter 18-এ trait object নিয়ে আলোচনা করব। আপাতত, শুধু জেনে রাখুন যে Box<dyn Error>
মানে function টি এমন একটি type return করবে যা Error
trait implement করে, কিন্তু আমাদের specify করতে হবে না যে return value-টি specific কোন type-এর হবে। এটি আমাদেরকে error value return করার flexibility দেয় যা different error case-এ different type-এর হতে পারে। dyn
keyword টি dynamic-এর সংক্ষিপ্ত রূপ।
দ্বিতীয়ত, আমরা Chapter 9-এ আলোচনা করা ?
operator-এর পক্ষে expect
-এর call সরিয়ে দিয়েছি। কোনো error-এর উপর panic!
করার পরিবর্তে, ?
current function থেকে error value-টি return করবে যাতে caller সেটি handle করতে পারে।
তৃতীয়ত, run
function টি এখন success case-এ একটি Ok
value return করে। আমরা signature-এ run
function-এর success type ()
হিসেবে declare করেছি, যার অর্থ হল আমাদের unit type value-টিকে Ok
value-তে wrap করতে হবে। এই Ok(())
syntax টি প্রথমে একটু অদ্ভুত লাগতে পারে, কিন্তু এইভাবে ()
ব্যবহার করা হল idiomatic উপায় এটা indicate করার জন্য যে আমরা run
কে শুধুমাত্র এর side effect-গুলোর জন্য call করছি; এটি আমাদের প্রয়োজনীয় কোনো value return করে না।
আপনি যখন এই code run করবেন, এটি compile হবে কিন্তু একটি warning প্রদর্শন করবে:
$ 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 আমাদের বলছে যে আমাদের code Result
value-টিকে ignore করেছে এবং Result
value-টি indicate করতে পারে যে একটি error ঘটেছে। কিন্তু আমরা check করছি না যে কোনো error হয়েছে কিনা, এবং compiler আমাদের মনে করিয়ে দিচ্ছে যে সম্ভবত আমাদের এখানে কিছু error-handling code থাকা উচিত ছিল! আসুন এখনই সেই সমস্যাটি সংশোধন করি।
main
-এ run
থেকে Returned Error হ্যান্ডেল করা
আমরা error-গুলো check করব এবং Listing 12-10-এ Config::build
-এর সাথে ব্যবহৃত পদ্ধতির মতোই একটি technique ব্যবহার করে সেগুলি handle করব, তবে সামান্য ভিন্নতা সহ:
Filename: 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
ব্যবহার করি এটা check করার জন্য যে run
একটি Err
value return করে কিনা এবং যদি করে তবে process::exit(1)
call করার জন্য। run
function এমন কোনো value return করে না যা আমরা unwrap
করতে চাই, যেমন Config::build
, Config
instance return করে। যেহেতু run
success case-এ ()
return করে, তাই আমরা শুধুমাত্র একটি error detect করার বিষয়ে আগ্রহী, তাই unwrapped value return করার জন্য আমাদের unwrap_or_else
-এর প্রয়োজন নেই, যেটি শুধুমাত্র ()
হবে।
if let
এবং unwrap_or_else
function-গুলোর body উভয় ক্ষেত্রেই একই: আমরা error print করি এবং exit করি।
কোডকে একটি Library Crate-এ Split করা
আমাদের minigrep
project-টি এখনও পর্যন্ত ভালো দেখাচ্ছে! এখন আমরা src/main.rs file-টিকে split করব এবং কিছু code src/lib.rs file-এ রাখব। এইভাবে, আমরা code test করতে পারব এবং src/main.rs file-টিতে responsibility কম রাখতে পারব।
আসুন src/main.rs থেকে src/lib.rs-এ সমস্ত code সরিয়ে নিই যা main
function-এ নেই:
run
function definition- Relevant
use
statement-গুলো Config
-এর definitionConfig::build
function definition
src/lib.rs-এর contents-এ Listing 12-13-এ দেখানো signature গুলো থাকা উচিত (সংक्षिप्तতার জন্য আমরা function-গুলোর body বাদ দিয়েছি)। মনে রাখবেন যে যতক্ষণ না আমরা Listing 12-14-এ src/main.rs modify করি, ততক্ষণ পর্যন্ত এটি compile হবে না।
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
// --snip--
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 })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// --snip--
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
আমরা pub
keyword-টির উদার ব্যবহার করেছি: Config
-এ, এর field এবং এর build
method-এ এবং run
function-এ। আমাদের এখন একটি library crate রয়েছে যার একটি public API রয়েছে যা আমরা test করতে পারি!
এখন আমাদের Listing 12-14-এ দেখানো code-টিকে src/lib.rs-এ সরানো code binary crate-এর scope-এ src/main.rs-এ আনতে হবে।
use std::env;
use std::process;
use minigrep::Config;
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) = minigrep::run(config) {
// --snip--
println!("Application error: {e}");
process::exit(1);
}
}
আমরা library crate থেকে binary crate-এর scope-এ Config
type-টি আনার জন্য একটি use minigrep::Config
লাইন যোগ করি এবং আমরা run
function-টির আগে আমাদের crate-এর নাম prefix করি। এখন সমস্ত functionality সংযুক্ত হওয়া উচিত এবং কাজ করা উচিত। cargo run
দিয়ে প্রোগ্রামটি run করুন এবং নিশ্চিত করুন যে সবকিছু সঠিকভাবে কাজ করছে।
উফ! এটা অনেক কাজ ছিল, কিন্তু আমরা ভবিষ্যতে নিজেদের সাফল্যের জন্য প্রস্তুত করেছি। এখন error handle করা অনেক সহজ, এবং আমরা code-টিকে আরও modular করেছি। এখন থেকে আমাদের প্রায় সমস্ত কাজ src/lib.rs-এ করা হবে।
আসুন এই নতুন পাওয়া modularity-র সুবিধা নিই এমন কিছু করে যা পুরানো code-এর সাথে করা কঠিন হত কিন্তু নতুন code-এর সাথে সহজ: আমরা কিছু test লিখব!