একটি সংখ্যা অনুমানের গেম প্রোগ্রামিং (Programming a Guessing Game)
আসুন, একসাথে একটি হ্যান্ডস-অন প্রোজেক্টে কাজ করার মাধ্যমে Rust-এর জগতে প্রবেশ করি! এই চ্যাপ্টারটি আপনাকে কিছু সাধারণ Rust কনসেপ্টের সাথে পরিচয় করিয়ে দেবে, একটি বাস্তব প্রোগ্রামে সেগুলো কীভাবে ব্যবহার করা হয় তা দেখিয়ে। আপনি let
, match
, মেথড, অ্যাসোসিয়েটেড ফাংশন, এক্সটার্নাল ক্রেট এবং আরও অনেক কিছু সম্পর্কে জানতে পারবেন! নিচের চ্যাপ্টারগুলোতে, আমরা এই ধারণাগুলো আরও বিস্তারিতভাবে আলোচনা করব। এই চ্যাপ্টারে, আপনি শুধুমাত্র মৌলিক বিষয়গুলো অনুশীলন করবেন।
আমরা একটি ক্লাসিক বিগিনার প্রোগ্রামিং সমস্যা সমাধান করব: একটি সংখ্যা অনুমানের গেম। এটি কীভাবে কাজ করে তা নিচে বলা হলো: প্রোগ্রামটি 1 থেকে 100-এর মধ্যে একটি র্যান্ডম সংখ্যা তৈরি করবে। তারপর এটি প্লেয়ারকে একটি সংখ্যা অনুমান করতে বলবে। একটি সংখ্যা অনুমান করার পরে, প্রোগ্রামটি জানাবে যে অনুমানটি খুব কম না বেশি হয়েছে। যদি অনুমানটি সঠিক হয়, তাহলে গেমটি একটি অভিনন্দন বার্তা প্রিন্ট করবে এবং শেষ হয়ে যাবে।
একটি নতুন প্রোজেক্ট সেট আপ করা (Setting Up a New Project)
একটি নতুন প্রোজেক্ট সেট আপ করতে, চ্যাপ্টার ১-এ তৈরি করা projects ডিরেক্টরিতে যান এবং Cargo ব্যবহার করে নিচের মতো একটি নতুন প্রোজেক্ট তৈরি করুন:
$ cargo new guessing_game
$ cd guessing_game
প্রথম কমান্ড, cargo new
, প্রোজেক্টের নাম (guessing_game
) প্রথম আর্গুমেন্ট হিসেবে নেয়। দ্বিতীয় কমান্ডটি নতুন প্রোজেক্টের ডিরেক্টরিতে যাওয়া নির্দেশ করে।
জেনারেট হওয়া Cargo.toml ফাইলটি দেখুন:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
আপনি যেমন চ্যাপ্টার ১-এ দেখেছেন, cargo new
আপনার জন্য একটি “Hello, world!” প্রোগ্রাম তৈরি করে। src/main.rs ফাইলটি দেখুন:
Filename: src/main.rs
fn main() { println!("Hello, world!"); }
এবার চলুন, এই “Hello, world!” প্রোগ্রামটি কম্পাইল করি এবং cargo run
কমান্ড ব্যবহার করে একই ধাপে চালাই:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `file:///projects/guessing_game/target/debug/guessing_game`
Hello, world!
run
কমান্ডটি তখন কাজে আসে যখন আপনাকে একটি প্রোজেক্টে দ্রুত পুনরাবৃত্তি করতে হয়, যেমনটি আমরা এই গেমে করব। অর্থাৎ, পরবর্তী ধাপে যাওয়ার আগে প্রতিটি পুনরাবৃত্তি দ্রুত পরীক্ষা করে নেওয়া যাবে।
src/main.rs ফাইলটি আবার খুলুন। আপনি এই ফাইলেই সমস্ত কোড লিখবেন।
একটি অনুমান প্রক্রিয়া করা (Processing a Guess)
অনুমান করার গেম প্রোগ্রামটির প্রথম অংশ ব্যবহারকারীর ইনপুট চাইবে, সেই ইনপুটটি প্রক্রিয়া করবে এবং ইনপুটটি প্রত্যাশিত ফর্ম্যাটে আছে কিনা তা পরীক্ষা করবে। শুরু করার জন্য, আমরা প্লেয়ারকে একটি সংখ্যা অনুমান করতে দেব। Listing 2-1-এর কোডটি src/main.rs-এ লিখুন।
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এই কোডটিতে অনেক তথ্য রয়েছে, তাই চলুন লাইন ধরে ধরে আলোচনা করি। ব্যবহারকারীর ইনপুট নিতে এবং তারপর ফলাফলটি আউটপুট হিসেবে প্রিন্ট করতে, আমাদের io
ইনপুট/আউটপুট লাইব্রেরিটিকে স্কোপের মধ্যে আনতে হবে। io
লাইব্রেরিটি স্ট্যান্ডার্ড লাইব্রেরি থেকে আসে, যাকে std
বলা হয়:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
ডিফল্টরূপে, Rust-এর স্ট্যান্ডার্ড লাইব্রেরিতে সংজ্ঞায়িত আইটেমগুলোর একটি সেট রয়েছে, যা প্রতিটি প্রোগ্রামের স্কোপে আনা হয়। এই সেটটিকে প্রেলিউড (prelude) বলা হয়, এবং আপনি স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশনে prelude-এর সবকিছু দেখতে পারেন।
যদি আপনি যে টাইপটি ব্যবহার করতে চান সেটি প্রেলিউডে না থাকে, তাহলে আপনাকে use
স্টেটমেন্ট ব্যবহার করে সেই টাইপটিকে স্পষ্টতই স্কোপে আনতে হবে। std::io
লাইব্রেরি ব্যবহার করলে আপনি বেশ কিছু দরকারী ফিচার পাবেন, যার মধ্যে ব্যবহারকারীর ইনপুট নেওয়ার ক্ষমতাও রয়েছে।
আপনি যেমন চ্যাপ্টার ১-এ দেখেছেন, main
ফাংশনটি প্রোগ্রামে প্রবেশের পথ:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
fn
সিনট্যাক্স একটি নতুন ফাংশন ঘোষণা করে; বন্ধনী, ()
, নির্দেশ করে যে কোনও প্যারামিটার নেই; এবং কার্লি ব্র্যাকেট, {}
, ফাংশনের বডি শুরু করে।
আপনি চ্যাপ্টার ১-এ আরও শিখেছেন যে, println!
হল একটি ম্যাক্রো যা স্ক্রিনে একটি স্ট্রিং প্রিন্ট করে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এই কোডটি একটি প্রম্পট প্রিন্ট করছে, যাতে গেমটি কী তা বলা হয়েছে এবং ব্যবহারকারীর কাছ থেকে ইনপুট চাওয়া হয়েছে।
ভেরিয়েবল ব্যবহার করে মান সংরক্ষণ করা (Storing Values with Variables)
এরপর, আমরা ব্যবহারকারীর ইনপুট সংরক্ষণ করার জন্য একটি ভেরিয়েবল তৈরি করব, এইভাবে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এবার প্রোগ্রামটি আরও মজাদার হচ্ছে! এই ছোট লাইনে অনেক কিছু ঘটছে। আমরা ভেরিয়েবল তৈরি করতে let
স্টেটমেন্ট ব্যবহার করি। আরেকটি উদাহরণ নিচে দেওয়া হলো:
let apples = 5;
এই লাইনটি apples
নামের একটি নতুন ভেরিয়েবল তৈরি করে এবং এটিকে 5 মানের সাথে বাইন্ড করে। Rust-এ, ভেরিয়েবলগুলো ডিফল্টরূপে ইমিউটেবল (immutable) হয়, অর্থাৎ একবার আমরা ভেরিয়েবলকে একটি মান দিলে, সেই মানটি পরিবর্তন হবে না। আমরা চ্যাপ্টার ৩-এর “ভেরিয়েবল এবং মিউটেবিলিটি” বিভাগে এই ধারণাটি নিয়ে বিস্তারিত আলোচনা করব। একটি ভেরিয়েবলকে মিউটেবল (mutable) করতে, আমরা ভেরিয়েবলের নামের আগে mut
যোগ করি:
let apples = 5; // immutable
let mut bananas = 5; // mutable
দ্রষ্টব্য:
//
সিনট্যাক্স একটি কমেন্ট শুরু করে, যা লাইনের শেষ পর্যন্ত চলতে থাকে। Rust কমেন্টের ভেতরের সবকিছু উপেক্ষা করে। আমরা চ্যাপ্টার ৩-এ কমেন্ট নিয়ে আরও বিস্তারিত আলোচনা করব।
অনুমানের গেমের প্রোগ্রামে ফিরে আসা যাক। আপনি এখন জানেন যে let mut guess
guess
নামের একটি মিউটেবল ভেরিয়েবল তৈরি করবে। সমান চিহ্ন (=
) Rust-কে বলে যে আমরা এখনই ভেরিয়েবলের সাথে কিছু বাইন্ড করতে চাই। সমান চিহ্নের ডানদিকে guess
-এর মান রয়েছে, যেটি String::new
কল করার ফলাফল। String::new
একটি ফাংশন, যা একটি String
-এর নতুন ইন্সট্যান্স রিটার্ন করে। String
হল স্ট্যান্ডার্ড লাইব্রেরি থেকে পাওয়া একটি স্ট্রিং টাইপ, যা প্রসারণযোগ্য (growable), UTF-8 এনকোডেড টেক্সট।
::new
লাইনের ::
সিনট্যাক্স নির্দেশ করে যে new
হল String
টাইপের একটি অ্যাসোসিয়েটেড ফাংশন। একটি অ্যাসোসিয়েটেড ফাংশন হল এমন একটি ফাংশন যা একটি টাইপের উপর ইমপ্লিমেন্ট করা হয়, এক্ষেত্রে String
। এই new
ফাংশনটি একটি নতুন, খালি স্ট্রিং তৈরি করে। আপনি অনেক টাইপের ওপর new
ফাংশন দেখতে পাবেন, কারণ এটি একটি সাধারণ নাম, যা কোনো কিছুর একটি নতুন মান তৈরি করে।
পুরো let mut guess = String::new();
লাইনটি একটি মিউটেবল ভেরিয়েবল তৈরি করেছে, যা বর্তমানে একটি String
-এর নতুন, খালি ইন্সট্যান্সের সাথে বাইন্ড করা আছে।
ব্যবহারকারীর ইনপুট গ্রহণ করা (Receiving User Input)
মনে করে দেখুন, আমরা প্রোগ্রামের প্রথম লাইনে use std::io;
দিয়ে স্ট্যান্ডার্ড লাইব্রেরি থেকে ইনপুট/আউটপুট ফাংশনালিটি অন্তর্ভুক্ত করেছিলাম। এবার আমরা io
মডিউল থেকে stdin
ফাংশনটি কল করব, যা আমাদের ব্যবহারকারীর ইনপুট হ্যান্ডেল করতে দেবে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
যদি আমরা প্রোগ্রামের শুরুতে use std::io;
দিয়ে io
লাইব্রেরি ইম্পোর্ট না করতাম, তাহলেও আমরা এই ফাংশনটিকে std::io::stdin
লিখে ব্যবহার করতে পারতাম। stdin
ফাংশনটি std::io::Stdin
-এর একটি ইন্সট্যান্স রিটার্ন করে, যেটি আপনার টার্মিনালের স্ট্যান্ডার্ড ইনপুটের হ্যান্ডেলের প্রতিনিধিত্ব করে।
এরপর, .read_line(&mut guess)
লাইনটি ব্যবহারকারীর কাছ থেকে ইনপুট নেওয়ার জন্য স্ট্যান্ডার্ড ইনপুট হ্যান্ডেলে read_line
মেথড কল করে। আমরা read_line
-কে আর্গুমেন্ট হিসেবে &mut guess
পাস করছি, যাতে এটি ব্যবহারকারীর ইনপুট কোন স্ট্রিংয়ে সংরক্ষণ করবে তা বলতে পারে। read_line
-এর মূল কাজ হল ব্যবহারকারী স্ট্যান্ডার্ড ইনপুটে যা টাইপ করে, সেটি একটি স্ট্রিংয়ে যুক্ত করা (স্ট্রিংয়ের আগের কনটেন্ট মুছে না দিয়ে)। তাই আমরা সেই স্ট্রিংটিকে আর্গুমেন্ট হিসেবে পাস করি। স্ট্রিং আর্গুমেন্টটিকে অবশ্যই মিউটেবল হতে হবে, যাতে মেথডটি স্ট্রিংয়ের কনটেন্ট পরিবর্তন করতে পারে।
&
নির্দেশ করে যে এই আর্গুমেন্টটি একটি রেফারেন্স, যা আপনাকে আপনার কোডের একাধিক অংশকে মেমরিতে ডেটা একাধিকবার কপি না করেই ডেটার একটি অংশ অ্যাক্সেস করার সুবিধা দেয়। রেফারেন্স একটি জটিল ফিচার, এবং Rust-এর অন্যতম প্রধান সুবিধা হল রেফারেন্স ব্যবহার করা কতটা নিরাপদ এবং সহজ। এই প্রোগ্রামটি শেষ করার জন্য আপনাকে সেই সমস্ত বিবরণ জানার দরকার নেই। আপাতত, আপনার শুধু এটুকু জানলেই চলবে যে ভেরিয়েবলের মতো রেফারেন্সগুলোও ডিফল্টরূপে ইমিউটেবল হয়। তাই, এটিকে মিউটেবল করার জন্য আপনাকে &guess
-এর পরিবর্তে &mut guess
লিখতে হবে। (চ্যাপ্টার ৪ রেফারেন্স আরও বিশদভাবে ব্যাখ্যা করবে।)
Result
দিয়ে সম্ভাব্য ত্রুটি সামলানো (Handling Potential Failure with Result
)
আমরা এখনও এই লাইনেই কাজ করছি। আমরা এখন টেক্সটের তৃতীয় লাইন নিয়ে আলোচনা করছি, কিন্তু মনে রাখবেন যে এটি এখনও কোডের একটি একক লজিক্যাল লাইনের অংশ। পরের অংশটি হল এই মেথড:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
আমরা এই কোডটিকে এভাবে লিখতে পারতাম:
io::stdin().read_line(&mut guess).expect("Failed to read line");
কিন্তু, একটি লম্বা লাইন পড়া কঠিন, তাই এটিকে ভাগ করে নেওয়া ভালো। .method_name()
সিনট্যাক্স দিয়ে যখন আপনি একটি মেথড কল করেন, তখন প্রায়ই একটি নতুন লাইন এবং অন্যান্য হোয়াইটস্পেস যোগ করে লম্বা লাইনগুলোকে ভেঙে ফেলা বুদ্ধিমানের কাজ। এবার আলোচনা করা যাক এই লাইনটি কী করে।
আগে যেমন উল্লেখ করা হয়েছে, read_line
ব্যবহারকারী যা ইনপুট দেয়, সেটি আমাদের দেওয়া স্ট্রিংয়ে রাখে। কিন্তু এটি একটি Result
মানও রিটার্ন করে। Result
হল একটি গণনা (enumeration), যাকে প্রায়শই enum বলা হয়। এটি এমন একটি টাইপ, যা একাধিক সম্ভাব্য অবস্থার মধ্যে একটিতে থাকতে পারে। আমরা প্রতিটি সম্ভাব্য অবস্থাকে একটি ভেরিয়েন্ট বলি।
চ্যাপ্টার ৬-এ enum সম্পর্কে আরও বিস্তারিত আলোচনা করা হবে। এই Result
টাইপগুলোর উদ্দেশ্য হল এরর-হ্যান্ডলিং তথ্য এনকোড করা।
Result
-এর ভেরিয়েন্টগুলো হল Ok
এবং Err
। Ok
ভেরিয়েন্ট নির্দেশ করে যে অপারেশন সফল হয়েছে এবং এর মধ্যে সফলভাবে জেনারেট হওয়া মান রয়েছে। Err
ভেরিয়েন্ট মানে অপারেশন ব্যর্থ হয়েছে এবং এর মধ্যে অপারেশনটি কীভাবে বা কেন ব্যর্থ হয়েছে সে সম্পর্কে তথ্য রয়েছে।
যেকোনো টাইপের মানের মতোই Result
টাইপের মানগুলোরও নিজস্ব মেথড ডিফাইন করা থাকে। Result
-এর একটি ইন্সট্যান্সের একটি expect
মেথড রয়েছে, যাকে আপনি কল করতে পারেন। যদি Result
-এর এই ইন্সট্যান্সটি একটি Err
মান হয়, তাহলে expect
প্রোগ্রামটিকে ক্র্যাশ করাবে এবং আপনি expect
-এর আর্গুমেন্ট হিসেবে যে মেসেজটি পাস করেছেন সেটি প্রদর্শন করবে। যদি read_line
মেথড একটি Err
রিটার্ন করে, তাহলে সম্ভবত এটি অন্তর্নিহিত অপারেটিং সিস্টেম থেকে আসা কোনো এররের ফলাফল। যদি Result
-এর এই ইন্সট্যান্সটি একটি Ok
মান হয়, তাহলে expect
Ok
-এর মধ্যে থাকা রিটার্ন মানটি গ্রহণ করবে এবং শুধুমাত্র সেই মানটি আপনাকে রিটার্ন করবে, যাতে আপনি সেটি ব্যবহার করতে পারেন। এক্ষেত্রে, সেই মানটি হল ব্যবহারকারীর ইনপুটের বাইটের সংখ্যা।
আপনি যদি expect
কল না করেন, তাহলে প্রোগ্রামটি কম্পাইল হবে, কিন্তু আপনি একটি ওয়ার্নিং পাবেন:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= 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
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust সতর্ক করে যে আপনি read_line
থেকে রিটার্ন হওয়া Result
মানটি ব্যবহার করেননি, যা নির্দেশ করে যে প্রোগ্রামটি একটি সম্ভাব্য এরর হ্যান্ডেল করেনি।
ওয়ার্নিংটি দমন করার সঠিক উপায় হল আসলে এরর-হ্যান্ডলিং কোড লেখা। কিন্তু আমাদের ক্ষেত্রে, কোনো সমস্যা হলে আমরা শুধু এই প্রোগ্রামটিকে ক্র্যাশ করাতে চাই, তাই আমরা expect
ব্যবহার করতে পারি। আপনি চ্যাপ্টার ৯-এ এরর থেকে পুনরুদ্ধার সম্পর্কে জানতে পারবেন।
println!
প্লেসহোল্ডার দিয়ে মান প্রিন্ট করা (Printing Values with println!
Placeholders)
ক্লোজিং কার্লি ব্র্যাকেট ছাড়াও, এখনও পর্যন্ত কোডটিতে আলোচনা করার মতো আর একটি লাইন রয়েছে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এই লাইনটি সেই স্ট্রিংটিকে প্রিন্ট করে, যেখানে এখন ব্যবহারকারীর ইনপুট রয়েছে। {}
কার্লি ব্র্যাকেটের সেটটি হল একটি প্লেসহোল্ডার: {}
-কে কাঁকড়ার ছোট সাঁড়াশি হিসেবে ভাবতে পারেন, যা একটি মানকে ধরে রাখে। যখন একটি ভেরিয়েবলের মান প্রিন্ট করা হয়, তখন কার্লি ব্র্যাকেটের ভেতরে ভেরিয়েবলের নাম দেওয়া যেতে পারে। যখন একটি এক্সপ্রেশনের মূল্যায়ন করা ফলাফল প্রিন্ট করা হয়, তখন ফরম্যাট স্ট্রিংয়ে খালি কার্লি ব্র্যাকেট বসানো হয়। তারপর ফরম্যাট স্ট্রিংয়ের পরে, প্রতিটি খালি কার্লি ব্র্যাকেট প্লেসহোল্ডারে প্রিন্ট করার জন্য কমা দিয়ে আলাদা করা এক্সপ্রেশনের একটি তালিকা একই ক্রমে দেওয়া হয়। println!
-এর একটি কলে একটি ভেরিয়েবল এবং একটি এক্সপ্রেশনের ফলাফল প্রিন্ট করা এমন দেখাবে:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
এই কোডটি x = 5 and y + 2 = 12
প্রিন্ট করবে।
প্রথম অংশের পরীক্ষা (Testing the First Part)
আসুন, অনুমানের গেমের প্রথম অংশটি পরীক্ষা করি। cargo run
ব্যবহার করে এটি চালান:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
এই পর্যন্ত, গেমের প্রথম অংশটি সম্পন্ন হয়েছে: আমরা কীবোর্ড থেকে ইনপুট নিচ্ছি এবং সেটি প্রিন্ট করছি।
একটি সংখ্যা অনুমানের গেম প্রোগ্রামিং (Programming a Guessing Game)
আসুন, একসাথে একটি হ্যান্ডস-অন প্রোজেক্টে কাজ করার মাধ্যমে Rust-এর জগতে ঝাঁপ দেই! এই চ্যাপ্টারটি আপনাকে কিছু সাধারণ Rust কনসেপ্টের সাথে পরিচয় করিয়ে দেবে, একটি বাস্তব প্রোগ্রামে সেগুলো কীভাবে ব্যবহার করা হয় তা দেখিয়ে। আপনি let
, match
, মেথড, অ্যাসোসিয়েটেড ফাংশন, এক্সটার্নাল ক্রেট এবং আরও অনেক কিছু সম্পর্কে জানতে পারবেন! নিচের চ্যাপ্টারগুলোতে, আমরা এই ধারণাগুলো আরও বিস্তারিতভাবে আলোচনা করব। এই চ্যাপ্টারে, আপনি শুধুমাত্র মৌলিক বিষয়গুলো অনুশীলন করবেন।
আমরা একটি ক্লাসিক বিগিনার প্রোগ্রামিং সমস্যা সমাধান করব: একটি সংখ্যা অনুমানের গেম। এটি কীভাবে কাজ করে তা নিচে বলা হলো: প্রোগ্রামটি 1 থেকে 100-এর মধ্যে একটি র্যান্ডম সংখ্যা তৈরি করবে। তারপর এটি প্লেয়ারকে একটি সংখ্যা অনুমান করতে বলবে। একটি সংখ্যা অনুমান করার পরে, প্রোগ্রামটি জানাবে যে অনুমানটি খুব কম না বেশি হয়েছে। যদি অনুমানটি সঠিক হয়, তাহলে গেমটি একটি অভিনন্দন বার্তা প্রিন্ট করবে এবং শেষ হয়ে যাবে।
একটি নতুন প্রোজেক্ট সেট আপ করা (Setting Up a New Project)
একটি নতুন প্রোজেক্ট সেট আপ করতে, চ্যাপ্টার ১-এ তৈরি করা projects ডিরেক্টরিতে যান এবং Cargo ব্যবহার করে নিচের মতো একটি নতুন প্রোজেক্ট তৈরি করুন:
$ cargo new guessing_game
$ cd guessing_game
প্রথম কমান্ড, cargo new
, প্রোজেক্টের নাম (guessing_game
) প্রথম আর্গুমেন্ট হিসেবে নেয়। দ্বিতীয় কমান্ডটি নতুন প্রোজেক্টের ডিরেক্টরিতে যাওয়া নির্দেশ করে।
জেনারেট হওয়া Cargo.toml ফাইলটি দেখুন:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
আপনি যেমন চ্যাপ্টার ১-এ দেখেছেন, cargo new
আপনার জন্য একটি “Hello, world!” প্রোগ্রাম তৈরি করে। src/main.rs ফাইলটি দেখুন:
Filename: src/main.rs
fn main() { println!("Hello, world!"); }
এবার চলুন, এই “Hello, world!” প্রোগ্রামটি কম্পাইল করি এবং cargo run
কমান্ড ব্যবহার করে একই ধাপে চালাই:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `file:///projects/guessing_game/target/debug/guessing_game`
Hello, world!
run
কমান্ডটি তখন কাজে আসে যখন আপনাকে একটি প্রোজেক্টে দ্রুত পুনরাবৃত্তি করতে হয়, যেমনটি আমরা এই গেমে করব। অর্থাৎ, পরবর্তী ধাপে যাওয়ার আগে প্রতিটি পুনরাবৃত্তি দ্রুত পরীক্ষা করে নেওয়া যাবে।
src/main.rs ফাইলটি আবার খুলুন। আপনি এই ফাইলেই সমস্ত কোড লিখবেন।
একটি অনুমান প্রক্রিয়া করা (Processing a Guess)
অনুমান করার গেম প্রোগ্রামটির প্রথম অংশ ব্যবহারকারীর ইনপুট চাইবে, সেই ইনপুটটি প্রক্রিয়া করবে এবং ইনপুটটি প্রত্যাশিত ফর্ম্যাটে আছে কিনা তা পরীক্ষা করবে। শুরু করার জন্য, আমরা প্লেয়ারকে একটি সংখ্যা অনুমান করতে দেব। Listing 2-1-এর কোডটি src/main.rs-এ লিখুন।
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এই কোডটিতে অনেক তথ্য রয়েছে, তাই চলুন লাইন ধরে ধরে আলোচনা করি। ব্যবহারকারীর ইনপুট নিতে এবং তারপর ফলাফলটি আউটপুট হিসেবে প্রিন্ট করতে, আমাদের io
ইনপুট/আউটপুট লাইব্রেরিটিকে স্কোপের মধ্যে আনতে হবে। io
লাইব্রেরিটি স্ট্যান্ডার্ড লাইব্রেরি থেকে আসে, যাকে std
বলা হয়:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
ডিফল্টরূপে, Rust-এর স্ট্যান্ডার্ড লাইব্রেরিতে সংজ্ঞায়িত আইটেমগুলোর একটি সেট রয়েছে, যা প্রতিটি প্রোগ্রামের স্কোপে আনা হয়। এই সেটটিকে প্রেলিউড (prelude) বলা হয়, এবং আপনি স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশনে prelude-এর সবকিছু দেখতে পারেন।
যদি আপনি যে টাইপটি ব্যবহার করতে চান সেটি প্রেলিউডে না থাকে, তাহলে আপনাকে use
স্টেটমেন্ট ব্যবহার করে সেই টাইপটিকে স্পষ্টতই স্কোপে আনতে হবে। std::io
লাইব্রেরি ব্যবহার করলে আপনি বেশ কিছু দরকারী ফিচার পাবেন, যার মধ্যে ব্যবহারকারীর ইনপুট নেওয়ার ক্ষমতাও রয়েছে।
আপনি যেমন চ্যাপ্টার ১-এ দেখেছেন, main
ফাংশনটি প্রোগ্রামে প্রবেশের পথ:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
fn
সিনট্যাক্স একটি নতুন ফাংশন ঘোষণা করে; বন্ধনী, ()
, নির্দেশ করে যে কোনও প্যারামিটার নেই; এবং কার্লি ব্র্যাকেট, {}
, ফাংশনের বডি শুরু করে।
আপনি চ্যাপ্টার ১-এ আরও শিখেছেন যে, println!
হল একটি ম্যাক্রো যা স্ক্রিনে একটি স্ট্রিং প্রিন্ট করে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এই কোডটি একটি প্রম্পট প্রিন্ট করছে, যাতে গেমটি কী তা বলা হয়েছে এবং ব্যবহারকারীর কাছ থেকে ইনপুট চাওয়া হয়েছে।
ভেরিয়েবল ব্যবহার করে মান সংরক্ষণ করা (Storing Values with Variables)
এরপর, আমরা ব্যবহারকারীর ইনপুট সংরক্ষণ করার জন্য একটি ভেরিয়েবল তৈরি করব, এইভাবে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এবার প্রোগ্রামটি আরও মজাদার হচ্ছে! এই ছোট লাইনে অনেক কিছু ঘটছে। আমরা ভেরিয়েবল তৈরি করতে let
স্টেটমেন্ট ব্যবহার করি। আরেকটি উদাহরণ নিচে দেওয়া হলো:
let apples = 5;
এই লাইনটি apples
নামের একটি নতুন ভেরিয়েবল তৈরি করে এবং এটিকে 5 মানের সাথে বাইন্ড করে। Rust-এ, ভেরিয়েবলগুলো ডিফল্টরূপে ইমিউটেবল (immutable) হয়, অর্থাৎ একবার আমরা ভেরিয়েবলকে একটি মান দিলে, সেই মানটি পরিবর্তন হবে না। আমরা চ্যাপ্টার ৩-এর “ভেরিয়েবল এবং মিউটেবিলিটি” বিভাগে এই ধারণাটি নিয়ে বিস্তারিত আলোচনা করব। একটি ভেরিয়েবলকে মিউটেবল (mutable) করতে, আমরা ভেরিয়েবলের নামের আগে mut
যোগ করি:
let apples = 5; // immutable
let mut bananas = 5; // mutable
দ্রষ্টব্য:
//
সিনট্যাক্স একটি কমেন্ট শুরু করে, যা লাইনের শেষ পর্যন্ত চলতে থাকে। Rust কমেন্টের ভেতরের সবকিছু উপেক্ষা করে। আমরা চ্যাপ্টার ৩-এ কমেন্ট নিয়ে আরও বিস্তারিত আলোচনা করব।
অনুমানের গেমের প্রোগ্রামে ফিরে আসা যাক। আপনি এখন জানেন যে let mut guess
guess
নামের একটি মিউটেবল ভেরিয়েবল তৈরি করবে। সমান চিহ্ন (=
) Rust-কে বলে যে আমরা এখনই ভেরিয়েবলের সাথে কিছু বাইন্ড করতে চাই। সমান চিহ্নের ডানদিকে guess
-এর মান রয়েছে, যেটি String::new
কল করার ফলাফল। String::new
একটি ফাংশন, যা একটি String
-এর নতুন ইন্সট্যান্স রিটার্ন করে। String
হল স্ট্যান্ডার্ড লাইব্রেরি থেকে পাওয়া একটি স্ট্রিং টাইপ, যা প্রসারণযোগ্য (growable), UTF-8 এনকোডেড টেক্সট।
::new
লাইনের ::
সিনট্যাক্স নির্দেশ করে যে new
হল String
টাইপের একটি অ্যাসোসিয়েটেড ফাংশন। একটি অ্যাসোসিয়েটেড ফাংশন হল এমন একটি ফাংশন যা একটি টাইপের উপর ইমপ্লিমেন্ট করা হয়, এক্ষেত্রে String
। এই new
ফাংশনটি একটি নতুন, খালি স্ট্রিং তৈরি করে। আপনি অনেক টাইপের ওপর new
ফাংশন দেখতে পাবেন, কারণ এটি একটি সাধারণ নাম, যা কোনো কিছুর একটি নতুন মান তৈরি করে।
পুরো let mut guess = String::new();
লাইনটি একটি মিউটেবল ভেরিয়েবল তৈরি করেছে, যা বর্তমানে একটি String
-এর নতুন, খালি ইন্সট্যান্সের সাথে বাইন্ড করা আছে।
ব্যবহারকারীর ইনপুট গ্রহণ করা (Receiving User Input)
মনে করে দেখুন, আমরা প্রোগ্রামের প্রথম লাইনে use std::io;
দিয়ে স্ট্যান্ডার্ড লাইব্রেরি থেকে ইনপুট/আউটপুট ফাংশনালিটি অন্তর্ভুক্ত করেছিলাম। এবার আমরা io
মডিউল থেকে stdin
ফাংশনটি কল করব, যা আমাদের ব্যবহারকারীর ইনপুট হ্যান্ডেল করতে দেবে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
যদি আমরা প্রোগ্রামের শুরুতে use std::io;
দিয়ে io
লাইব্রেরি ইম্পোর্ট না করতাম, তাহলেও আমরা এই ফাংশনটিকে std::io::stdin
লিখে ব্যবহার করতে পারতাম। stdin
ফাংশনটি std::io::Stdin
-এর একটি ইন্সট্যান্স রিটার্ন করে, যেটি আপনার টার্মিনালের স্ট্যান্ডার্ড ইনপুটের হ্যান্ডেলের প্রতিনিধিত্ব করে।
এরপর, .read_line(&mut guess)
লাইনটি ব্যবহারকারীর কাছ থেকে ইনপুট নেওয়ার জন্য স্ট্যান্ডার্ড ইনপুট হ্যান্ডেলে read_line
মেথড কল করে। আমরা read_line
-কে আর্গুমেন্ট হিসেবে &mut guess
পাস করছি, যাতে এটি ব্যবহারকারীর ইনপুট কোন স্ট্রিংয়ে সংরক্ষণ করবে তা বলতে পারে। read_line
-এর মূল কাজ হল ব্যবহারকারী স্ট্যান্ডার্ড ইনপুটে যা টাইপ করে, সেটি একটি স্ট্রিংয়ে যুক্ত করা (স্ট্রিংয়ের আগের কনটেন্ট মুছে না দিয়ে)। তাই আমরা সেই স্ট্রিংটিকে আর্গুমেন্ট হিসেবে পাস করি। স্ট্রিং আর্গুমেন্টটিকে অবশ্যই মিউটেবল হতে হবে, যাতে মেথডটি স্ট্রিংয়ের কনটেন্ট পরিবর্তন করতে পারে।
&
নির্দেশ করে যে এই আর্গুমেন্টটি একটি রেফারেন্স, যা আপনাকে আপনার কোডের একাধিক অংশকে মেমরিতে ডেটা একাধিকবার কপি না করেই ডেটার একটি অংশ অ্যাক্সেস করার সুবিধা দেয়। রেফারেন্স একটি জটিল ফিচার, এবং Rust-এর অন্যতম প্রধান সুবিধা হল রেফারেন্স ব্যবহার করা কতটা নিরাপদ এবং সহজ। এই প্রোগ্রামটি শেষ করার জন্য আপনাকে সেই সমস্ত বিবরণ জানার দরকার নেই। আপাতত, আপনার শুধু এটুকু জানলেই চলবে যে ভেরিয়েবলের মতো রেফারেন্সগুলোও ডিফল্টরূপে ইমিউটেবল হয়। তাই, এটিকে মিউটেবল করার জন্য আপনাকে &guess
-এর পরিবর্তে &mut guess
লিখতে হবে। (চ্যাপ্টার ৪ রেফারেন্স আরও বিশদভাবে ব্যাখ্যা করবে।)
Result
দিয়ে সম্ভাব্য ত্রুটি সামলানো (Handling Potential Failure with Result
)
আমরা এখনও এই লাইনেই কাজ করছি। আমরা এখন টেক্সটের তৃতীয় লাইন নিয়ে আলোচনা করছি, কিন্তু মনে রাখবেন যে এটি এখনও কোডের একটি একক লজিক্যাল লাইনের অংশ। পরের অংশটি হল এই মেথড:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
আমরা এই কোডটিকে এভাবে লিখতে পারতাম:
io::stdin().read_line(&mut guess).expect("Failed to read line");
কিন্তু, একটি লম্বা লাইন পড়া কঠিন, তাই এটিকে ভাগ করে নেওয়া ভালো। .method_name()
সিনট্যাক্স দিয়ে যখন আপনি একটি মেথড কল করেন, তখন প্রায়ই একটি নতুন লাইন এবং অন্যান্য হোয়াইটস্পেস যোগ করে লম্বা লাইনগুলোকে ভেঙে ফেলা বুদ্ধিমানের কাজ। এবার আলোচনা করা যাক এই লাইনটি কী করে।
আগে যেমন উল্লেখ করা হয়েছে, read_line
ব্যবহারকারী যা ইনপুট দেয়, সেটি আমাদের দেওয়া স্ট্রিংয়ে রাখে। কিন্তু এটি একটি Result
মানও রিটার্ন করে। Result
হল একটি গণনা (enumeration), যাকে প্রায়শই enum বলা হয়। এটি এমন একটি টাইপ, যা একাধিক সম্ভাব্য অবস্থার মধ্যে একটিতে থাকতে পারে। আমরা প্রতিটি সম্ভাব্য অবস্থাকে একটি ভেরিয়েন্ট বলি।
চ্যাপ্টার ৬-এ enum সম্পর্কে আরও বিস্তারিত আলোচনা করা হবে। এই Result
টাইপগুলোর উদ্দেশ্য হল এরর-হ্যান্ডলিং তথ্য এনকোড করা।
Result
-এর ভেরিয়েন্টগুলো হল Ok
এবং Err
। Ok
ভেরিয়েন্ট নির্দেশ করে যে অপারেশন সফল হয়েছে এবং এর মধ্যে সফলভাবে জেনারেট হওয়া মান রয়েছে। Err
ভেরিয়েন্ট মানে অপারেশন ব্যর্থ হয়েছে এবং এর মধ্যে অপারেশনটি কীভাবে বা কেন ব্যর্থ হয়েছে সে সম্পর্কে তথ্য রয়েছে।
যেকোনো টাইপের মানের মতোই Result
টাইপের মানগুলোরও নিজস্ব মেথড ডিফাইন করা থাকে। Result
-এর একটি ইন্সট্যান্সের একটি expect
মেথড রয়েছে, যাকে আপনি কল করতে পারেন। যদি Result
-এর এই ইন্সট্যান্সটি একটি Err
মান হয়, তাহলে expect
প্রোগ্রামটিকে ক্র্যাশ করাবে এবং আপনি expect
-এর আর্গুমেন্ট হিসেবে যে মেসেজটি পাস করেছেন সেটি প্রদর্শন করবে। যদি read_line
মেথড একটি Err
রিটার্ন করে, তাহলে সম্ভবত এটি অন্তর্নিহিত অপারেটিং সিস্টেম থেকে আসা কোনো এররের ফলাফল। যদি Result
-এর এই ইন্সট্যান্সটি একটি Ok
মান হয়, তাহলে expect
Ok
-এর মধ্যে থাকা রিটার্ন মানটি গ্রহণ করবে এবং শুধুমাত্র সেই মানটি আপনাকে রিটার্ন করবে, যাতে আপনি সেটি ব্যবহার করতে পারেন। এক্ষেত্রে, সেই মানটি হল ব্যবহারকারীর ইনপুটের বাইটের সংখ্যা।
আপনি যদি expect
কল না করেন, তাহলে প্রোগ্রামটি কম্পাইল হবে, কিন্তু আপনি একটি ওয়ার্নিং পাবেন:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= 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
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust সতর্ক করে যে আপনি read_line
থেকে রিটার্ন হওয়া Result
মানটি ব্যবহার করেননি, যা নির্দেশ করে যে প্রোগ্রামটি একটি সম্ভাব্য এরর হ্যান্ডেল করেনি।
ওয়ার্নিংটি দমন করার সঠিক উপায় হল আসলে এরর-হ্যান্ডলিং কোড লেখা। কিন্তু আমাদের ক্ষেত্রে, কোনো সমস্যা হলে আমরা শুধু এই প্রোগ্রামটিকে ক্র্যাশ করাতে চাই, তাই আমরা expect
ব্যবহার করতে পারি। আপনি চ্যাপ্টার ৯-এ এরর থেকে পুনরুদ্ধার সম্পর্কে জানতে পারবেন।
println!
প্লেসহোল্ডার দিয়ে মান প্রিন্ট করা (Printing Values with println!
Placeholders)
ক্লোজিং কার্লি ব্র্যাকেট ছাড়াও, এখনও পর্যন্ত কোডটিতে আলোচনা করার মতো আর একটি লাইন রয়েছে:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
এই লাইনটি সেই স্ট্রিংটিকে প্রিন্ট করে, যেখানে এখন ব্যবহারকারীর ইনপুট রয়েছে। {}
কার্লি ব্র্যাকেটের সেটটি হল একটি প্লেসহোল্ডার: {}
-কে কাঁকড়ার ছোট সাঁড়াশি হিসেবে ভাবতে পারেন, যা একটি মানকে ধরে রাখে। যখন একটি ভেরিয়েবলের মান প্রিন্ট করা হয়, তখন কার্লি ব্র্যাকেটের ভেতরে ভেরিয়েবলের নাম দেওয়া যেতে পারে। যখন একটি এক্সপ্রেশনের মূল্যায়ন করা ফলাফল প্রিন্ট করা হয়, তখন ফরম্যাট স্ট্রিংয়ে খালি কার্লি ব্র্যাকেট বসানো হয়। তারপর ফরম্যাট স্ট্রিংয়ের পরে, প্রতিটি খালি কার্লি ব্র্যাকেট প্লেসহোল্ডারে প্রিন্ট করার জন্য কমা দিয়ে আলাদা করা এক্সপ্রেশনের একটি তালিকা একই ক্রমে দেওয়া হয়। println!
-এর একটি কলে একটি ভেরিয়েবল এবং একটি এক্সপ্রেশনের ফলাফল প্রিন্ট করা এমন দেখাবে:
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {x} and y + 2 = {}", y + 2); }
এই কোডটি x = 5 and y + 2 = 12
প্রিন্ট করবে।
প্রথম অংশের পরীক্ষা (Testing the First Part)
আসুন, অনুমানের গেমের প্রথম অংশটি পরীক্ষা করি। cargo run
ব্যবহার করে এটি চালান:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
এই পর্যন্ত, গেমের প্রথম অংশটি সম্পন্ন হয়েছে: আমরা কীবোর্ড থেকে ইনপুট নিচ্ছি এবং সেটি প্রিন্ট করছি।
একটি গোপন সংখ্যা তৈরি করা (Generating a Secret Number)
এরপর, আমাদের একটি গোপন সংখ্যা তৈরি করতে হবে, যেটি ব্যবহারকারী অনুমান করার চেষ্টা করবে। গোপন সংখ্যাটি প্রত্যেকবার আলাদা হওয়া উচিত, যাতে গেমটি একাধিকবার খেলতে মজা লাগে। আমরা 1 থেকে 100-এর মধ্যে একটি র্যান্ডম সংখ্যা ব্যবহার করব, যাতে গেমটি খুব কঠিন না হয়। Rust-এর স্ট্যান্ডার্ড লাইব্রেরিতে এখনও র্যান্ডম সংখ্যার কার্যকারিতা অন্তর্ভুক্ত নেই। তবে, Rust টিম এই কার্যকারিতা সহ একটি rand
ক্রেট সরবরাহ করে।
আরও কার্যকারিতা পেতে একটি ক্রেট ব্যবহার করা (Using a Crate to Get More Functionality)
মনে রাখবেন যে একটি ক্রেট হল Rust সোর্স কোড ফাইলগুলোর একটি সংগ্রহ। আমরা যে প্রোজেক্টটি তৈরি করছি সেটি হল একটি বাইনারি ক্রেট, যেটি একটি এক্সিকিউটেবল। rand
ক্রেটটি হল একটি লাইব্রেরি ক্রেট, যেটিতে কোড রয়েছে যা অন্য প্রোগ্রামগুলোতে ব্যবহার করার উদ্দেশ্যে তৈরি এবং নিজে থেকে চালানো যায় না।
Cargo-র এক্সটার্নাল ক্রেটগুলোর সমন্বয় হল সেই জায়গা যেখানে Cargo সত্যিই சிறந்து (তামিল শব্দ, অর্থ 'shines') দেখায়। rand
ব্যবহার করে এমন কোড লেখার আগে, আমাদের Cargo.toml ফাইলটিকে পরিবর্তন করতে হবে, যাতে rand
ক্রেটটি একটি ডিপেন্ডেন্সি হিসেবে অন্তর্ভুক্ত হয়। এখন সেই ফাইলটি খুলুন এবং Cargo আপনার জন্য তৈরি করা [dependencies]
সেকশন হেডারের নিচে, নিচের লাইনটি যোগ করুন। এখানে যেভাবে rand
নির্দিষ্ট করা হয়েছে, ঠিক সেভাবে এই ভার্সন নম্বর সহ নির্দিষ্ট করতে ভুলবেন না, নাহলে এই টিউটোরিয়ালের কোড উদাহরণগুলো কাজ নাও করতে পারে:
Filename: Cargo.toml
[dependencies]
rand = "0.8.5"
Cargo.toml ফাইলে, একটি হেডারের পরে যা কিছু থাকে তা সেই বিভাগের অংশ যা অন্য একটি বিভাগ শুরু না হওয়া পর্যন্ত চলতে থাকে। [dependencies]
-এ আপনি Cargo-কে জানান যে আপনার প্রোজেক্ট কোন এক্সটার্নাল ক্রেটগুলোর উপর নির্ভর করে এবং সেই ক্রেটগুলোর কোন ভার্সন আপনার প্রয়োজন। এক্ষেত্রে, আমরা rand
ক্রেটটিকে সেমান্টিক ভার্সন স্পেসিফায়ার 0.8.5
দিয়ে নির্দিষ্ট করি। Cargo সেমান্টিক ভার্সনিং (কখনও কখনও SemVer বলা হয়) বোঝে, যা ভার্সন নম্বর লেখার একটি স্ট্যান্ডার্ড। স্পেসিফায়ার 0.8.5
আসলে ^0.8.5
-এর শর্টহ্যান্ড, যার অর্থ হল যেকোনো ভার্সন যা কমপক্ষে 0.8.5 কিন্তু 0.9.0-এর নিচে।
Cargo এই ভার্সনগুলোকে 0.8.5 ভার্সনের সাথে সঙ্গতিপূর্ণ পাবলিক API-এর অধিকারী বলে মনে করে এবং এই স্পেসিফিকেশন নিশ্চিত করে যে আপনি সর্বশেষ প্যাচ রিলিজ পাবেন যা এখনও এই চ্যাপ্টারের কোডের সাথে কম্পাইল হবে। 0.9.0 বা তার বেশি কোনো ভার্সনের ক্ষেত্রে, নিচের উদাহরণগুলোতে ব্যবহৃত API-এর মতো একই API থাকার কোনো গ্যারান্টি নেই।
এখন, কোডের কোনো পরিবর্তন না করেই, চলুন প্রোজেক্টটি বিল্ড করি, যেমনটি Listing 2-2-তে দেখানো হয়েছে।
$ cargo build
Updating crates.io index
Locking 15 packages to latest Rust 1.85.0 compatible versions
Adding rand v0.8.5 (available: v0.9.0)
Compiling proc-macro2 v1.0.93
Compiling unicode-ident v1.0.17
Compiling libc v0.2.170
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.38
Compiling syn v2.0.98
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
আপনি হয়তো ভিন্ন ভার্সন নম্বর দেখতে পারেন (কিন্তু সেগুলো সবই কোডের সাথে সঙ্গতিপূর্ণ হবে, SemVer-এর কারণে!) এবং ভিন্ন লাইন (অপারেটিং সিস্টেমের উপর নির্ভর করে), এবং লাইনগুলো ভিন্ন ক্রমে থাকতে পারে।
যখন আমরা একটি এক্সটার্নাল ডিপেন্ডেন্সি অন্তর্ভুক্ত করি, Cargo সেই ডিপেন্ডেন্সির প্রয়োজনীয় সবকিছুর সর্বশেষ ভার্সন রেজিস্ট্রি থেকে নিয়ে আসে, যেটি Crates.io থেকে ডেটার একটি কপি। Crates.io হল সেই জায়গা যেখানে Rust ইকোসিস্টেমের লোকেরা তাদের ওপেন সোর্স Rust প্রোজেক্টগুলো অন্যদের ব্যবহারের জন্য পোস্ট করে।
রেজিস্ট্রি আপডেট করার পরে, Cargo [dependencies]
সেকশনটি পরীক্ষা করে এবং তালিকাভুক্ত যেকোনো ক্রেট ডাউনলোড করে, যেগুলো ইতিমধ্যেই ডাউনলোড করা হয়নি। এই ক্ষেত্রে, যদিও আমরা শুধুমাত্র rand
-কে ডিপেন্ডেন্সি হিসেবে তালিকাভুক্ত করেছি, Cargo rand
কাজ করার জন্য যে অন্যান্য ক্রেটগুলোর উপর নির্ভর করে সেগুলোও নিয়ে এসেছে। ক্রেটগুলো ডাউনলোড করার পরে, Rust সেগুলোকে কম্পাইল করে এবং তারপর ডিপেন্ডেন্সিগুলো উপলব্ধ করে প্রোজেক্টটি কম্পাইল করে।
আপনি যদি কোনো পরিবর্তন না করেই আবার cargo build
চালান, তাহলে আপনি Finished
লাইনটি ছাড়া আর কোনো আউটপুট পাবেন না। Cargo জানে যে এটি ইতিমধ্যেই ডিপেন্ডেন্সিগুলো ডাউনলোড এবং কম্পাইল করেছে, এবং আপনি আপনার Cargo.toml ফাইলে সেগুলো সম্পর্কে কোনো পরিবর্তন করেননি। Cargo আরও জানে যে আপনি আপনার কোড সম্পর্কে কোনো পরিবর্তন করেননি, তাই এটি সেটিও পুনরায় কম্পাইল করে না। করার মতো কিছু না থাকায়, এটি কেবল শেষ হয়ে যায়।
আপনি যদি src/main.rs ফাইলটি খোলেন, একটি সামান্য পরিবর্তন করেন, এবং তারপর সংরক্ষণ করে আবার বিল্ড করেন, তাহলে আপনি কেবল দুটি লাইনের আউটপুট দেখতে পাবেন:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
এই লাইনগুলো দেখায় যে Cargo শুধুমাত্র src/main.rs ফাইলে আপনার ছোট পরিবর্তনের সাথে বিল্ডটি আপডেট করে। আপনার ডিপেন্ডেন্সিগুলো পরিবর্তন হয়নি, তাই Cargo জানে যে এটি ইতিমধ্যেই সেগুলোর জন্য যা ডাউনলোড এবং কম্পাইল করেছে তা পুনরায় ব্যবহার করতে পারে।
Cargo.lock ফাইল দিয়ে রিপ্রোডিউসিবল বিল্ড নিশ্চিত করা (Ensuring Reproducible Builds with the Cargo.lock File)
Cargo-র একটি মেকানিজম রয়েছে যা নিশ্চিত করে যে আপনি বা অন্য কেউ যখনই আপনার কোড বিল্ড করবেন, তখনই যেন একই আর্টিফ্যাক্ট পুনরায় তৈরি করা যায়: আপনি অন্যথায় নির্দেশ না দেওয়া পর্যন্ত Cargo শুধুমাত্র আপনার নির্দিষ্ট করা ডিপেন্ডেন্সিগুলোর ভার্সনগুলোই ব্যবহার করবে। উদাহরণস্বরূপ, ধরা যাক পরের সপ্তাহে rand
ক্রেটের 0.8.6 ভার্সন প্রকাশিত হল, এবং সেই ভার্সনে একটি গুরুত্বপূর্ণ বাগ ফিক্স রয়েছে, কিন্তু এটিতে একটি রিগ্রেশনও রয়েছে যা আপনার কোডকে ভেঙে দেবে। এটি হ্যান্ডেল করার জন্য, আপনি যখন প্রথমবার cargo build
চালান তখন Rust Cargo.lock ফাইলটি তৈরি করে, তাই এখন আমাদের guessing_game ডিরেক্টরিতে এটি রয়েছে।
আপনি যখন প্রথমবার একটি প্রোজেক্ট বিল্ড করেন, Cargo মানদণ্ড পূরণ করে এমন ডিপেন্ডেন্সিগুলোর সমস্ত ভার্সন খুঁজে বের করে এবং তারপর সেগুলোকে Cargo.lock ফাইলে লিখে রাখে। ভবিষ্যতে যখন আপনি আপনার প্রোজেক্ট বিল্ড করবেন, Cargo দেখবে যে Cargo.lock ফাইলটি বিদ্যমান এবং ভার্সনগুলো পুনরায় বের করার সমস্ত কাজ না করে সেখানে নির্দিষ্ট করা ভার্সনগুলো ব্যবহার করবে। এটি আপনাকে স্বয়ংক্রিয়ভাবে একটি রিপ্রোডিউসিবল বিল্ড করতে দেয়। অন্য কথায়, আপনার প্রোজেক্টটি 0.8.5-এই থাকবে যতক্ষণ না আপনি স্পষ্টতই আপগ্রেড করেন, Cargo.lock ফাইলের কারণে। যেহেতু রিপ্রোডিউসিবল বিল্ডের জন্য Cargo.lock ফাইলটি গুরুত্বপূর্ণ, তাই এটিকে প্রায়শই আপনার প্রোজেক্টের বাকি কোডের সাথে সোর্স কন্ট্রোলে চেক ইন করা হয়।
একটি নতুন ভার্সন পেতে একটি ক্রেট আপডেট করা (Updating a Crate to Get a New Version)
যখন আপনি একটি ক্রেট আপডেট করতে চান, Cargo update
কমান্ড সরবরাহ করে, যেটি Cargo.lock ফাইলটিকে উপেক্ষা করবে এবং Cargo.toml-এ আপনার স্পেসিফিকেশন পূরণ করে এমন সমস্ত সর্বশেষ ভার্সন খুঁজে বের করবে। তারপর Cargo সেই ভার্সনগুলোকে Cargo.lock ফাইলে লিখবে। এই ক্ষেত্রে, Cargo শুধুমাত্র 0.8.5-এর চেয়ে বড় এবং 0.9.0-এর চেয়ে ছোট ভার্সনগুলো খুঁজবে। যদি rand
ক্রেট দুটি নতুন ভার্সন 0.8.6 এবং 0.9.0 প্রকাশ করে, তাহলে আপনি যদি cargo update
চালান তবে আপনি নিম্নলিখিতটি দেখতে পাবেন:
$ cargo update
Updating crates.io index
Locking 1 package to latest Rust 1.85.0 compatible version
Updating rand v0.8.5 -> v0.8.6 (available: v0.9.0)
Cargo 0.9.0 রিলিজটিকে উপেক্ষা করে। এই সময়ে, আপনি আপনার Cargo.lock ফাইলে একটি পরিবর্তনও লক্ষ্য করবেন, যেখানে উল্লেখ করা হয়েছে যে আপনি এখন যে rand
ক্রেট ভার্সনটি ব্যবহার করছেন সেটি হল 0.8.6। rand
ভার্সন 0.9.0 বা 0.9.x সিরিজের যেকোনো ভার্সন ব্যবহার করতে, আপনাকে Cargo.toml ফাইলটিকে এর পরিবর্তে এইরকম দেখাতে আপডেট করতে হবে:
[dependencies]
rand = "0.9.0"
পরের বার যখন আপনি cargo build
চালাবেন, Cargo উপলব্ধ ক্রেটগুলোর রেজিস্ট্রি আপডেট করবে এবং আপনার নির্দিষ্ট করা নতুন ভার্সন অনুযায়ী আপনার rand
প্রয়োজনীয়তাগুলো পুনরায় মূল্যায়ন করবে।
Cargo এবং এর ইকোসিস্টেম সম্পর্কে আরও অনেক কিছু বলার আছে, যা আমরা চ্যাপ্টার 14-তে আলোচনা করব, কিন্তু আপাতত, আপনার এটুকুই জানা দরকার। Cargo লাইব্রেরিগুলোকে পুনরায় ব্যবহার করা খুব সহজ করে তোলে, তাই Rustacean-রা বেশ কয়েকটি প্যাকেজ থেকে একত্রিত করে ছোট প্রোজেক্ট লিখতে সক্ষম।
একটি র্যান্ডম সংখ্যা তৈরি করা (Generating a Random Number)
আসুন, অনুমান করার জন্য একটি সংখ্যা তৈরি করতে rand
ব্যবহার করা শুরু করি। পরবর্তী ধাপ হল src/main.rs আপডেট করা, যেমনটি Listing 2-3-তে দেখানো হয়েছে।
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
প্রথমে আমরা use rand::Rng;
লাইনটি যোগ করি। Rng
ট্রেইট সেই মেথডগুলো সংজ্ঞায়িত করে যেগুলো র্যান্ডম সংখ্যা জেনারেটররা প্রয়োগ করে এবং সেই মেথডগুলো ব্যবহার করার জন্য এই ট্রেইটটি আমাদের স্কোপে থাকতে হবে। চ্যাপ্টার ১০-এ ট্রেইটগুলো বিস্তারিতভাবে আলোচনা করা হবে।
এরপর, আমরা মাঝখানে দুটি লাইন যোগ করছি। প্রথম লাইনে, আমরা rand::thread_rng
ফাংশনটি কল করি যা আমাদের নির্দিষ্ট র্যান্ডম সংখ্যা জেনারেটর দেয় যা আমরা ব্যবহার করতে যাচ্ছি: যেটি এক্সিকিউশনের বর্তমান থ্রেডের জন্য লোকাল এবং অপারেটিং সিস্টেম দ্বারা সিডেড। তারপর আমরা র্যান্ডম সংখ্যা জেনারেটরের উপর gen_range
মেথডটি কল করি। এই মেথডটি Rng
ট্রেইট দ্বারা সংজ্ঞায়িত করা হয়েছে যা আমরা use rand::Rng;
স্টেটমেন্ট দিয়ে স্কোপে এনেছি। gen_range
মেথডটি একটি রেঞ্জ এক্সপ্রেশনকে আর্গুমেন্ট হিসেবে নেয় এবং রেঞ্জের মধ্যে একটি র্যান্ডম সংখ্যা তৈরি করে। আমরা এখানে যে ধরনের রেঞ্জ এক্সপ্রেশন ব্যবহার করছি সেটি start..=end
ফর্ম নেয় এবং নিম্ন ও উচ্চ উভয় সীমানাতেই অন্তর্ভুক্ত থাকে, তাই আমাদের 1 থেকে 100-এর মধ্যে একটি সংখ্যা অনুরোধ করতে 1..=100
নির্দিষ্ট করতে হবে।
দ্রষ্টব্য: আপনি শুধু কোন ট্রেইট ব্যবহার করবেন এবং কোন মেথড এবং ফাংশন একটি ক্রেট থেকে কল করবেন তা এমনি এমনি জানবেন না, তাই প্রতিটি ক্রেটের ডকুমেন্টেশন থাকে যাতে এটি ব্যবহারের নির্দেশাবলী থাকে। Cargo-র আরেকটি দারুণ ফিচার হল
cargo doc --open
কমান্ড চালালে আপনার সমস্ত ডিপেন্ডেন্সির দেওয়া ডকুমেন্টেশন লোকালি তৈরি হবে এবং আপনার ব্রাউজারে খুলবে। উদাহরণস্বরূপ, আপনি যদিrand
ক্রেটের অন্যান্য কার্যকারিতা সম্পর্কে আগ্রহী হন, তাহলেcargo doc --open
চালান এবং বাম দিকের সাইডবারেrand
-এ ক্লিক করুন।
দ্বিতীয় নতুন লাইনটি গোপন সংখ্যাটি প্রিন্ট করে। এটি দরকারী যখন আমরা প্রোগ্রামটি ডেভেলপ করছি তখন এটি পরীক্ষা করতে, কিন্তু আমরা ফাইনাল ভার্সন থেকে এটি মুছে ফেলব। প্রোগ্রামটি শুরু হওয়ার সাথে সাথেই উত্তর প্রিন্ট করলে সেটি আর তেমন কোনো গেম থাকে না!
কয়েকবার প্রোগ্রামটি চালানোর চেষ্টা করুন:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
আপনার ভিন্ন র্যান্ডম সংখ্যা পাওয়া উচিত, এবং সেগুলো সবই 1 থেকে 100-এর মধ্যে সংখ্যা হওয়া উচিত। দারুন কাজ!
অনুমানের সাথে গোপন সংখ্যার তুলনা করা (Comparing the Guess to the Secret Number)
এখন আমাদের কাছে ব্যবহারকারীর ইনপুট এবং একটি র্যান্ডম সংখ্যা রয়েছে, আমরা সেগুলোর তুলনা করতে পারি। সেই ধাপটি Listing 2-4-এ দেখানো হয়েছে। মনে রাখবেন যে এই কোডটি এখনই কম্পাইল হবে না, যেমনটি আমরা ব্যাখ্যা করব।
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
প্রথমে আমরা আরেকটি use
স্টেটমেন্ট যোগ করি, স্ট্যান্ডার্ড লাইব্রেরি থেকে std::cmp::Ordering
নামক একটি টাইপকে স্কোপে আনি। Ordering
টাইপটি হল আরেকটি enum এবং এর ভেরিয়েন্টগুলো হল Less
, Greater
, এবং Equal
। আপনি যখন দুটি মান তুলনা করেন তখন এই তিনটি ফলাফল সম্ভব।
তারপর আমরা নিচে পাঁচটি নতুন লাইন যোগ করি যা Ordering
টাইপ ব্যবহার করে। cmp
মেথড দুটি মান তুলনা করে এবং যে কোনো কিছুর উপর কল করা যেতে পারে যা তুলনা করা যায়। এটি আপনি যেটির সাথে তুলনা করতে চান তার একটি রেফারেন্স নেয়: এখানে এটি guess
-কে secret_number
-এর সাথে তুলনা করছে। তারপর এটি Ordering
enum-এর একটি ভেরিয়েন্ট রিটার্ন করে যা আমরা use
স্টেটমেন্ট দিয়ে স্কোপে এনেছি। আমরা একটি match
এক্সপ্রেশন ব্যবহার করি, guess
এবং secret_number
-এর মান সহ cmp
-এর কলে কোন Ordering
-এর ভেরিয়েন্ট রিটার্ন করা হয়েছিল তার উপর ভিত্তি করে পরবর্তীতে কী করতে হবে তা নির্ধারণ করতে।
একটি match
এক্সপ্রেশন আর্ম দিয়ে তৈরি। একটি আর্ম একটি প্যাটার্ন নিয়ে গঠিত, যার সাথে ম্যাচ করতে হবে, এবং কোডটি চালানো উচিত যদি match
-কে দেওয়া মান সেই আর্মের প্যাটার্নের সাথে মেলে। Rust match
-কে দেওয়া মান নেয় এবং প্রতিটি আর্মের প্যাটার্নের মধ্য দিয়ে যায়। প্যাটার্ন এবং match
কনস্ট্রাক্ট হল Rust-এর শক্তিশালী ফিচার: এগুলো আপনাকে বিভিন্ন পরিস্থিতি প্রকাশ করতে দেয় যা আপনার কোড সম্মুখীন হতে পারে এবং নিশ্চিত করে যে আপনি সেগুলোর সবই হ্যান্ডেল করেছেন। এই ফিচারগুলো যথাক্রমে চ্যাপ্টার ৬ এবং চ্যাপ্টার 19-এ বিস্তারিতভাবে আলোচনা করা হবে।
আসুন, আমরা এখানে যে match
এক্সপ্রেশনটি ব্যবহার করি তার একটি উদাহরণ দেখি। ধরা যাক যে ব্যবহারকারী 50 অনুমান করেছে এবং র্যান্ডমভাবে তৈরি গোপন সংখ্যাটি এবার 38।
কোড যখন 50-কে 38-এর সাথে তুলনা করে, তখন cmp
মেথডটি Ordering::Greater
রিটার্ন করবে, কারণ 50, 38-এর চেয়ে বড়। match
এক্সপ্রেশনটি Ordering::Greater
মান পায় এবং প্রতিটি আর্মের প্যাটার্ন পরীক্ষা করতে শুরু করে। এটি প্রথম আর্মের প্যাটার্ন, Ordering::Less
-এর দিকে তাকায় এবং দেখে যে Ordering::Greater
মানটি Ordering::Less
-এর সাথে মেলে না, তাই এটি সেই আর্মের কোডটিকে উপেক্ষা করে এবং পরের আর্মে চলে যায়। পরের আর্মের প্যাটার্নটি হল Ordering::Greater
, যেটি Ordering::Greater
-এর সাথে মিলে যায়! সেই আর্মের সাথে সম্পর্কিত কোডটি এক্সিকিউট হবে এবং স্ক্রিনে Too big!
প্রিন্ট করবে। প্রথম সফল ম্যাচের পরেই match
এক্সপ্রেশনটি শেষ হয়ে যায়, তাই এই পরিস্থিতিতে এটি শেষ আর্মের দিকে তাকাবে না।
কিন্তু, Listing 2-4-এর কোডটি এখনও কম্পাইল হবে না। চলুন চেষ্টা করি:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/cmp.rs:964:8
|
964 | fn cmp(&self, other: &Self) -> Ordering;
| ^^^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
এররের মূল অংশে বলা হয়েছে যে এখানে টাইপ মিলছে না (mismatched types)। Rust-এ একটি শক্তিশালী, স্ট্যাটিক টাইপ সিস্টেম রয়েছে। তবে, এতে টাইপ ইনফারেন্সও (type inference) রয়েছে। যখন আমরা let mut guess = String::new()
লিখেছিলাম, তখন Rust অনুমান করতে পেরেছিল যে guess
একটি String
হওয়া উচিত এবং আমাদের টাইপ লিখতে বাধ্য করেনি। অন্যদিকে, secret_number
হল একটি সংখ্যা টাইপ। Rust-এর কয়েকটি সংখ্যা টাইপের মান 1 থেকে 100-এর মধ্যে হতে পারে: i32
, একটি 32-বিট সংখ্যা; u32
, একটি আনসাইনড 32-বিট সংখ্যা; i64
, একটি 64-বিট সংখ্যা; এবং আরও অনেক কিছু। অন্যভাবে উল্লেখ না করা পর্যন্ত, Rust ডিফল্টভাবে i32
ব্যবহার করে, যেটি secret_number
-এর টাইপ, যদি না আপনি অন্য কোথাও টাইপ সম্পর্কিত তথ্য যোগ করেন যা Rust-কে ভিন্ন সাংখ্যিক টাইপ অনুমান করতে বাধ্য করে। এররের কারণ হল Rust একটি স্ট্রিং এবং একটি সংখ্যা টাইপের তুলনা করতে পারে না।
শেষ পর্যন্ত, আমরা চাই যে প্রোগ্রামটি ইনপুট হিসেবে যে String
পড়ে, সেটিকে একটি সংখ্যা টাইপে রূপান্তর করতে, যাতে আমরা এটিকে সংখ্যাগতভাবে গোপন সংখ্যার সাথে তুলনা করতে পারি। আমরা main
ফাংশন বডিতে এই লাইনটি যোগ করে তা করি:
Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
লাইনটি হল:
let guess: u32 = guess.trim().parse().expect("অনুগ্রহ করে একটি সংখ্যা টাইপ করুন!");
আমরা guess
নামে একটি ভেরিয়েবল তৈরি করি। কিন্তু, প্রোগ্রামে কি ইতিমধ্যেই guess
নামে একটি ভেরিয়েবল নেই? আছে, কিন্তু সুবিধাজনকভাবে Rust আমাদের guess
-এর আগের মানটিকে একটি নতুন মান দিয়ে শ্যাডো (shadow) করার অনুমতি দেয়। শ্যাডোয়িং (Shadowing) আমাদের guess
ভেরিয়েবলের নামটি পুনরায় ব্যবহার করার সুযোগ দেয়, guess_str
এবং guess
-এর মতো দুটি আলাদা ভেরিয়েবল তৈরি করতে বাধ্য করার পরিবর্তে। আমরা চ্যাপ্টার ৩-এ এটি আরও বিশদে আলোচনা করব, তবে আপাতত, জেনে রাখুন যে এই ফিচারটি প্রায়শই ব্যবহৃত হয় যখন আপনি একটি মানকে এক টাইপ থেকে অন্য টাইপে রূপান্তর করতে চান।
আমরা এই নতুন ভেরিয়েবলটিকে guess.trim().parse()
এক্সপ্রেশনের সাথে বাইন্ড করি। এক্সপ্রেশনের guess
সেই আসল guess
ভেরিয়েবলকে বোঝায়, যেখানে ইনপুটটি একটি স্ট্রিং হিসাবে ছিল। একটি String
ইন্সট্যান্সের উপর trim
মেথডটি শুরু এবং শেষের যেকোনো হোয়াইটস্পেস সরিয়ে দেবে, যা স্ট্রিংটিকে u32
-তে রূপান্তর করার আগে আমাদের অবশ্যই করতে হবে, কারণ u32
শুধুমাত্র সংখ্যাসূচক ডেটা ধারণ করতে পারে। ব্যবহারকারীকে read_line
সম্পূর্ণ করতে এবং তাদের অনুমান ইনপুট করতে enter চাপতে হবে, যা স্ট্রিংটিতে একটি নতুন লাইন ক্যারেক্টার যুক্ত করে। উদাহরণস্বরূপ, যদি ব্যবহারকারী 5 টাইপ করে এবং enter চাপে, তাহলে guess
দেখতে এরকম হবে: 5\n
। \n
মানে “newline”। (Windows-এ, enter চাপলে একটি ক্যারেজ রিটার্ন এবং একটি নতুন লাইন আসে, \r\n
।) trim
মেথডটি \n
বা \r\n
সরিয়ে দেয়, ফলে শুধুমাত্র 5
থাকে।
স্ট্রিং-এর উপর parse
মেথড একটি স্ট্রিংকে অন্য টাইপে রূপান্তর করে। এখানে, আমরা এটিকে একটি স্ট্রিং থেকে একটি সংখ্যায় রূপান্তর করতে ব্যবহার করি। let guess: u32
ব্যবহার করে আমাদের Rust-কে জানাতে হবে যে আমরা ঠিক কোন সংখ্যা টাইপ চাই। guess
-এর পরে কোলন (:
) Rust-কে বলে যে আমরা ভেরিয়েবলের টাইপ অ্যানোটেট করব। Rust-এর কয়েকটি বিল্ট-ইন সংখ্যা টাইপ রয়েছে; এখানে দেখানো u32
হল একটি আনসাইনড, 32-বিট ইন্টিজার। এটি একটি ছোট ধনাত্মক সংখ্যার জন্য একটি ভালো ডিফল্ট পছন্দ। আপনি চ্যাপ্টার ৩-এ অন্যান্য সংখ্যা টাইপ সম্পর্কে জানতে পারবেন।
অতিরিক্তভাবে, এই উদাহরণ প্রোগ্রামে u32
অ্যানোটেশন এবং secret_number
-এর সাথে তুলনা করার অর্থ হল Rust অনুমান করবে যে secret_number
-ও একটি u32
হওয়া উচিত। তাই এখন তুলনাটি একই টাইপের দুটি মানের মধ্যে হবে!
parse
মেথডটি কেবল সেইসব ক্যারেক্টারের উপর কাজ করবে যেগুলোকে যুক্তিযুক্তভাবে সংখ্যায় রূপান্তর করা যেতে পারে এবং তাই সহজেই এরর ঘটাতে পারে। উদাহরণস্বরূপ, যদি স্ট্রিংটিতে A👍%
থাকে, তাহলে এটিকে একটি সংখ্যায় রূপান্তর করার কোনো উপায় থাকবে না। যেহেতু এটি ব্যর্থ হতে পারে, তাই parse
মেথডটি একটি Result
টাইপ রিটার্ন করে, অনেকটা read_line
মেথডের মতোই (আগে “Result
দিয়ে সম্ভাব্য ত্রুটি সামলানো”)-তে আলোচনা করা হয়েছে)। আমরা এই Result
-টিকে আবার expect
মেথড ব্যবহার করে একইভাবে পরিচালনা করব। যদি parse
একটি Err
Result
ভেরিয়েন্ট রিটার্ন করে কারণ এটি স্ট্রিং থেকে একটি সংখ্যা তৈরি করতে পারেনি, তাহলে expect
কলটি গেমটিকে ক্র্যাশ করাবে এবং আমরা যে মেসেজটি দেব সেটি প্রিন্ট করবে। যদি parse
সফলভাবে স্ট্রিংটিকে একটি সংখ্যায় রূপান্তর করতে পারে, তাহলে এটি Result
-এর Ok
ভেরিয়েন্ট রিটার্ন করবে এবং expect
Ok
মান থেকে আমাদের কাঙ্ক্ষিত সংখ্যাটি রিটার্ন করবে।
চলুন এবার প্রোগ্রামটি চালানো যাক:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
দারুণ! অনুমানের আগে স্পেস যোগ করা হলেও, প্রোগ্রামটি বুঝতে পেরেছে যে ব্যবহারকারী 76 অনুমান করেছে। বিভিন্ন ধরনের ইনপুট দিয়ে ভিন্ন আচরণ যাচাই করতে কয়েকবার প্রোগ্রামটি চালান: সঠিকভাবে সংখ্যাটি অনুমান করুন, খুব বেশি একটি সংখ্যা অনুমান করুন এবং খুব কম একটি সংখ্যা অনুমান করুন।
আমাদের গেমের বেশিরভাগ অংশ এখন কাজ করছে, কিন্তু ব্যবহারকারী কেবল একটি অনুমান করতে পারে। চলুন একটি লুপ যোগ করে এটি পরিবর্তন করি!
লুপিংয়ের মাধ্যমে একাধিক অনুমানের সুযোগ (Allowing Multiple Guesses with Looping)
loop
কীওয়ার্ডটি একটি অসীম লুপ তৈরি করে। ব্যবহারকারীদের সংখ্যা অনুমান করার আরও সুযোগ দিতে আমরা একটি লুপ যোগ করব:
Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
আপনি দেখতে পাচ্ছেন, আমরা অনুমান ইনপুট প্রম্পট থেকে শুরু করে সবকিছু একটি লুপের মধ্যে নিয়ে এসেছি। লুপের ভেতরের লাইনগুলোকে আরও চারটি স্পেস দিয়ে ইনডেন্ট করতে ভুলবেন না এবং আবার প্রোগ্রামটি চালান। প্রোগ্রামটি এখন চিরকালের জন্য আরেকটি অনুমানের জন্য জিজ্ঞাসা করবে, যা আসলে একটি নতুন সমস্যা তৈরি করে। মনে হচ্ছে ব্যবহারকারী গেমটি বন্ধ করতে পারবে না!
ব্যবহারকারী সবসময় কীবোর্ড শর্টকাট ctrl-c ব্যবহার করে প্রোগ্রামটি থামাতে পারে। কিন্তু এই অতৃপ্ত দৈত্য থেকে বাঁচার আরেকটি উপায় আছে, যেমনটি “অনুমানের সাথে গোপন সংখ্যার তুলনা করা”-তে parse
আলোচনায় উল্লেখ করা হয়েছে: যদি ব্যবহারকারী একটি অ-সংখ্যাসূচক উত্তর প্রবেশ করান, তাহলে প্রোগ্রামটি ক্র্যাশ করবে। ব্যবহারকারীকে গেম বন্ধ করার অনুমতি দেওয়ার জন্য আমরা সেটি কাজে লাগাতে পারি, যেমনটি এখানে দেখানো হয়েছে:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
quit
টাইপ করলে গেমটি বন্ধ হয়ে যাবে, কিন্তু আপনি যেমন লক্ষ্য করবেন, অন্য কোনো অ-সংখ্যাসূচক ইনপুট দিলেও এটি বন্ধ হয়ে যাবে। অন্তত বলতে গেলে, এটি আদর্শ নয়; আমরা চাই যে গেমটি তখনই বন্ধ হোক যখন সঠিক সংখ্যাটি অনুমান করা হয়।
সঠিক অনুমানের পরে বন্ধ হওয়া (Quitting After a Correct Guess)
আসুন, একটি break
স্টেটমেন্ট যোগ করে গেমটি এমনভাবে প্রোগ্রাম করি যাতে ব্যবহারকারী জিতলে বন্ধ হয়ে যায়:
Filename: src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
You win!
-এর পরে break
লাইনটি যোগ করলে, ব্যবহারকারী যখন গোপন সংখ্যাটি সঠিকভাবে অনুমান করে, তখন প্রোগ্রামটি লুপ থেকে বেরিয়ে আসে। লুপ থেকে বেরিয়ে আসার অর্থ প্রোগ্রাম থেকেও বেরিয়ে আসা, কারণ লুপটি হল main
-এর শেষ অংশ।
অবৈধ ইনপুট হ্যান্ডেল করা (Handling Invalid Input)
গেমের আচরণকে আরও উন্নত করতে, ব্যবহারকারী যখন একটি অ-সংখ্যা ইনপুট দেয় তখন প্রোগ্রামটি ক্র্যাশ করার পরিবর্তে, আসুন গেমটিকে অ-সংখ্যা উপেক্ষা করতে দিই যাতে ব্যবহারকারী অনুমান চালিয়ে যেতে পারে। আমরা guess
-কে String
থেকে u32
-তে রূপান্তর করা লাইনটি পরিবর্তন করে তা করতে পারি, যেমনটি Listing 2-5-এ দেখানো হয়েছে।
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
এররে ক্র্যাশ করা থেকে এরর হ্যান্ডেলিংয়ে যেতে আমরা একটি expect
কল থেকে একটি match
এক্সপ্রেশনে পরিবর্তন করি। মনে রাখবেন যে parse
একটি Result
টাইপ রিটার্ন করে এবং Result
হল একটি enum যার ভেরিয়েন্টগুলো হল Ok
এবং Err
। আমরা এখানে একটি match
এক্সপ্রেশন ব্যবহার করছি, যেমনটি আমরা cmp
মেথডের Ordering
ফলাফলের সাথে করেছিলাম।
যদি parse
সফলভাবে স্ট্রিংটিকে একটি সংখ্যায় পরিণত করতে সক্ষম হয়, তাহলে এটি একটি Ok
মান রিটার্ন করবে, যেটিতে প্রাপ্ত সংখ্যাটি থাকবে। সেই Ok
মানটি প্রথম আর্মের প্যাটার্নের সাথে মিলবে, এবং match
এক্সপ্রেশনটি কেবল parse
যে num
মানটি তৈরি করেছে এবং Ok
মানের ভিতরে রেখেছে সেটি রিটার্ন করবে। সেই সংখ্যাটি যেখানে আমাদের দরকার, অর্থাৎ আমরা যে নতুন guess
ভেরিয়েবল তৈরি করছি, সেখানেই চলে যাবে।
যদি parse
স্ট্রিংটিকে একটি সংখ্যায় পরিণত করতে সক্ষম না হয়, তাহলে এটি একটি Err
মান রিটার্ন করবে, যেটিতে এরর সম্পর্কে আরও তথ্য থাকবে। Err
মানটি প্রথম match
আর্মের Ok(num)
প্যাটার্নের সাথে মেলে না, তবে এটি দ্বিতীয় আর্মের Err(_)
প্যাটার্নের সাথে মেলে। আন্ডারস্কোর, _
, হল একটি ক্যাচ-অল মান; এই উদাহরণে, আমরা বলছি যে আমরা সমস্ত Err
মানের সাথে মেলাতে চাই, সেগুলোর ভিতরে যাই তথ্য থাকুক না কেন। তাই প্রোগ্রামটি দ্বিতীয় আর্মের কোড, continue
, চালাবে, যেটি প্রোগ্রামটিকে loop
-এর পরবর্তী পুনরাবৃত্তিতে যেতে এবং আরেকটি অনুমানের জন্য জিজ্ঞাসা করতে বলে। সুতরাং, কার্যকরভাবে, প্রোগ্রামটি parse
-এর সম্মুখীন হতে পারে এমন সমস্ত এরর উপেক্ষা করে!
এবার প্রোগ্রামের সবকিছু প্রত্যাশিতভাবে কাজ করা উচিত। চলুন চেষ্টা করি:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
দারুণ! একটি ছোট্ট ফাইনাল টুইক দিয়ে, আমরা অনুমানের গেমটি শেষ করব। মনে রাখবেন যে প্রোগ্রামটি এখনও গোপন সংখ্যাটি প্রিন্ট করছে। সেটি পরীক্ষার জন্য ভাল কাজ করেছিল, কিন্তু এটি গেমটিকে নষ্ট করে দেয়। চলুন println!
-টি মুছে ফেলি যেটি গোপন সংখ্যাটি আউটপুট করে। Listing 2-6 চূড়ান্ত কোডটি দেখায়।
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
এই পর্যায়ে, আপনি সফলভাবে অনুমানের গেমটি তৈরি করেছেন। অভিনন্দন!
সারসংক্ষেপ (Summary)
এই প্রোজেক্টটি আপনাকে অনেকগুলো নতুন Rust কনসেপ্টের সাথে পরিচয় করিয়ে দেওয়ার একটি হ্যান্ডস-অন উপায় ছিল: let
, match
, ফাংশন, এক্সটার্নাল ক্রেটগুলোর ব্যবহার এবং আরও অনেক কিছু। পরের কয়েকটি চ্যাপ্টারে, আপনি এই কনসেপ্টগুলো সম্পর্কে আরও বিস্তারিত জানতে পারবেন। চ্যাপ্টার ৩-এ বেশিরভাগ প্রোগ্রামিং ল্যাঙ্গুয়েজের কনসেপ্টগুলো, যেমন ভেরিয়েবল, ডেটা টাইপ এবং ফাংশন কভার করে এবং সেগুলো Rust-এ কীভাবে ব্যবহার করতে হয় তা দেখায়। চ্যাপ্টার ৪-এ ওনারশিপ (ownership) নিয়ে আলোচনা করা হয়েছে, একটি ফিচার যা Rust-কে অন্যান্য ল্যাঙ্গুয়েজ থেকে আলাদা করে। চ্যাপ্টার ৫-এ স্ট্রাক্ট এবং মেথড সিনট্যাক্স নিয়ে আলোচনা করা হয়েছে এবং চ্যাপ্টার ৬ ব্যাখ্যা করে যে কীভাবে enum কাজ করে।