শুরু করা যাক
রাস্ট এর সাথে আপনার যাত্রা শুরু করা যাক! শেখার অনেক কিছু আছে, কিন্তু প্রতিটি যাত্রার একটি শুরু থাকে। এই অধ্যায়ে, আমরা আলোচনা করব:
- Linux, macOS, এবং Windows-এ রাস্ট ইনস্টল করা
Hello, world!
প্রিন্ট করে এমন একটি প্রোগ্রাম লেখাcargo
, রাস্টের প্যাকেজ ম্যানেজার এবং বিল্ড সিস্টেম ব্যবহার করা
Installation
প্রথম ধাপ হল রাস্ট ইনস্টল করা। আমরা rustup
এর মাধ্যমে রাস্ট ডাউনলোড করব, যা রাস্টের ভার্সন এবং সম্পর্কিত টুলগুলো পরিচালনা করার জন্য একটি কমান্ড লাইন টুল। ডাউনলোড করার জন্য আপনার একটি ইন্টারনেট সংযোগ প্রয়োজন হবে।
দ্রষ্টব্য: আপনি যদি কোনো কারণে
rustup
ব্যবহার করতে না চান, তাহলে আরও বিকল্পের জন্য Other Rust Installation Methods পেজটি দেখুন।
নিচের পদক্ষেপগুলো রাস্ট কম্পাইলারের সর্বশেষ স্টেবল ভার্সন ইনস্টল করবে। রাস্টের স্ট্যাবিলিটি গ্যারান্টি নিশ্চিত করে যে এই বইয়ের সমস্ত উদাহরণ যা কম্পাইল হয়, তা রাস্টের নতুন ভার্সনগুলোর সাথেও কম্পাইল হতে থাকবে। ভার্সনভেদে আউটপুট সামান্য ভিন্ন হতে পারে কারণ রাস্ট প্রায়শই এরর মেসেজ এবং ওয়ার্নিং উন্নত করে। অর্থাৎ, এই পদক্ষেপগুলো ব্যবহার করে আপনি রাস্টের যেকোনো নতুন, স্টেবল ভার্সন ইনস্টল করলে তা এই বইয়ের বিষয়বস্তুর সাথে সঠিকভাবে কাজ করবে।
কমান্ড লাইন নোটেশন
এই অধ্যায়ে এবং পুরো বই জুড়ে, আমরা টার্মিনালে ব্যবহৃত কিছু কমান্ড দেখাব। যে লাইনগুলো আপনার টার্মিনালে প্রবেশ করানো উচিত, সেগুলোর সবই
$
দিয়ে শুরু হয়। আপনার$
চিহ্নটি টাইপ করার দরকার নেই; এটি কমান্ড লাইন প্রম্পট যা প্রতিটি কমান্ডের শুরু নির্দেশ করার জন্য দেখানো হয়। যে লাইনগুলো$
দিয়ে শুরু হয় না, সেগুলো সাধারণত আগের কমান্ডের আউটপুট দেখায়। অতিরিক্তভাবে, PowerShell-এর জন্য নির্দিষ্ট উদাহরণগুলোতে$
এর পরিবর্তে>
ব্যবহার করা হবে।
Linux বা macOS-এ rustup
ইনস্টল করা
আপনি যদি Linux বা macOS ব্যবহার করেন, তাহলে একটি টার্মিনাল খুলুন এবং নিম্নলিখিত কমান্ডটি প্রবেশ করান:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
এই কমান্ডটি একটি স্ক্রিপ্ট ডাউনলোড করে এবং rustup
টুলের ইনস্টলেশন শুরু করে, যা রাস্টের সর্বশেষ স্টেবল ভার্সন ইনস্টল করে। আপনাকে আপনার পাসওয়ার্ডের জন্য অনুরোধ করা হতে পারে। ইনস্টলেশন সফল হলে, নিম্নলিখিত লাইনটি প্রদর্শিত হবে:
Rust is installed now. Great!
আপনার একটি linker-এরও প্রয়োজন হবে, যা একটি প্রোগ্রাম এবং রাস্ট তার কম্পাইল করা আউটপুটগুলোকে একটি ফাইলে যুক্ত করতে এটি ব্যবহার করে। সম্ভবত আপনার কাছে ইতিমধ্যে একটি linker আছে। যদি আপনি linker errors পান, তবে আপনার একটি C compiler ইনস্টল করা উচিত, যার সাথে সাধারণত একটি linker অন্তর্ভুক্ত থাকে। একটি C compiler থাকা সুবিধাজনক কারণ কিছু সাধারণ রাস্ট প্যাকেজ C কোডের উপর নির্ভর করে এবং সেগুলোর জন্য একটি C compiler প্রয়োজন হবে।
macOS-এ, আপনি নিম্নলিখিত কমান্ডটি চালিয়ে একটি C compiler পেতে পারেন:
$ xcode-select --install
Linux ব্যবহারকারীদের সাধারণত তাদের ডিস্ট্রিবিউশনের ডকুমেন্টেশন অনুযায়ী GCC বা Clang ইনস্টল করা উচিত। উদাহরণস্বরূপ, আপনি যদি Ubuntu ব্যবহার করেন, তাহলে আপনি build-essential
প্যাকেজটি ইনস্টল করতে পারেন।
Windows-এ rustup
ইনস্টল করা
Windows-এ, https://www.rust-lang.org/tools/install এ যান এবং রাস্ট ইনস্টল করার জন্য নির্দেশাবলী অনুসরণ করুন। ইনস্টলেশনের এক পর্যায়ে, আপনাকে Visual Studio ইনস্টল করার জন্য অনুরোধ জানানো হবে। এটি একটি linker এবং প্রোগ্রাম কম্পাইল করার জন্য প্রয়োজনীয় নেটিভ লাইব্রেরি সরবরাহ করে। এই ধাপে আপনার যদি আরও সাহায্যের প্রয়োজন হয়, তাহলে https://rust-lang.github.io/rustup/installation/windows-msvc.html দেখুন।
এই বইয়ের বাকি অংশে এমন কমান্ড ব্যবহার করা হয়েছে যা cmd.exe এবং PowerShell উভয় ক্ষেত্রেই কাজ করে। যদি কোনো নির্দিষ্ট পার্থক্য থাকে, আমরা ব্যাখ্যা করব কোনটি ব্যবহার করতে হবে।
Troubleshooting
আপনার রাস্ট সঠিকভাবে ইনস্টল হয়েছে কিনা তা পরীক্ষা করতে, একটি শেল খুলুন এবং এই লাইনটি প্রবেশ করান:
$ rustc --version
আপনার সর্বশেষ প্রকাশিত স্টেবল ভার্সনের ভার্সন নম্বর, কমিট হ্যাশ এবং কমিট তারিখ নিম্নলিখিত ফরম্যাটে দেখতে পাওয়া উচিত:
rustc x.y.z (abcabcabc yyyy-mm-dd)
আপনি যদি এই তথ্য দেখতে পান, তাহলে আপনি সফলভাবে রাস্ট ইনস্টল করেছেন! যদি আপনি এই তথ্য দেখতে না পান, তাহলে রাস্ট আপনার %PATH%
সিস্টেম ভেরিয়েবলে আছে কিনা তা নিম্নরূপ পরীক্ষা করুন।
Windows CMD-তে, ব্যবহার করুন:
> echo %PATH%
PowerShell-এ, ব্যবহার করুন:
> echo $env:Path
Linux এবং macOS-এ, ব্যবহার করুন:
$ echo $PATH
যদি সবকিছু ঠিক থাকে এবং রাস্ট এখনও কাজ না করে, তবে সাহায্য পাওয়ার জন্য বেশ কয়েকটি জায়গা আছে। অন্যান্য রাস্টেশিয়ানদের (একটি মজার ডাকনাম যা আমরা নিজেদেরকে বলি) সাথে কীভাবে যোগাযোগ করবেন তা জানতে কমিউনিটি পেজ দেখুন।
আপডেট এবং আনইনস্টল করা
একবার rustup
এর মাধ্যমে রাস্ট ইনস্টল হয়ে গেলে, নতুন প্রকাশিত ভার্সনে আপডেট করা সহজ। আপনার শেল থেকে, নিম্নলিখিত আপডেট স্ক্রিপ্টটি চালান:
$ rustup update
রাস্ট এবং rustup
আনইনস্টল করতে, আপনার শেল থেকে নিম্নলিখিত আনইনস্টল স্ক্রিপ্টটি চালান:
$ rustup self uninstall
লোকাল ডকুমেন্টেশন
রাস্ট ইনস্টলেশনের সাথে ডকুমেন্টেশনের একটি লোকাল কপিও অন্তর্ভুক্ত থাকে যাতে আপনি এটি অফলাইনে পড়তে পারেন। আপনার ব্রাউজারে লোকাল ডকুমেন্টেশন খুলতে rustup doc
চালান।
যখনই স্ট্যান্ডার্ড লাইব্রেরি দ্বারা কোনো টাইপ বা ফাংশন সরবরাহ করা হয় এবং আপনি নিশ্চিত নন যে এটি কী করে বা কীভাবে এটি ব্যবহার করতে হয়, তখন তা জানতে অ্যাপ্লিকেশন প্রোগ্রামিং ইন্টারফেস (API) ডকুমেন্টেশন ব্যবহার করুন!
টেক্সট এডিটর এবং ইন্টিগ্রেটেড ডেভেলপমেন্ট এনভায়রনমেন্ট
এই বইটি আপনি রাস্ট কোড লেখার জন্য কোন টুল ব্যবহার করেন সে সম্পর্কে কোনো অনুমান করে না। প্রায় যেকোনো text editor দিয়েই কাজ চালানো যাবে! তবে, অনেক text editor এবং ইন্টিগ্রেটেড ডেভেলপমেন্ট এনভায়রনমেন্ট (IDE)-এর রাস্টের জন্য বিল্ট-ইন সাপোর্ট রয়েছে। আপনি সবসময় রাস্ট ওয়েবসাইটের টুলস পেজে অনেক এডিটর এবং IDE-এর একটি মোটামুটি আপ-টু-ডেট তালিকা খুঁজে পেতে পারেন।
এই বইটির সাথে অফলাইনে কাজ করা
বেশ কিছু উদাহরণে, আমরা স্ট্যান্ডার্ড লাইব্রেরির বাইরের রাস্ট প্যাকেজ ব্যবহার করব। সেই উদাহরণগুলো নিয়ে কাজ করার জন্য, আপনার হয় একটি ইন্টারনেট সংযোগ প্রয়োজন হবে অথবা সেই dependency-গুলো আগে থেকে ডাউনলোড করে রাখতে হবে। Dependency-গুলো আগে থেকে ডাউনলোড করতে, আপনি নিম্নলিখিত কমান্ডগুলো চালাতে পারেন। (আমরা cargo
কী এবং এই প্রতিটি কমান্ড কী করে তা পরে বিস্তারিতভাবে ব্যাখ্যা করব।)
$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add rand@0.8.5 trpl@0.2.0
এটি এই প্যাকেজগুলোর ডাউনলোড cache করবে যাতে আপনাকে পরে সেগুলো ডাউনলোড করতে না হয়। একবার আপনি এই কমান্ডটি চালালে, get-dependencies
ফোল্ডারটি রাখার দরকার নেই। আপনি যদি এই কমান্ডটি চালিয়ে থাকেন, তাহলে বইয়ের বাকি অংশে সমস্ত cargo
কমান্ডের সাথে --offline
ফ্ল্যাগ ব্যবহার করে নেটওয়ার্ক ব্যবহার করার চেষ্টার পরিবর্তে এই cache করা ভার্সনগুলো ব্যবহার করতে পারবেন।
Hello, world!
এখন যেহেতু আপনি রাস্ট ইনস্টল করেছেন, আপনার প্রথম রাস্ট প্রোগ্রাম লেখার সময় এসেছে। নতুন কোনো ভাষা শেখার সময় স্ক্রিনে Hello, world!
টেক্সট প্রিন্ট করে এমন একটি ছোট প্রোগ্রাম লেখা একটি ঐতিহ্য, তাই আমরাও এখানে তাই করব!
দ্রষ্টব্য: এই বইটিতে কমান্ড লাইনের সাথে প্রাথমিক পরিচিতি আছে বলে ধরে নেওয়া হয়েছে। রাস্ট আপনার এডিটিং, টুলিং বা আপনার কোড কোথায় থাকবে সে সম্পর্কে কোনো নির্দিষ্ট চাহিদা রাখে না, তাই আপনি যদি কমান্ড লাইনের পরিবর্তে একটি ইন্টিগ্রেটেড ডেভেলপমেন্ট এনভায়রনমেন্ট (IDE) ব্যবহার করতে পছন্দ করেন, তবে আপনার প্রিয় IDE ব্যবহার করতে পারেন। অনেক IDE-তেই এখন রাস্ট সাপোর্ট রয়েছে; বিস্তারিত জানতে IDE-এর ডকুমেন্টেশন দেখুন। রাস্ট টিম
rust-analyzer
-এর মাধ্যমে சிறந்த IDE সাপোর্ট সক্ষম করার দিকে মনোনিবেশ করেছে। আরও বিস্তারিত জানতে পরিশিষ্ট ডি দেখুন।
একটি প্রজেক্ট ডিরেক্টরি তৈরি করা
আপনি আপনার রাস্ট কোড সংরক্ষণ করার জন্য একটি ডিরেক্টরি তৈরি করে শুরু করবেন। আপনার কোড কোথায় থাকবে তা রাস্টের কাছে গুরুত্বপূর্ণ নয়, তবে এই বইয়ের অনুশীলন এবং প্রজেক্টগুলোর জন্য, আমরা আপনার হোম ডিরেক্টরিতে একটি projects ডিরেক্টরি তৈরি করার এবং আপনার সমস্ত প্রজেক্ট সেখানে রাখার পরামর্শ দিচ্ছি।
একটি টার্মিনাল খুলুন এবং একটি projects ডিরেক্টরি এবং তার মধ্যে "Hello, world!" প্রজেক্টের জন্য একটি ডিরেক্টরি তৈরি করতে নিম্নলিখিত কমান্ডগুলো প্রবেশ করান।
Linux, macOS এবং Windows-এর PowerShell-এর জন্য, এটি প্রবেশ করান:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
Windows CMD-এর জন্য, এটি প্রবেশ করান:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
একটি রাস্ট প্রোগ্রাম লেখা এবং চালানো
এরপর, একটি নতুন সোর্স ফাইল তৈরি করুন এবং এর নাম দিন main.rs। রাস্ট ফাইলগুলো সর্বদা .rs এক্সটেনশন দিয়ে শেষ হয়। আপনি যদি আপনার ফাইলের নামে একাধিক শব্দ ব্যবহার করেন, তবে সেগুলোকে আলাদা করার জন্য আন্ডারস্কোর ব্যবহার করার নিয়ম। উদাহরণস্বরূপ, helloworld.rs এর পরিবর্তে hello_world.rs ব্যবহার করুন।
এখন আপনি যে main.rs ফাইলটি তৈরি করেছেন সেটি খুলুন এবং লিস্টিং ১-১ এর কোডটি প্রবেশ করান।
fn main() { println!("Hello, world!"); }
ফাইলটি সংরক্ষণ করুন এবং ~/projects/hello_world ডিরেক্টরিতে আপনার টার্মিনাল উইন্ডোতে ফিরে যান। Linux বা macOS-এ, ফাইলটি কম্পাইল এবং রান করতে নিম্নলিখিত কমান্ডগুলো প্রবেশ করান:
$ rustc main.rs
$ ./main
Hello, world!
Windows-এ, ./main
এর পরিবর্তে .\main
কমান্ডটি প্রবেশ করান:
> rustc main.rs
> .\main
Hello, world!
আপনার অপারেটিং সিস্টেম নির্বিশেষে, Hello, world!
স্ট্রিংটি টার্মিনালে প্রিন্ট হওয়া উচিত। আপনি যদি এই আউটপুটটি না দেখেন, তাহলে সাহায্যের জন্য ইনস্টলেশন বিভাগের "Troubleshooting" অংশে ফিরে যান।
যদি Hello, world!
প্রিন্ট হয়ে থাকে, অভিনন্দন! আপনি আনুষ্ঠানিকভাবে একটি রাস্ট প্রোগ্রাম লিখেছেন। এটি আপনাকে একজন রাস্ট প্রোগ্রামার করে তুলেছে—স্বাগতম!
একটি রাস্ট প্রোগ্রামের অ্যানাটমি
চলুন এই "Hello, world!" প্রোগ্রামটি বিস্তারিতভাবে পর্যালোচনা করি। এখানে পাজলের প্রথম অংশ:
fn main() { }
এই লাইনগুলো main
নামে একটি ফাংশন সংজ্ঞায়িত করে। main
ফাংশনটি বিশেষ: এটি প্রতিটি এক্সিকিউটেবল রাস্ট প্রোগ্রামে সর্বদা প্রথম কোড যা রান হয়। এখানে, প্রথম লাইনটি main
নামে একটি ফাংশন ঘোষণা করে যার কোনো প্যারামিটার নেই এবং কিছুই রিটার্ন করে না। যদি প্যারামিটার থাকত, তবে সেগুলো ()
বন্ধনীর ভিতরে যেত।
ফাংশন বডি {}
দিয়ে মোড়ানো থাকে। রাস্টের সমস্ত ফাংশন বডির চারপাশে কার্লি ব্র্যাকেট প্রয়োজন। ফাংশন ঘোষণার একই লাইনে ওপেনিং কার্লি ব্র্যাকেট রাখা ভালো স্টাইল, এবং এর মধ্যে একটি স্পেস যোগ করা উচিত।
দ্রষ্টব্য: আপনি যদি রাস্ট প্রজেক্ট জুড়ে একটি স্ট্যান্ডার্ড স্টাইল মেনে চলতে চান, তবে আপনি আপনার কোডকে একটি নির্দিষ্ট স্টাইলে ফর্ম্যাট করার জন্য
rustfmt
নামক একটি স্বয়ংক্রিয় ফর্মাটার টুল ব্যবহার করতে পারেন (rustfmt
সম্পর্কে আরও জানতে পরিশিষ্ট ডি দেখুন)। রাস্ট টিম এই টুলটিকে স্ট্যান্ডার্ড রাস্ট ডিস্ট্রিবিউশনের সাথে অন্তর্ভুক্ত করেছে, যেমনrustc
, তাই এটি ইতিমধ্যে আপনার কম্পিউটারে ইনস্টল থাকা উচিত!
main
ফাংশনের বডিতে নিম্নলিখিত কোডটি রয়েছে:
#![allow(unused)] fn main() { println!("Hello, world!"); }
এই লাইনটি এই ছোট প্রোগ্রামের সমস্ত কাজ করে: এটি স্ক্রিনে টেক্সট প্রিন্ট করে। এখানে তিনটি গুরুত্বপূর্ণ বিষয় লক্ষ্য করার আছে।
প্রথমত, println!
একটি রাস্ট ম্যাক্রো কল করে। যদি এটি একটি ফাংশন কল করত, তবে এটি println
( !
ছাড়া) হিসাবে লেখা হতো। রাস্ট ম্যাক্রো হলো এমন কোড লেখার একটি উপায় যা রাস্ট সিনট্যাক্স প্রসারিত করার জন্য কোড তৈরি করে, এবং আমরা অধ্যায় ২০-এ এগুলি নিয়ে আরও বিস্তারিত আলোচনা করব। আপাতত, আপনার শুধু এটা জানা দরকার যে !
ব্যবহার করার অর্থ হল আপনি একটি সাধারণ ফাংশনের পরিবর্তে একটি ম্যাক্রো কল করছেন এবং ম্যাক্রোগুলো সবসময় ফাংশনের মতো একই নিয়ম অনুসরণ করে না।
দ্বিতীয়ত, আপনি "Hello, world!"
স্ট্রিংটি দেখতে পাচ্ছেন। আমরা এই স্ট্রিংটি println!
-এ একটি আর্গুমেন্ট হিসাবে পাস করি, এবং স্ট্রিংটি স্ক্রিনে প্রিন্ট হয়।
তৃতীয়ত, আমরা লাইনটি একটি সেমিকোলন (;
) দিয়ে শেষ করি, যা নির্দেশ করে যে এই এক্সপ্রেশনটি শেষ হয়েছে এবং পরবর্তীটি শুরু করার জন্য প্রস্তুত। রাস্ট কোডের বেশিরভাগ লাইন একটি সেমিকোলন দিয়ে শেষ হয়।
কম্পাইল এবং রান করা দুটি আলাদা ধাপ
আপনি এইমাত্র একটি নতুন তৈরি করা প্রোগ্রাম রান করেছেন, তাই চলুন প্রক্রিয়ার প্রতিটি ধাপ পরীক্ষা করে দেখি।
একটি রাস্ট প্রোগ্রাম রান করার আগে, আপনাকে অবশ্যই রাস্ট কম্পাইলার ব্যবহার করে এটি কম্পাইল করতে হবে, rustc
কমান্ড প্রবেশ করিয়ে এবং আপনার সোর্স ফাইলের নাম পাস করে, যেমন:
$ rustc main.rs
আপনার যদি C বা C++ ব্যাকগ্রাউন্ড থাকে, তাহলে আপনি লক্ষ্য করবেন যে এটি gcc
বা clang
-এর মতো। সফলভাবে কম্পাইল করার পর, রাস্ট একটি বাইনারি এক্সিকিউটেবল আউটপুট দেয়।
Linux, macOS এবং Windows-এর PowerShell-এ, আপনি আপনার শেলে ls
কমান্ড প্রবেশ করে এক্সিকিউটেবলটি দেখতে পারেন:
$ ls
main main.rs
Linux এবং macOS-এ, আপনি দুটি ফাইল দেখতে পাবেন। Windows-এর PowerShell-এ, আপনি CMD ব্যবহার করে যে তিনটি ফাইল দেখতে পেতেন, সেই একই তিনটি ফাইল দেখতে পাবেন। Windows-এর CMD-তে, আপনি নিম্নলিখিতটি প্রবেশ করাবেন:
> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs
এটি .rs এক্সটেনশন সহ সোর্স কোড ফাইল, এক্সিকিউটেবল ফাইল (Windows-এ main.exe, কিন্তু অন্য সব প্ল্যাটফর্মে main), এবং Windows ব্যবহার করার সময়, .pdb এক্সটেনশন সহ ডিবাগিং তথ্য সম্বলিত একটি ফাইল দেখায়। এখান থেকে, আপনি main বা main.exe ফাইলটি চালান, এইভাবে:
$ ./main # or .\main on Windows
যদি আপনার main.rs আপনার "Hello, world!" প্রোগ্রাম হয়, এই লাইনটি আপনার টার্মিনালে Hello, world!
প্রিন্ট করবে।
আপনি যদি রুবি, পাইথন বা জাভাস্ক্রিপ্টের মতো ডাইনামিক ল্যাঙ্গুয়েজের সাথে বেশি পরিচিত হন, তাহলে আপনি হয়তো একটি প্রোগ্রামকে আলাদা ধাপে কম্পাইল এবং রান করতে অভ্যস্ত নন। রাস্ট একটি ahead-of-time compiled ভাষা, যার মানে হল আপনি একটি প্রোগ্রাম কম্পাইল করে এক্সিকিউটেবলটি অন্য কাউকে দিতে পারেন, এবং তারা রাস্ট ইনস্টল না করেও এটি চালাতে পারবে। আপনি যদি কাউকে একটি .rb, .py, বা .js ফাইল দেন, তাদের যথাক্রমে রুবি, পাইথন বা জাভাস্ক্রিপ্ট ইমপ্লিমেন্টেশন ইনস্টল করা থাকতে হবে। কিন্তু সেই ভাষাগুলিতে, আপনার প্রোগ্রাম কম্পাইল এবং রান করার জন্য শুধুমাত্র একটি কমান্ড প্রয়োজন। ভাষার ডিজাইনে সবকিছুই একটি ট্রেড-অফ।
সাধারণ প্রোগ্রামগুলির জন্য শুধু rustc
দিয়ে কম্পাইল করাই ঠিক আছে, কিন্তু আপনার প্রজেক্ট বড় হওয়ার সাথে সাথে আপনি সমস্ত অপশন পরিচালনা করতে এবং আপনার কোড শেয়ার করা সহজ করতে চাইবেন। এরপর, আমরা আপনাকে কার্গো টুলের সাথে পরিচয় করিয়ে দেব, যা আপনাকে বাস্তব-বিশ্বের রাস্ট প্রোগ্রাম লিখতে সাহায্য করবে।
হ্যালো, কার্গো!
কার্গো হলো রাস্টের বিল্ড সিস্টেম এবং প্যাকেজ ম্যানেজার। বেশিরভাগ রাস্টেশিয়ান (Rustaceans) এই টুলটি তাদের রাস্ট প্রজেক্ট পরিচালনার জন্য ব্যবহার করেন কারণ কার্গো আপনার জন্য অনেক কাজ করে দেয়, যেমন আপনার কোড বিল্ড করা, আপনার কোডের উপর নির্ভরশীল লাইব্রেরিগুলো ডাউনলোড করা এবং সেই লাইব্রেরিগুলো বিল্ড করা। (আমরা আপনার কোডের প্রয়োজনীয় লাইব্রেরিগুলোকে dependencies বলি।)
সবচেয়ে সহজ রাস্ট প্রোগ্রামগুলোতে, যেমন আমরা এখন পর্যন্ত যেটি লিখেছি, কোনো dependencies থাকে না। যদি আমরা কার্গো দিয়ে "Hello, world!" প্রজেক্টটি তৈরি করতাম, তবে এটি কেবল কার্গোর সেই অংশটি ব্যবহার করত যা আপনার কোড বিল্ড করার কাজটি করে। যখন আপনি আরও জটিল রাস্ট প্রোগ্রাম লিখবেন, তখন আপনি dependencies যোগ করবেন, এবং যদি আপনি কার্গো ব্যবহার করে একটি প্রজেক্ট শুরু করেন, তবে dependencies যোগ করা অনেক সহজ হবে।
যেহেতু বিশাল সংখ্যক রাস্ট প্রজেক্ট কার্গো ব্যবহার করে, তাই এই বইয়ের বাকি অংশে ধরে নেওয়া হয়েছে যে আপনিও কার্গো ব্যবহার করছেন। আপনি যদি "Installation" বিভাগে আলোচিত অফিসিয়াল ইনস্টলারগুলো ব্যবহার করে থাকেন, তবে কার্গো রাস্টের সাথেই ইনস্টল হয়ে আসে। যদি আপনি অন্য কোনো উপায়ে রাস্ট ইনস্টল করে থাকেন, তবে আপনার টার্মিনালে নিম্নলিখিত কমান্ডটি প্রবেশ করিয়ে কার্গো ইনস্টল করা আছে কিনা তা পরীক্ষা করুন:
$ cargo --version
আপনি যদি একটি ভার্সন নম্বর দেখতে পান, তাহলে আপনার কাছে এটি আছে! যদি আপনি command not found
এর মতো কোনো এরর দেখতে পান, তাহলে কার্গো কীভাবে আলাদাভাবে ইনস্টল করতে হয় তা জানতে আপনার ইনস্টলেশন পদ্ধতির ডকুমেন্টেশন দেখুন।
কার্গো দিয়ে একটি প্রজেক্ট তৈরি করা
আসুন, কার্গো ব্যবহার করে একটি নতুন প্রজেক্ট তৈরি করি এবং দেখি এটি আমাদের মূল "Hello, world!" প্রজেক্ট থেকে কীভাবে আলাদা। আপনার projects ডিরেক্টরিতে (অথবা যেখানে আপনি আপনার কোড সংরক্ষণ করার সিদ্ধান্ত নিয়েছেন) ফিরে যান। তারপর, যেকোনো অপারেটিং সিস্টেমে, নিম্নলিখিত কমান্ড চালান:
$ cargo new hello_cargo
$ cd hello_cargo
প্রথম কমান্ডটি hello_cargo নামে একটি নতুন ডিরেক্টরি এবং প্রজেক্ট তৈরি করে। আমরা আমাদের প্রজেক্টের নাম দিয়েছি hello_cargo, এবং কার্গো একই নামের একটি ডিরেক্টরিতে এর ফাইলগুলো তৈরি করে।
hello_cargo ডিরেক্টরিতে যান এবং ফাইলগুলো তালিকাভুক্ত করুন। আপনি দেখবেন যে কার্গো আমাদের জন্য দুটি ফাইল এবং একটি ডিরেক্টরি তৈরি করেছে: একটি Cargo.toml ফাইল এবং একটি src ডিরেক্টরি যার ভিতরে একটি main.rs ফাইল রয়েছে।
এটি একটি .gitignore ফাইলের সাথে একটি নতুন গিট রিপোজিটরিও ইনিশিয়ালাইজ করেছে। আপনি যদি একটি বিদ্যমান গিট রিপোজিটরির ভিতরে cargo new
চালান তবে গিট ফাইল তৈরি হবে না; আপনি cargo new --vcs=git
ব্যবহার করে এই আচরণটি পরিবর্তন করতে পারেন।
দ্রষ্টব্য: গিট একটি সাধারণ ভার্সন কন্ট্রোল সিস্টেম। আপনি
--vcs
ফ্ল্যাগ ব্যবহার করেcargo new
কে একটি ভিন্ন ভার্সন কন্ট্রোল সিস্টেম বা কোনো ভার্সন কন্ট্রোল সিস্টেম ব্যবহার না করার জন্য পরিবর্তন করতে পারেন। উপলব্ধ বিকল্পগুলো দেখতেcargo new --help
চালান।
আপনার পছন্দের টেক্সট এডিটরে Cargo.toml ফাইলটি খুলুন। এটি লিস্টিং ১-২ এর কোডের মতো দেখতে হওয়া উচিত।
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]
এই ফাইলটি TOML (Tom’s Obvious, Minimal Language) ফরম্যাটে লেখা, যা কার্গোর কনফিগারেশন ফরম্যাট।
প্রথম লাইন, [package]
, একটি সেকশন হেডিং যা নির্দেশ করে যে নিম্নলিখিত স্টেটমেন্টগুলো একটি প্যাকেজ কনফিগার করছে। আমরা এই ফাইলে আরও তথ্য যোগ করার সাথে সাথে অন্যান্য সেকশন যোগ করব।
পরবর্তী তিনটি লাইন আপনার প্রোগ্রাম কম্পাইল করার জন্য কার্গোর প্রয়োজনীয় কনফিগারেশন তথ্য সেট করে: নাম, ভার্সন এবং রাস্টের এডিশন যা ব্যবহার করতে হবে। আমরা পরিশিষ্ট ই -তে edition
কী সম্পর্কে কথা বলব।
শেষ লাইন, [dependencies]
, আপনার প্রজেক্টের যেকোনো dependencies তালিকাভুক্ত করার জন্য একটি সেকশনের শুরু। রাস্ট-এ, কোডের প্যাকেজগুলোকে crates বলা হয়। এই প্রজেক্টের জন্য আমাদের অন্য কোনো ক্রেটসের প্রয়োজন হবে না, তবে অধ্যায় ২-এর প্রথম প্রজেক্টে আমাদের প্রয়োজন হবে, তাই আমরা তখন এই dependencies সেকশনটি ব্যবহার করব।
এখন src/main.rs খুলুন এবং একবার দেখুন:
ফাইলের নাম: src/main.rs
fn main() { println!("Hello, world!"); }
কার্গো আপনার জন্য একটি "Hello, world!" প্রোগ্রাম তৈরি করেছে, ঠিক যেমনটি আমরা লিস্টিং ১-১ এ লিখেছিলাম! এখন পর্যন্ত, আমাদের প্রজেক্ট এবং কার্গো দ্বারা তৈরি প্রজেক্টের মধ্যে পার্থক্য হলো কার্গো কোডটি src ডিরেক্টরিতে রেখেছে এবং আমাদের টপ ডিরেক্টরিতে একটি Cargo.toml কনফিগারেশন ফাইল রয়েছে।
কার্গো আশা করে যে আপনার সোর্স ফাইলগুলো src ডিরেক্টরির ভিতরে থাকবে। টপ-লেভেল প্রজেক্ট ডিরেক্টরিটি শুধুমাত্র README ফাইল, লাইসেন্সের তথ্য, কনফিগারেশন ফাইল এবং আপনার কোডের সাথে সম্পর্কিত নয় এমন অন্য কিছুর জন্য। কার্গো ব্যবহার করা আপনাকে আপনার প্রজেক্টগুলো সংগঠিত করতে সাহায্য করে। সবকিছুর জন্য একটি জায়গা আছে, এবং সবকিছু তার জায়গায় আছে।
আপনি যদি এমন একটি প্রজেক্ট শুরু করেন যা কার্গো ব্যবহার করে না, যেমন আমরা "Hello, world!" প্রজেক্টের সাথে করেছি, আপনি এটিকে এমন একটি প্রজেক্টে রূপান্তর করতে পারেন যা কার্গো ব্যবহার করে। প্রজেক্ট কোডটি src ডিরেক্টরিতে সরান এবং একটি উপযুক্ত Cargo.toml ফাইল তৈরি করুন। সেই Cargo.toml ফাইলটি পাওয়ার একটি সহজ উপায় হল cargo init
চালানো, যা আপনার জন্য এটি স্বয়ংক্রিয়ভাবে তৈরি করবে।
একটি কার্গো প্রজেক্ট বিল্ড এবং রান করা
এখন চলুন দেখি কার্গো দিয়ে "Hello, world!" প্রোগ্রামটি বিল্ড এবং রান করার সময় কী ভিন্ন হয়! আপনার hello_cargo ডিরেক্টরি থেকে, নিম্নলিখিত কমান্ডটি প্রবেশ করে আপনার প্রজেক্টটি বিল্ড করুন:
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
এই কমান্ডটি আপনার বর্তমান ডিরেক্টরির পরিবর্তে target/debug/hello_cargo (অথবা Windows-এ target\debug\hello_cargo.exe) তে একটি এক্সিকিউটেবল ফাইল তৈরি করে। যেহেতু ডিফল্ট বিল্ডটি একটি ডিবাগ বিল্ড, কার্গো বাইনারিটিকে debug নামের একটি ডিরেক্টরিতে রাখে। আপনি এই কমান্ড দিয়ে এক্সিকিউটেবলটি চালাতে পারেন:
$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
Hello, world!
যদি সবকিছু ঠিকঠাক চলে, Hello, world!
টার্মিনালে প্রিন্ট হওয়া উচিত। প্রথমবার cargo build
চালানোর ফলে কার্গো টপ লেভেলে একটি নতুন ফাইল তৈরি করে: Cargo.lock। এই ফাইলটি আপনার প্রজেক্টের dependencies-এর সঠিক ভার্সনগুলোর ট্র্যাক রাখে। এই প্রজেক্টের কোনো dependencies নেই, তাই ফাইলটি কিছুটা ফাঁকা। আপনাকে এই ফাইলটি ম্যানুয়ালি পরিবর্তন করার প্রয়োজন হবে না; কার্গো আপনার জন্য এর বিষয়বস্তু পরিচালনা করে।
আমরা এইমাত্র cargo build
দিয়ে একটি প্রজেক্ট বিল্ড করেছি এবং ./target/debug/hello_cargo
দিয়ে এটি রান করেছি, কিন্তু আমরা cargo run
ব্যবহার করে কোডটি কম্পাইল করে এবং তারপর ফলাফল এক্সিকিউটেবলটি এক কমান্ডে চালাতে পারি:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
cargo build
চালানো এবং তারপর বাইনারির পুরো পাথ ব্যবহার করার কথা মনে রাখার চেয়ে cargo run
ব্যবহার করা বেশি সুবিধাজনক, তাই বেশিরভাগ ডেভেলপার cargo run
ব্যবহার করেন।
লক্ষ্য করুন যে এবার আমরা কার্গো hello_cargo
কম্পাইল করছে এমন কোনো আউটপুট দেখিনি। কার্গো বুঝতে পেরেছে যে ফাইলগুলো পরিবর্তন হয়নি, তাই এটি পুনরায় বিল্ড না করে শুধু বাইনারিটি চালিয়েছে। আপনি যদি আপনার সোর্স কোড পরিবর্তন করতেন, কার্গো প্রজেক্টটি চালানোর আগে পুনরায় বিল্ড করত, এবং আপনি এই আউটপুটটি দেখতে পেতেন:
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
Running `target/debug/hello_cargo`
Hello, world!
কার্গো cargo check
নামে একটি কমান্ডও সরবরাহ করে। এই কমান্ডটি দ্রুত আপনার কোড পরীক্ষা করে নিশ্চিত করে যে এটি কম্পাইল হয় কিন্তু একটি এক্সিকিউটেবল তৈরি করে না:
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
আপনি কেন একটি এক্সিকিউটেবল চাইবেন না? প্রায়শই, cargo check
cargo build
এর চেয়ে অনেক দ্রুত হয় কারণ এটি একটি এক্সিকিউটেবল তৈরির ধাপটি এড়িয়ে যায়। আপনি যদি কোড লেখার সময় ক্রমাগত আপনার কাজ পরীক্ষা করতে থাকেন, তবে cargo check
ব্যবহার করা আপনার প্রজেক্ট এখনও কম্পাইল হচ্ছে কিনা তা জানানোর প্রক্রিয়াটিকে দ্রুততর করবে! এই কারণে, অনেক রাস্টেশিয়ান তাদের প্রোগ্রাম লেখার সময় পর্যায়ক্রমে cargo check
চালান যাতে এটি কম্পাইল হয় তা নিশ্চিত করা যায়। তারপর তারা যখন এক্সিকিউটেবল ব্যবহার করার জন্য প্রস্তুত হয় তখন cargo build
চালান।
আসুন আমরা এখন পর্যন্ত কার্গো সম্পর্কে যা শিখেছি তার সারসংক্ষেপ করি:
- আমরা
cargo new
ব্যবহার করে একটি প্রজেক্ট তৈরি করতে পারি। - আমরা
cargo build
ব্যবহার করে একটি প্রজেক্ট বিল্ড করতে পারি। - আমরা
cargo run
ব্যবহার করে এক ধাপে একটি প্রজেক্ট বিল্ড এবং রান করতে পারি। - আমরা
cargo check
ব্যবহার করে এরর পরীক্ষা করার জন্য বাইনারি তৈরি না করে একটি প্রজেক্ট বিল্ড করতে পারি। - বিল্ডের ফলাফল আমাদের কোডের একই ডিরেক্টরিতে সংরক্ষণ করার পরিবর্তে, কার্গো এটি target/debug ডিরেক্টরিতে সংরক্ষণ করে।
কার্গো ব্যবহারের একটি অতিরিক্ত সুবিধা হলো, আপনি যে অপারেটিং সিস্টেমেই কাজ করুন না কেন কমান্ডগুলো একই থাকে। তাই, এই মুহূর্তে থেকে, আমরা আর লিনাক্স এবং ম্যাকওএস বনাম উইন্ডোজের জন্য নির্দিষ্ট নির্দেশনা প্রদান করব না।
রিলিজের জন্য বিল্ড করা
যখন আপনার প্রজেক্ট অবশেষে রিলিজের জন্য প্রস্তুত হবে, আপনি অপটিমাইজেশন সহ এটি কম্পাইল করতে cargo build --release
ব্যবহার করতে পারেন। এই কমান্ডটি target/debug এর পরিবর্তে target/release-এ একটি এক্সিকিউটেবল তৈরি করবে। অপটিমাইজেশনগুলো আপনার রাস্ট কোডকে দ্রুত চালাতে সাহায্য করে, কিন্তু সেগুলো চালু করলে আপনার প্রোগ্রাম কম্পাইল হতে বেশি সময় লাগে। এই কারণেই দুটি ভিন্ন প্রোফাইল রয়েছে: একটি ডেভেলপমেন্টের জন্য, যখন আপনি দ্রুত এবং প্রায়শই পুনর্নির্মাণ করতে চান, এবং অন্যটি চূড়ান্ত প্রোগ্রাম তৈরির জন্য যা আপনি একজন ব্যবহারকারীকে দেবেন যা বারবার পুনর্নির্মাণ করা হবে না এবং যা যত দ্রুত সম্ভব চলবে। আপনি যদি আপনার কোডের চলার সময় বেঞ্চমার্কিং করেন, তবে cargo build --release
চালাতে এবং target/release-এর এক্সিকিউটেবল দিয়ে বেঞ্চমার্ক করতে ভুলবেন না।
কার্গো একটি কনভেনশন হিসাবে
সহজ প্রজেক্টগুলোর ক্ষেত্রে, কার্গো শুধু rustc
ব্যবহারের চেয়ে খুব বেশি সুবিধা দেয় না, কিন্তু আপনার প্রোগ্রামগুলো আরও জটিল হওয়ার সাথে সাথে এটি তার যোগ্যতা প্রমাণ করবে। যখন প্রোগ্রাম একাধিক ফাইলে বিস্তৃত হয় বা কোনো dependency-র প্রয়োজন হয়, তখন কার্গোকে বিল্ড সমন্বয় করতে দেওয়া অনেক সহজ।
যদিও hello_cargo
প্রজেক্টটি সহজ, এটি এখন আপনার বাকি রাস্ট ক্যারিয়ারে ব্যবহার করার মতো অনেক আসল টুলিং ব্যবহার করে। প্রকৃতপক্ষে, যেকোনো বিদ্যমান প্রজেক্টে কাজ করার জন্য, আপনি গিট ব্যবহার করে কোড চেক আউট করতে, সেই প্রজেক্টের ডিরেক্টরিতে পরিবর্তন করতে এবং বিল্ড করতে নিম্নলিখিত কমান্ডগুলো ব্যবহার করতে পারেন:
$ git clone example.org/someproject
$ cd someproject
$ cargo build
কার্গো সম্পর্কে আরও তথ্যের জন্য, এর ডকুমেন্টেশন দেখুন।
সারসংক্ষেপ
আপনি ইতিমধ্যে আপনার রাস্ট যাত্রায় একটি দুর্দান্ত শুরু করেছেন! এই অধ্যায়ে, আপনি শিখেছেন কীভাবে:
rustup
ব্যবহার করে রাস্টের সর্বশেষ স্টেবল ভার্সন ইনস্টল করতে হয়- একটি নতুন রাস্ট ভার্সনে আপডেট করতে হয়
- স্থানীয়ভাবে ইনস্টল করা ডকুমেন্টেশন খুলতে হয়
rustc
সরাসরি ব্যবহার করে একটি "Hello, world!" প্রোগ্রাম লিখতে এবং চালাতে হয়- কার্গোর কনভেনশন ব্যবহার করে একটি নতুন প্রজেক্ট তৈরি এবং চালাতে হয়
রাস্ট কোড পড়া এবং লেখাতে অভ্যস্ত হওয়ার জন্য আরও একটি গুরুত্বপূর্ণ প্রোগ্রাম তৈরি করার জন্য এটি একটি দুর্দান্ত সময়। তাই, অধ্যায় ২-এ, আমরা একটি অনুমান করার খেলার প্রোগ্রাম তৈরি করব। আপনি যদি রাস্ট-এ সাধারণ প্রোগ্রামিং ধারণাগুলো কীভাবে কাজ করে তা শেখার মাধ্যমে শুরু করতে চান, তবে অধ্যায় ৩ দেখুন এবং তারপর অধ্যায় ২-এ ফিরে আসুন।
অনুমান করার গেম প্রোগ্রামিং
চলুন, একটি বাস্তব প্রজেক্টের মাধ্যমে রাস্টের জগতে ঝাঁপিয়ে পড়া যাক! এই অধ্যায়ে আমরা কিছু সাধারণ রাস্ট কনসেপ্টের সাথে পরিচিত হব এবং দেখব কীভাবে একটি বাস্তব প্রোগ্রামে সেগুলো ব্যবহার করা যায়। আপনি let
, match
, মেথড, অ্যাসোসিয়েটেড ফাংশন, এক্সটার্নাল ক্রেইট এবং আরও অনেক কিছু সম্পর্কে জানতে পারবেন! পরবর্তী অধ্যায়গুলোতে আমরা এই ধারণাগুলো আরও বিস্তারিতভাবে আলোচনা করব। এই অধ্যায়ে, আপনি শুধু মৌলিক বিষয়গুলো অনুশীলন করবেন।
আমরা একটি ক্লাসিক প্রোগ্রামিং সমস্যা—অনুমান করার গেম (guessing game)—তৈরি করব। এটি যেভাবে কাজ করবে তা হলো: প্রোগ্রামটি ১ থেকে ১০০ এর মধ্যে একটি র্যান্ডম পূর্ণসংখ্যা (integer) তৈরি করবে। এরপর এটি প্লেয়ারকে একটি অনুমান প্রবেশ করানোর জন্য বলবে। একটি অনুমান প্রবেশ করানোর পর, প্রোগ্রামটি জানাবে যে অনুমানটি খুব কম নাকি খুব বেশি। যদি অনুমান সঠিক হয়, গেমটি একটি অভিনন্দন বার্তা প্রিন্ট করে খেলা শেষ করে দেবে।
নতুন প্রজেক্ট সেটআপ করা
একটি নতুন প্রজেক্ট সেটআপ করতে, আপনি অধ্যায় ১-এ যে projects ডিরেক্টরি তৈরি করেছিলেন সেখানে যান এবং কার্গো ব্যবহার করে একটি নতুন প্রজেক্ট তৈরি করুন, যেমন:
$ cargo new guessing_game
$ cd guessing_game
প্রথম কমান্ড, cargo new
, প্রজেক্টের নাম (guessing_game
) প্রথম আর্গুমেন্ট হিসেবে নেয়। দ্বিতীয় কমান্ডটি নতুন প্রজেক্টের ডিরেক্টরিতে পরিবর্তন করে।
তৈরি হওয়া Cargo.toml ফাইলটি দেখুন:
ফাইলের নাম: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
যেমনটি আপনি অধ্যায় ১-এ দেখেছেন, cargo new
আপনার জন্য একটি "Hello, world!" প্রোগ্রাম তৈরি করে। src/main.rs ফাইলটি দেখুন:
ফাইলের নাম: 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 `target/debug/guessing_game`
Hello, world!
যখন কোনো প্রজেক্টে দ্রুত পরিবর্তন ও পরীক্ষা করার প্রয়োজন হয়, তখন run
কমান্ডটি খুব কাজে আসে, যেমনটা আমরা এই গেমে করব—প্রতিটি ধাপ দ্রুত পরীক্ষা করে পরবর্তী ধাপে এগিয়ে যাব।
src/main.rs ফাইলটি আবার খুলুন। আপনি সমস্ত কোড এই ফাইলেই লিখবেন।
একটি অনুমান প্রসেস করা
অনুমান করার গেম প্রোগ্রামের প্রথম অংশে ব্যবহারকারীর কাছ থেকে ইনপুট চাওয়া হবে, সেই ইনপুট প্রসেস করা হবে এবং ইনপুটটি প্রত্যাশিত বিন্যাসে আছে কিনা তা পরীক্ষা করা হবে। শুরু করার জন্য, আমরা প্লেয়ারকে একটি অনুমান ইনপুট করার সুযোগ দেব। লিস্টিং ২-১ এর কোডটি 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}");
}
ডিফল্টরূপে, রাস্ট স্ট্যান্ডার্ড লাইব্রেরিতে কিছু আইটেম সংজ্ঞায়িত করে রাখে যা প্রতিটি প্রোগ্রামের স্কোপে নিয়ে আসা হয়। এই সেটটিকে 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}");
}
এই কোডটি একটি প্রম্পট প্রিন্ট করছে যা গেমটি কী তা জানাচ্ছে এবং ব্যবহারকারীর কাছ থেকে ইনপুট চাইছে।
ভ্যারিয়েবলের মাধ্যমে মান সংরক্ষণ করা
এরপর, আমরা ব্যবহারকারীর ইনপুট সংরক্ষণের জন্য একটি variable তৈরি করব, এভাবে:
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 মানের সাথে বাইন্ড করে। রাস্ট-এ, ভ্যারিয়েবলগুলো ডিফল্টরূপে অপরিবর্তনীয় (immutable), যার মানে একবার আমরা ভ্যারিয়েবলে একটি মান দিলে, সেই মান আর পরিবর্তন হবে না। আমরা এই ধারণাটি অধ্যায় ৩-এর "Variables and Mutability" বিভাগে বিস্তারিতভাবে আলোচনা করব। একটি ভ্যারিয়েবলকে পরিবর্তনযোগ্য (mutable) করতে, আমরা ভ্যারিয়েবলের নামের আগে mut
যোগ করি:
let apples = 5; // immutable
let mut bananas = 5; // mutable
দ্রষ্টব্য:
//
সিনট্যাক্স একটি কমেন্ট শুরু করে যা লাইনের শেষ পর্যন্ত চলে। রাস্ট কমেন্টের মধ্যে থাকা সবকিছু উপেক্ষা করে। আমরা অধ্যায় ৩-এ কমেন্ট সম্পর্কে আরও বিস্তারিত আলোচনা করব।
guessing game প্রোগ্রামে ফিরে আসা যাক, আপনি এখন জানেন যে let mut guess
একটি পরিবর্তনযোগ্য ভ্যারিয়েবল guess
তৈরি করবে। সমান চিহ্ন (=
) রাস্টকে বলে যে আমরা এখন ভ্যারিয়েবলের সাথে কিছু একটা বাইন্ড করতে চাই। সমান চিহ্নের ডানদিকে guess
-এর মান রয়েছে, যা String::new
কল করার ফলাফল, এটি একটি ফাংশন যা String
-এর একটি নতুন ইনস্ট্যান্স প্রদান করে। String
হল একটি স্ট্রিং টাইপ যা স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা হয় এবং এটি একটি প্রসারণযোগ্য, UTF-8 এনকোডেড টেক্সট।
::new
লাইনে ::
সিনট্যাক্সটি নির্দেশ করে যে new
হল String
টাইপের একটি associated function। একটি associated function হলো এমন একটি ফাংশন যা একটি টাইপের উপর ইমপ্লিমেন্ট করা হয়, এই ক্ষেত্রে String
। এই new
ফাংশনটি একটি নতুন, খালি স্ট্রিং তৈরি করে। আপনি অনেক টাইপের উপরেই একটি new
ফাংশন খুঁজে পাবেন কারণ এটি কোনো কিছুর নতুন মান তৈরি করার জন্য একটি সাধারণ নাম।
পুরো কথায়, let mut guess = String::new();
লাইনটি একটি পরিবর্তনযোগ্য ভ্যারিয়েবল তৈরি করেছে যা বর্তমানে একটি String
-এর নতুন, খালি ইনস্ট্যান্সের সাথে বাইন্ড করা আছে। যাক!
ব্যবহারকারীর ইনপুট গ্রহণ করা
স্মরণ করুন যে আমরা প্রোগ্রামের প্রথম লাইনে 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
মেথড কল করে। আমরা &mut guess
কে read_line
এর আর্গুমেন্ট হিসেবে পাস করছি, যাতে এটি জানতে পারে ব্যবহারকারীর ইনপুট কোন স্ট্রিং-এ সংরক্ষণ করতে হবে। read_line
-এর পুরো কাজটি হলো ব্যবহারকারী স্ট্যান্ডার্ড ইনপুটে যা টাইপ করে তা একটি স্ট্রিং-এ যুক্ত করা (এর বিষয়বস্তু ওভাররাইট না করে), তাই আমরা সেই স্ট্রিংটিকে একটি আর্গুমেন্ট হিসাবে পাস করি। স্ট্রিং আর্গুমেন্টটি পরিবর্তনযোগ্য হতে হবে যাতে মেথডটি স্ট্রিং এর বিষয়বস্তু পরিবর্তন করতে পারে।
&
চিহ্নটি নির্দেশ করে যে এই আর্গুমেন্টটি একটি reference, যা আপনার কোডের একাধিক অংশকে মেমরিতে একাধিকবার ডেটা কপি না করে একই ডেটা অ্যাক্সেস করার একটি উপায় দেয়। Reference একটি জটিল বৈশিষ্ট্য, এবং রাস্টের অন্যতম প্রধান সুবিধা হল reference ব্যবহার করা কতটা নিরাপদ এবং সহজ। এই প্রোগ্রামটি শেষ করার জন্য আপনাকে সেই সব বিস্তারিত জানতে হবে না। আপাতত, আপনাকে শুধু জানতে হবে যে, ভ্যারিয়েবলের মতো, reference-ও ডিফল্টরূপে অপরিবর্তনীয়। তাই, এটিকে পরিবর্তনযোগ্য করার জন্য আপনাকে &guess
এর পরিবর্তে &mut guess
লিখতে হবে। (অধ্যায় ৪ reference সম্পর্কে আরও বিস্তারিত ব্যাখ্যা করবে।)
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 বলা হয়, যা এমন একটি টাইপ যা একাধিক সম্ভাব্য অবস্থায় থাকতে পারে। আমরা প্রতিটি সম্ভাব্য অবস্থাকে একটি variant বলি।
অধ্যায় ৬ এ enum সম্পর্কে আরও বিস্তারিত আলোচনা করা হবে। এই Result
টাইপগুলোর উদ্দেশ্য হল ত্রুটি-হ্যান্ডলিং তথ্য এনকোড করা।
Result
-এর variant-গুলো হল Ok
এবং Err
। Ok
variant নির্দেশ করে যে অপারেশনটি সফল হয়েছে, এবং এতে সফলভাবে তৈরি হওয়া মানটি থাকে। Err
variant মানে অপারেশনটি ব্যর্থ হয়েছে, এবং এতে অপারেশনটি কীভাবে বা কেন ব্যর্থ হয়েছে সে সম্পর্কে তথ্য থাকে।
যেকোনো টাইপের মানের মতোই, 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
রাস্ট সতর্ক করে যে আপনি read_line
থেকে প্রাপ্ত Result
মানটি ব্যবহার করেননি, যা নির্দেশ করে যে প্রোগ্রামটি একটি সম্ভাব্য ত্রুটি পরিচালনা করেনি।
সতর্কবার্তাটি দূর করার সঠিক উপায় হলো আসলে ত্রুটি-হ্যান্ডলিং কোড লেখা, কিন্তু আমাদের ক্ষেত্রে আমরা শুধু চাই যে কোনো সমস্যা হলে প্রোগ্রামটি ক্র্যাশ করুক, তাই আমরা expect
ব্যবহার করতে পারি। আপনি ত্রুটি থেকে পুনরুদ্ধার সম্পর্কে অধ্যায় ৯ এ শিখবেন।
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}");
}
এই লাইনটি সেই স্ট্রিংটি প্রিন্ট করে যা এখন ব্যবহারকারীর ইনপুট ধারণ করে। {}
কার্লি ব্র্যাকেটের সেট একটি প্লেসহোল্ডার: {}
-কে ছোট কাঁকড়ার চিমটার মতো ভাবুন যা একটি মানকে ধরে রাখে। একটি ভ্যারিয়েবলের মান প্রিন্ট করার সময়, ভ্যারিয়েবলের নামটি কার্লি ব্র্যাকেটের ভিতরে যেতে পারে। একটি এক্সপ্রেশন মূল্যায়ন করার ফলাফল প্রিন্ট করার সময়, ফরম্যাট স্ট্রিং-এ খালি কার্লি ব্র্যাকেট রাখুন, তারপর ফরম্যাট স্ট্রিং এর পরে একটি কমা-বিভক্ত এক্সপ্রেশনের তালিকা দিন যা প্রতিটি খালি কার্লি ব্র্যাকেট প্লেসহোল্ডারে একই ক্রমে প্রিন্ট হবে। একটি 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
প্রিন্ট করবে।
প্রথম অংশ পরীক্ষা করা
চলুন guessing game-এর প্রথম অংশটি পরীক্ষা করি। এটি 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
এই মুহূর্তে, গেমের প্রথম অংশটি সম্পন্ন হয়েছে: আমরা কীবোর্ড থেকে ইনপুট পাচ্ছি এবং তারপর তা প্রিন্ট করছি।
একটি গোপন সংখ্যা তৈরি করা
এরপর, আমাদের একটি গোপন সংখ্যা তৈরি করতে হবে যা ব্যবহারকারী অনুমান করার চেষ্টা করবে। গোপন সংখ্যাটি প্রতিবার ভিন্ন হওয়া উচিত যাতে গেমটি একাধিকবার খেলতে মজা লাগে। আমরা ১ থেকে ১০০ এর মধ্যে একটি র্যান্ডম সংখ্যা ব্যবহার করব যাতে গেমটি খুব বেশি কঠিন না হয়। রাস্ট এখনও তার স্ট্যান্ডার্ড লাইব্রেরিতে র্যান্ডম সংখ্যা তৈরির কার্যকারিতা অন্তর্ভুক্ত করেনি। তবে, রাস্ট টিম একটি rand
crate সরবরাহ করে যেখানে এই কার্যকারিতা রয়েছে।
আরও কার্যকারিতা পেতে একটি Crate ব্যবহার করা
মনে রাখবেন যে একটি crate হলো রাস্ট সোর্স কোড ফাইলের একটি সংগ্রহ। আমরা যে প্রজেক্টটি তৈরি করছি তা একটি binary crate, যা একটি এক্সিকিউটেবল। rand
crate একটি library crate, যা এমন কোড ধারণ করে যা অন্য প্রোগ্রামে ব্যবহারের উদ্দেশ্যে তৈরি এবং এটি নিজে থেকে এক্সিকিউট করা যায় না।
কার্গোর এক্সটার্নাল crate সমন্বয় করার ক্ষমতা এখানেই কার্গোর আসল শক্তি প্রকাশ পায়। rand
ব্যবহার করে কোড লেখার আগে, আমাদের Cargo.toml ফাইলটি পরিবর্তন করে rand
crate-কে একটি ডিপেন্ডেন্সি হিসেবে অন্তর্ভুক্ত করতে হবে। ফাইলটি এখন খুলুন এবং [dependencies]
সেকশন হেডারের নিচে নিম্নলিখিত লাইনটি যোগ করুন যা কার্গো আপনার জন্য তৈরি করেছে। নিশ্চিত করুন যে আপনি rand
-কে ঠিক যেমনভাবে আমরা এখানে দিয়েছি, এই ভার্সন নম্বর সহ নির্দিষ্ট করেছেন, অন্যথায় এই টিউটোরিয়ালের কোড উদাহরণগুলো কাজ নাও করতে পারে:
ফাইলের নাম: Cargo.toml
[dependencies]
rand = "0.8.5"
Cargo.toml ফাইলে, একটি হেডারের পরে যা কিছু থাকে তা সেই সেকশনের অংশ যতক্ষণ না অন্য একটি সেকশন শুরু হয়। [dependencies]
-তে আপনি কার্গোকে বলেন আপনার প্রজেক্ট কোন এক্সটার্নাল crate-গুলির উপর নির্ভর করে এবং সেই crate-গুলির কোন ভার্সন আপনার প্রয়োজন। এক্ষেত্রে, আমরা rand
crate-কে সেমান্টিক ভার্সন স্পেসিফায়ার 0.8.5
দিয়ে নির্দিষ্ট করছি। কার্গো Semantic Versioning (কখনও কখনও SemVer বলা হয়) বোঝে, যা ভার্সন নম্বর লেখার একটি স্ট্যান্ডার্ড। 0.8.5
স্পেসিফায়ারটি আসলে ^0.8.5
-এর একটি সংক্ষিপ্ত রূপ, যার মানে হল এমন যেকোনো ভার্সন যা অন্তত 0.8.5 কিন্তু 0.9.0 এর নিচে।
কার্গো এই ভার্সনগুলোকে 0.8.5 ভার্সনের সাথে সামঞ্জস্যপূর্ণ পাবলিক API আছে বলে মনে করে, এবং এই স্পেসিফিকেশন নিশ্চিত করে যে আপনি সর্বশেষ প্যাচ রিলিজ পাবেন যা এখনও এই অধ্যায়ের কোডের সাথে কম্পাইল হবে। 0.9.0 বা তার বেশি যেকোনো ভার্সনের একই API থাকবে এমন কোনো নিশ্চয়তা নেই যা নিম্নলিখিত উদাহরণগুলিতে ব্যবহৃত হয়েছে।
এখন, কোডের কোনো পরিবর্তন না করে, চলুন প্রজেক্টটি বিল্ড করি, যেমনটি লিস্টিং ২-২-এ দেখানো হয়েছে।
$ 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```
</Listing>
আপনি ভিন্ন ভার্সন নম্বর দেখতে পারেন (কিন্তু SemVer-এর কারণে সেগুলি সব কোডের সাথে সামঞ্জস্যপূর্ণ হবে) এবং ভিন্ন লাইন দেখতে পারেন (অপারেটিং সিস্টেমের উপর নির্ভর করে), এবং লাইনগুলি ভিন্ন ক্রমে থাকতে পারে।
যখন আমরা একটি এক্সটার্নাল ডিপেন্ডেন্সি অন্তর্ভুক্ত করি, কার্গো সেই ডিপেন্ডেন্সির জন্য প্রয়োজনীয় সমস্ত কিছুর সর্বশেষ ভার্সন _registry_ থেকে নিয়ে আসে, যা [Crates.io][cratesio] থেকে ডেটার একটি কপি। Crates.io হল সেই জায়গা যেখানে রাস্ট ইকোসিস্টেমের মানুষজন তাদের ওপেন সোর্স রাস্ট প্রজেক্টগুলো অন্যদের ব্যবহারের জন্য পোস্ট করে।
রেজিস্ট্রি আপডেট করার পরে, কার্গো `[dependencies]` সেকশন পরীক্ষা করে এবং তালিকাভুক্ত যেকোনো crate যা এখনও ডাউনলোড করা হয়নি তা ডাউনলোড করে। এই ক্ষেত্রে, যদিও আমরা কেবল `rand`-কে একটি ডিপেন্ডেন্সি হিসেবে তালিকাভুক্ত করেছি, কার্গো `rand`-এর কাজ করার জন্য প্রয়োজনীয় অন্যান্য crate-গুলিও নিয়ে এসেছে। crate-গুলি ডাউনলোড করার পরে, রাস্ট সেগুলি কম্পাইল করে এবং তারপর ডিপেন্ডেন্সি সহ প্রজেক্টটি কম্পাইল করে।
আপনি যদি কোনো পরিবর্তন না করে অবিলম্বে আবার `cargo build` চালান, আপনি `Finished` লাইন ছাড়া আর কোনো আউটপুট পাবেন না। কার্গো জানে যে এটি ইতিমধ্যে ডিপেন্ডেন্সিগুলি ডাউনলোড এবং কম্পাইল করেছে, এবং আপনি আপনার _Cargo.toml_ ফাইলে সেগুলি সম্পর্কে কিছু পরিবর্তন করেননি। কার্গো এটাও জানে যে আপনি আপনার কোড সম্পর্কে কিছু পরিবর্তন করেননি, তাই এটি সেটাও পুনরায় কম্পাইল করে না। করার মতো কিছু না থাকায়, এটি কেবল প্রস্থান করে।
আপনি যদি _src/main.rs_ ফাইলটি খোলেন, একটি তুচ্ছ পরিবর্তন করেন, এবং তারপর এটি সংরক্ষণ করে আবার বিল্ড করেন, আপনি কেবল দুটি আউটপুট লাইন দেখতে পাবেন:
<!-- manual-regeneration
cd listings/ch02-guessing-game-tutorial/listing-02-02/
touch src/main.rs
cargo build -->
```console
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
এই লাইনগুলি দেখায় যে কার্গো কেবল আপনার src/main.rs ফাইলের ক্ষুদ্র পরিবর্তনের সাথে বিল্ড আপডেট করে। আপনার ডিপেন্ডেন্সিগুলি পরিবর্তিত হয়নি, তাই কার্গো জানে যে এটি সেগুলির জন্য যা ইতিমধ্যে ডাউনলোড এবং কম্পাইল করেছে তা পুনরায় ব্যবহার করতে পারে।
Cargo.lock ফাইলের মাধ্যমে পুনরুৎপাদনযোগ্য বিল্ড নিশ্চিত করা
কার্গোর একটি মেকানিজম রয়েছে যা নিশ্চিত করে যে আপনি বা অন্য কেউ আপনার কোড বিল্ড করার সময় প্রতিবার একই আর্টিফ্যাক্ট পুনর্নির্মাণ করতে পারবেন: কার্গো কেবল আপনার নির্দিষ্ট করা ডিপেন্ডেন্সিগুলির ভার্সন ব্যবহার করবে যতক্ষণ না আপনি অন্যথায় নির্দেশ দেন। উদাহরণস্বরূপ, ধরা যাক আগামী সপ্তাহে rand
crate-এর 0.8.6 ভার্সন আসে, এবং সেই ভার্সনে একটি গুরুত্বপূর্ণ বাগ ফিক্স রয়েছে, কিন্তু এতে একটি রিগ্রেশনও রয়েছে যা আপনার কোড ভেঙে দেবে। এটি সামলাতে, রাস্ট প্রথমবার cargo build
চালানোর সময় Cargo.lock ফাইল তৈরি করে, তাই এখন আমাদের guessing_game ডিরেক্টরিতে এটি রয়েছে।
যখন আপনি প্রথমবার একটি প্রজেক্ট বিল্ড করেন, কার্গো ডিপেন্ডেন্সিগুলির সমস্ত ভার্সন বের করে যা মানদণ্ড পূরণ করে এবং তারপরে সেগুলি Cargo.lock ফাইলে লেখে। ভবিষ্যতে যখন আপনি আপনার প্রজেক্ট বিল্ড করবেন, কার্গো দেখবে যে Cargo.lock ফাইলটি বিদ্যমান এবং সেখানে নির্দিষ্ট ভার্সনগুলি ব্যবহার করবে, আবার ভার্সন বের করার সমস্ত কাজ না করে। এটি আপনাকে স্বয়ংক্রিয়ভাবে একটি পুনরুৎপাদনযোগ্য বিল্ড পেতে দেয়। অন্য কথায়, আপনার প্রজেক্ট 0.8.5-এ থাকবে যতক্ষণ না আপনি স্পষ্টভাবে আপগ্রেড করেন, Cargo.lock ফাইলের কারণে। যেহেতু Cargo.lock ফাইলটি পুনরুৎপাদনযোগ্য বিল্ডের জন্য গুরুত্বপূর্ণ, তাই এটি প্রায়শই আপনার প্রজেক্টের বাকি কোডের সাথে সোর্স কন্ট্রোলে চেক ইন করা হয়।
একটি নতুন ভার্সন পেতে একটি Crate আপডেট করা
যখন আপনি একটি crate আপডেট করতে চান, কার্গো update
কমান্ড সরবরাহ করে, যা Cargo.lock ফাইলটিকে উপেক্ষা করবে এবং Cargo.toml-এ আপনার স্পেসিফিকেশনগুলির সাথে মানানসই সমস্ত সর্বশেষ ভার্সন খুঁজে বের করবে। কার্গো তারপর সেই ভার্সনগুলি Cargo.lock ফাইলে লিখবে। এই ক্ষেত্রে, কার্গো কেবল 0.8.5 এর চেয়ে বড় এবং 0.9.0 এর চেয়ে কম ভার্সন খুঁজবে। যদি rand
crate-টি 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)
কার্গো 0.9.0 রিলিজটি উপেক্ষা করে। এই মুহূর্তে, আপনি আপনার Cargo.lock ফাইলে একটি পরিবর্তনও লক্ষ্য করবেন যা উল্লেখ করে যে আপনি এখন rand
crate-এর যে ভার্সনটি ব্যবহার করছেন তা হল 0.8.6। rand
ভার্সন 0.9.0 বা 0.9.x সিরিজের যেকোনো ভার্সন ব্যবহার করতে, আপনাকে Cargo.toml ফাইলটি আপডেট করে এমন দেখতে হবে:
[dependencies]
rand = "0.9.0"
পরবর্তীবার যখন আপনি cargo build
চালাবেন, কার্গো উপলব্ধ crate-গুলির রেজিস্ট্রি আপডেট করবে এবং আপনার নির্দিষ্ট করা নতুন ভার্সন অনুযায়ী আপনার rand
প্রয়োজনীয়তা পুনরায় মূল্যায়ন করবে।
কার্গো এবং এর ইকোসিস্টেম সম্পর্কে আরও অনেক কিছু বলার আছে, যা আমরা অধ্যায় ১৪-তে আলোচনা করব, কিন্তু আপাতত, আপনার এটুকুই জানা দরকার। কার্গো লাইব্রেরি পুনঃব্যবহার করা খুব সহজ করে তোলে, তাই রাস্টেশিয়ানরা ছোট ছোট প্রজেক্ট লিখতে পারে যা বিভিন্ন প্যাকেজ থেকে একত্রিত হয়।
একটি র্যান্ডম সংখ্যা তৈরি করা
চলুন অনুমান করার জন্য একটি সংখ্যা তৈরি করতে rand
ব্যবহার করা শুরু করি। পরবর্তী ধাপ হল src/main.rs আপডেট করা, যেমনটি লিস্টিং ২-৩-এ দেখানো হয়েছে।
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
নির্দিষ্ট করতে হবে।
দ্রষ্টব্য: আপনি কেবল জেনেই যাবেন না কোন ট্রেইট ব্যবহার করতে হবে এবং কোন মেথড এবং ফাংশনগুলি একটি crate থেকে কল করতে হবে, তাই প্রতিটি crate-এর ডকুমেন্টেশনে এটি ব্যবহারের জন্য নির্দেশাবলী থাকে। কার্গোর আরেকটি চমৎকার বৈশিষ্ট্য হল
cargo doc --open
কমান্ডটি চালালে এটি আপনার সমস্ত ডিপেন্ডেন্সি দ্বারা সরবরাহ করা ডকুমেন্টেশন স্থানীয়ভাবে তৈরি করবে এবং আপনার ব্রাউজারে খুলবে। আপনি যদিrand
crate-এর অন্যান্য কার্যকারিতা সম্পর্কে আগ্রহী হন, উদাহরণস্বরূপ,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
আপনার বিভিন্ন র্যান্ডম সংখ্যা পাওয়া উচিত, এবং সেগুলি সবই ১ থেকে ১০০ এর মধ্যে সংখ্যা হওয়া উচিত। দারুণ কাজ!
অনুমানের সাথে গোপন সংখ্যার তুলনা
এখন আমাদের কাছে ব্যবহারকারীর ইনপুট এবং একটি র্যান্ডম সংখ্যা আছে, আমরা তাদের তুলনা করতে পারি। সেই ধাপটি লিস্টিং ২-৪-এ দেখানো হয়েছে। মনে রাখবেন যে এই কোডটি এখনই কম্পাইল হবে না, যেমনটি আমরা ব্যাখ্যা করব।
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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
এক্সপ্রেশন arms (শাখা) দিয়ে গঠিত। একটি arm-এ একটি pattern (প্যাটার্ন) থাকে যার সাথে মেলানো হয়, এবং যে কোডটি চালানো উচিত যদি match
-কে দেওয়া মানটি সেই arm-এর প্যাটার্নের সাথে মিলে যায়। রাস্ট match
-কে দেওয়া মানটি নেয় এবং প্রতিটি arm-এর প্যাটার্ন একের পর এক পরীক্ষা করে। প্যাটার্ন এবং match
কনস্ট্রাক্ট হল শক্তিশালী রাস্ট বৈশিষ্ট্য: এগুলি আপনাকে আপনার কোড সম্মুখীন হতে পারে এমন বিভিন্ন পরিস্থিতি প্রকাশ করতে দেয় এবং তারা নিশ্চিত করে যে আপনি সেগুলি সবই পরিচালনা করেছেন। এই বৈশিষ্ট্যগুলি যথাক্রমে অধ্যায় ৬ এবং অধ্যায় ১৯-এ বিস্তারিতভাবে আলোচনা করা হবে।
চলুন আমরা এখানে যে match
এক্সপ্রেশনটি ব্যবহার করছি তার একটি উদাহরণ দিয়ে হেঁটে যাই। ধরা যাক ব্যবহারকারী ৫০ অনুমান করেছে এবং এইবার র্যান্ডমভাবে তৈরি গোপন সংখ্যাটি হল ৩৮।
যখন কোডটি ৫০ কে ৩৮ এর সাথে তুলনা করে, cmp
মেথডটি Ordering::Greater
প্রদান করবে কারণ ৫০, ৩৮ এর চেয়ে বড়। match
এক্সপ্রেশনটি Ordering::Greater
মানটি পায় এবং প্রতিটি arm-এর প্যাটার্ন পরীক্ষা করা শুরু করে। এটি প্রথম arm-এর প্যাটার্ন, Ordering::Less
-এর দিকে তাকায় এবং দেখে যে Ordering::Greater
মানটি Ordering::Less
-এর সাথে মেলে না, তাই এটি সেই arm-এর কোডটি উপেক্ষা করে এবং পরবর্তী arm-এ চলে যায়। পরবর্তী arm-এর প্যাটার্ন হল Ordering::Greater
, যা Ordering::Greater
-এর সাথে মিলে যায়! সেই arm-এর সংশ্লিষ্ট কোডটি কার্যকর হবে এবং স্ক্রিনে Too big!
প্রিন্ট করবে। match
এক্সপ্রেশনটি প্রথম সফল ম্যাচের পরে শেষ হয়ে যায়, তাই এটি এই পরিস্থিতিতে শেষ arm-টি দেখবে না।
তবে, লিস্টিং ২-৪-এর কোডটি এখনও কম্পাইল হবে না। চলুন চেষ্টা করি:
$ 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:23:21
|
23 | 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
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/cmp.rs:964:8
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 (বেমানান টাইপ)। রাস্টের একটি শক্তিশালী, স্ট্যাটিক টাইপ সিস্টেম আছে। তবে, এর টাইপ ইনফারেন্সও আছে। যখন আমরা let mut guess = String::new()
লিখেছিলাম, রাস্ট অনুমান করতে পেরেছিল যে guess
একটি String
হওয়া উচিত এবং আমাদের টাইপ লিখতে বাধ্য করেনি। অন্যদিকে, secret_number
একটি সংখ্যা টাইপ। রাস্টের কয়েকটি সংখ্যা টাইপের মান ১ থেকে ১০০ এর মধ্যে থাকতে পারে: i32
, একটি ৩২-বিট সংখ্যা; u32
, একটি আনসাইন্ড ৩২-বিট সংখ্যা; i64
, একটি ৬৪-বিট সংখ্যা; এবং আরও অন্যান্য। অন্যথায় নির্দিষ্ট না করা হলে, রাস্ট ডিফল্ট হিসেবে i32
ব্যবহার করে, যা secret_number
-এর টাইপ, যদি না আপনি অন্য কোথাও টাইপ তথ্য যোগ করেন যা রাস্টকে একটি ভিন্ন সংখ্যাসূচক টাইপ অনুমান করতে বাধ্য করবে। ত্রুটির কারণ হল রাস্ট একটি স্ট্রিং এবং একটি সংখ্যা টাইপের তুলনা করতে পারে না।
শেষ পর্যন্ত, আমরা প্রোগ্রামটি ইনপুট হিসাবে যে String
পড়ে তা একটি সংখ্যা টাইপে রূপান্তর করতে চাই যাতে আমরা এটিকে গোপন সংখ্যার সাথে সংখ্যাগতভাবে তুলনা করতে পারি। আমরা main
ফাংশনের বডিতে এই লাইনটি যোগ করে তা করি:
ফাইলের নাম: src/main.rs
use std::cmp::Ordering;
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.");
// --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("Please type a number!");
আমরা guess
নামে একটি ভ্যারিয়েবল তৈরি করি। কিন্তু অপেক্ষা করুন, প্রোগ্রামে কি ইতিমধ্যে guess
নামে একটি ভ্যারিয়েবল নেই? আছে, কিন্তু সহায়কভাবে রাস্ট আমাদের guess
-এর পূর্ববর্তী মানটিকে একটি নতুন মান দিয়ে শ্যাডো (shadow) করার অনুমতি দেয়। Shadowing আমাদের guess
ভ্যারিয়েবলের নামটি পুনরায় ব্যবহার করতে দেয়, যেমন guess_str
এবং guess
নামে দুটি অনন্য ভ্যারিয়েবল তৈরি করতে বাধ্য করার পরিবর্তে। আমরা এটি অধ্যায় ৩-এ আরও বিস্তারিতভাবে আলোচনা করব, কিন্তু আপাতত, জেনে রাখুন যে এই বৈশিষ্ট্যটি প্রায়শই ব্যবহৃত হয় যখন আপনি একটি মানকে এক টাইপ থেকে অন্য টাইপে রূপান্তর করতে চান।
আমরা এই নতুন ভ্যারিয়েবলটিকে guess.trim().parse()
এক্সপ্রেশনের সাথে বাইন্ড করি। এক্সপ্রেশনের guess
মূল guess
ভ্যারিয়েবলটিকে বোঝায় যা ইনপুটটিকে একটি স্ট্রিং হিসাবে ধারণ করেছিল। একটি String
ইনস্ট্যান্সের trim
মেথড শুরু এবং শেষের যেকোনো হোয়াইটস্পেস দূর করে দেবে, যা আমাদের স্ট্রিংটিকে একটি u32
-তে রূপান্তর করার আগে করতে হবে, যা কেবল সংখ্যাসূচক ডেটা ধারণ করতে পারে। ব্যবহারকারীকে read_line
সন্তুষ্ট করতে এবং তাদের অনুমান ইনপুট করতে enter চাপতে হবে, যা স্ট্রিংটিতে একটি নিউলাইন ক্যারেক্টার যোগ করে। উদাহরণস্বরূপ, যদি ব্যবহারকারী 5 টাইপ করে এবং enter চাপে, guess
দেখতে এমন হয়: 5\n
। \n
"নিউলাইন" প্রতিনিধিত্ব করে। (উইন্ডোজে, enter চাপলে একটি ক্যারেজ রিটার্ন এবং একটি নিউলাইন হয়, \r\n
।) trim
মেথড \n
বা \r\n
দূর করে, যার ফলে কেবল 5
থাকে।
স্ট্রিং-এর উপর parse
মেথড একটি স্ট্রিংকে অন্য টাইপে রূপান্তর করে। এখানে, আমরা এটিকে একটি স্ট্রিং থেকে একটি সংখ্যায় রূপান্তর করতে ব্যবহার করি। আমাদের রাস্টকে let guess: u32
ব্যবহার করে ঠিক কোন সংখ্যা টাইপ আমরা চাই তা বলতে হবে। guess
-এর পরে কোলন (:
) রাস্টকে বলে যে আমরা ভ্যারিয়েবলের টাইপ অ্যানোটেট করব। রাস্টের কয়েকটি অন্তর্নির্মিত সংখ্যা টাইপ আছে; এখানে দেখা u32
একটি আনসাইন্ড, ৩২-বিট ইন্টিজার। এটি একটি ছোট ধনাত্মক সংখ্যার জন্য একটি ভাল ডিফল্ট পছন্দ। আপনি অধ্যায় ৩-এ অন্যান্য সংখ্যা টাইপ সম্পর্কে শিখবেন।
উপরন্তু, এই উদাহরণ প্রোগ্রামে u32
অ্যানোটেশন এবং secret_number
-এর সাথে তুলনা করার মানে হল রাস্ট অনুমান করবে যে secret_number
-ও একটি u32
হওয়া উচিত। তাই এখন তুলনাটি একই টাইপের দুটি মানের মধ্যে হবে!
parse
মেথডটি কেবল সেই অক্ষরগুলির উপর কাজ করবে যেগুলিকে যৌক্তিকভাবে সংখ্যায় রূপান্তর করা যায় এবং তাই সহজেই ত্রুটি ঘটাতে পারে। উদাহরণস্বরূপ, যদি স্ট্রিংটিতে A👍%
থাকে, তবে সেটিকে সংখ্যায় রূপান্তর করার কোনো উপায় থাকবে না। কারণ এটি ব্যর্থ হতে পারে, parse
মেথডটি একটি Result
টাইপ প্রদান করে, যেমনটি read_line
মেথড করে (আগে “Handling Potential Failure with 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!
সুন্দর! অনুমানের আগে স্পেস যোগ করা সত্ত্বেও, প্রোগ্রামটি এখনও বুঝতে পেরেছে যে ব্যবহারকারী ৭৬ অনুমান করেছে। বিভিন্ন ধরণের ইনপুটের সাথে বিভিন্ন আচরণ যাচাই করার জন্য প্রোগ্রামটি কয়েকবার চালান: সংখ্যাটি সঠিকভাবে অনুমান করুন, খুব বেশি একটি সংখ্যা অনুমান করুন, এবং খুব কম একটি সংখ্যা অনুমান করুন।
আমাদের গেমের বেশিরভাগই এখন কাজ করছে, কিন্তু ব্যবহারকারী কেবল একটি অনুমান করতে পারে। চলুন একটি লুপ যোগ করে এটি পরিবর্তন করি!
লুপিংয়ের মাধ্যমে একাধিক অনুমানের অনুমতি দেওয়া
loop
কীওয়ার্ড একটি অসীম লুপ তৈরি করে। আমরা ব্যবহারকারীদের সংখ্যাটি অনুমান করার আরও সুযোগ দিতে একটি লুপ যোগ করব:
ফাইলের নাম: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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 ব্যবহার করে প্রোগ্রামটি বাধা দিতে পারে। কিন্তু এই অতৃপ্ত দৈত্য থেকে বাঁচার আরেকটি উপায় আছে, যেমনটি "Comparing the Guess to the Secret Number" বিভাগে 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
টাইপ করলে গেমটি থেকে বের হয়ে যাবে, কিন্তু আপনি যেমন লক্ষ্য করবেন, অন্য যেকোনো অ-সংখ্যা ইনপুট প্রবেশ করালেও তাই হবে। এটি সর্বোত্তম থেকে অনেক দূরে; আমরা চাই যে সঠিক সংখ্যা অনুমান করা হলে গেমটি বন্ধ হয়ে যাক।
সঠিক অনুমানের পর খেলা শেষ করা
চলুন একটি break
স্টেটমেন্ট যোগ করে গেমটি এমনভাবে প্রোগ্রাম করি যাতে ব্যবহারকারী জিতলে খেলাটি শেষ হয়ে যায়:
ফাইলের নাম: src/main.rs
use std::cmp::Ordering;
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}");
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
-এর শেষ অংশ।
অবৈধ ইনপুট পরিচালনা করা
গেমের আচরণ আরও পরিমার্জিত করার জন্য, ব্যবহারকারী যখন একটি অ-সংখ্যা ইনপুট করে তখন প্রোগ্রামটি ক্র্যাশ করার পরিবর্তে, চলুন গেমটিকে একটি অ-সংখ্যা উপেক্ষা করতে বাধ্য করি যাতে ব্যবহারকারী অনুমান চালিয়ে যেতে পারে। আমরা এটি সেই লাইনটি পরিবর্তন করে করতে পারি যেখানে guess
একটি String
থেকে একটি u32
-তে রূপান্তরিত হয়, যেমনটি লিস্টিং ২-৫-এ দেখানো হয়েছে।
use std::cmp::Ordering;
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}");
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
মানটি প্রথম arm-এর প্যাটার্নের সাথে মিলবে, এবং match
এক্সপ্রেশনটি কেবল parse
দ্বারা উৎপাদিত এবং Ok
মানের ভিতরে রাখা num
মানটি প্রদান করবে। সেই সংখ্যাটি আমরা যে নতুন guess
ভ্যারিয়েবলটি তৈরি করছি সেখানে ঠিক যেখানে আমরা চাই সেখানেই শেষ হবে।
যদি parse
স্ট্রিংটিকে একটি সংখ্যায় পরিণত করতে না পারে, তবে এটি একটি Err
মান প্রদান করবে যা ত্রুটি সম্পর্কে আরও তথ্য ধারণ করে। Err
মানটি প্রথম match
arm-এর Ok(num)
প্যাটার্নের সাথে মেলে না, তবে এটি দ্বিতীয় arm-এর Err(_)
প্যাটার্নের সাথে মেলে। আন্ডারস্কোর, _
, একটি ক্যাচ-অল মান; এই উদাহরণে, আমরা বলছি যে আমরা সমস্ত Err
মান মেলাতে চাই, তাদের ভিতরে যাই তথ্য থাকুক না কেন। তাই প্রোগ্রামটি দ্বিতীয় arm-এর কোড, 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!
অসাধারণ! একটি ছোট চূড়ান্ত পরিবর্তনের মাধ্যমে, আমরা guessing game শেষ করব। মনে রাখবেন যে প্রোগ্রামটি এখনও গোপন সংখ্যাটি প্রিন্ট করছে। এটি পরীক্ষার জন্য ভাল কাজ করেছে, কিন্তু এটি গেমটি নষ্ট করে দেয়। চলুন গোপন সংখ্যাটি আউটপুট করে এমন println!
টি মুছে ফেলি। লিস্টিং ২-৬ চূড়ান্ত কোডটি দেখায়।
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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;
}
}
}
}
এই মুহূর্তে, আপনি সফলভাবে guessing game তৈরি করেছেন। অভিনন্দন!
সারাংশ
এই প্রজেক্টটি আপনাকে অনেক নতুন রাস্ট কনসেপ্টের সাথে পরিচিত করার একটি বাস্তবসম্মত উপায় ছিল: let
, match
, ফাংশন, এক্সটার্নাল crate-এর ব্যবহার, এবং আরও অনেক কিছু। পরবর্তী কয়েকটি অধ্যায়ে, আপনি এই ধারণাগুলি সম্পর্কে আরও বিস্তারিতভাবে শিখবেন। অধ্যায় ৩ এমন ধারণাগুলি কভার করে যা বেশিরভাগ প্রোগ্রামিং ভাষাতেই আছে, যেমন ভ্যারিয়েবল, ডেটা টাইপ, এবং ফাংশন, এবং দেখায় কিভাবে রাস্ট-এ সেগুলি ব্যবহার করতে হয়। অধ্যায় ৪ ওনারশিপ অন্বেষণ করে, একটি বৈশিষ্ট্য যা রাস্টকে অন্যান্য ভাষা থেকে আলাদা করে। অধ্যায় ৫ struct এবং মেথড সিনট্যাক্স নিয়ে আলোচনা করে, এবং অধ্যায় ৬ ব্যাখ্যা করে কিভাবে enum কাজ করে।
সাধারণ প্রোগ্রামিং ধারণা
এই অধ্যায়ে এমন কিছু ধারণা নিয়ে আলোচনা করা হয়েছে যা প্রায় সব প্রোগ্রামিং ল্যাঙ্গুয়েজেই পাওয়া যায় এবং রাস্ট-এ সেগুলো কীভাবে কাজ করে। অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজের মূল ভিত্তি প্রায় একই রকম। এই অধ্যায়ে উপস্থাপিত কোনো ধারণাই শুধু রাস্টের জন্য অনন্য নয়, তবে আমরা রাস্টের প্রেক্ষাপটে সেগুলো আলোচনা করব এবং এই ধারণাগুলো ব্যবহারের প্রচলিত নিয়মগুলো ব্যাখ্যা করব।
বিশেষ করে, আপনি ভ্যারিয়েবল, বেসিক টাইপ, ফাংশন, কমেন্ট এবং কন্ট্রোল ফ্লো সম্পর্কে শিখবেন। এই মৌলিক বিষয়গুলো প্রতিটি রাস্ট প্রোগ্রামে থাকবে, এবং এগুলো আগেভাগে শিখে নিলে আপনার শুরু করার জন্য একটি শক্তিশালী ভিত্তি তৈরি হবে।
Keywords
অন্যান্য ল্যাঙ্গুয়েজের মতোই, রাস্ট ল্যাঙ্গুয়েজেরও কিছু নির্দিষ্ট keywords আছে যা শুধুমাত্র ল্যাঙ্গুয়েজের ব্যবহারের জন্য সংরক্ষিত। মনে রাখবেন যে আপনি এই শব্দগুলোকে ভ্যারিয়েবল বা ফাংশনের নাম হিসেবে ব্যবহার করতে পারবেন না। বেশিরভাগ কীওয়ার্ডের বিশেষ অর্থ রয়েছে, এবং আপনি আপনার রাস্ট প্রোগ্রামে বিভিন্ন কাজ করার জন্য সেগুলি ব্যবহার করবেন; কিছু কীওয়ার্ডের সাথে বর্তমানে কোনো কার্যকারিতা যুক্ত নেই তবে ভবিষ্যতে রাস্ট-এ যুক্ত হতে পারে এমন কার্যকারিতার জন্য সংরক্ষিত রাখা হয়েছে। আপনি পরিশিষ্ট এ-তে কীওয়ার্ডগুলোর একটি তালিকা খুঁজে পেতে পারেন।
ভ্যারিয়েবল এবং পরিবর্তনশীলতা (Variables and Mutability)
যেমনটি "ভ্যারিয়েবলের মাধ্যমে মান সংরক্ষণ করা" বিভাগে উল্লেখ করা হয়েছে, ডিফল্টরূপে, ভ্যারিয়েবলগুলো অপরিবর্তনীয় (immutable)। এটি রাস্টের অনেকগুলো পদ্ধতির মধ্যে একটি যা আপনাকে কোড এমনভাবে লিখতে উৎসাহিত করে যা রাস্টের দেওয়া নিরাপত্তা এবং সহজ কনকারেন্সি (concurrency)-এর সুবিধা নেয়। তবে, আপনার কাছে আপনার ভ্যারিয়েবলগুলোকে পরিবর্তনযোগ্য (mutable) করার বিকল্পও রয়েছে। চলুন অন্বেষণ করি কীভাবে এবং কেন রাস্ট আপনাকে অপরিবর্তনীয়তাকে অগ্রাধিকার দিতে উৎসাহিত করে এবং কেন কখনও কখনও আপনি এর থেকে সরে আসতে চাইতে পারেন।
যখন একটি ভ্যারিয়েবল অপরিবর্তনীয় হয়, একবার একটি মান একটি নামের সাথে বাইন্ড করা হলে, আপনি সেই মান পরিবর্তন করতে পারবেন না। এটি দেখানোর জন্য, আপনার projects ডিরেক্টরিতে cargo new variables
ব্যবহার করে variables নামে একটি নতুন প্রজেক্ট তৈরি করুন।
তারপর, আপনার নতুন variables ডিরেক্টরিতে, src/main.rs খুলুন এবং এর কোডটি নিম্নলিখিত কোড দিয়ে প্রতিস্থাপন করুন, যা এখনই কম্পাইল হবে না:
ফাইলের নাম: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
cargo run
ব্যবহার করে প্রোগ্রামটি সংরক্ষণ করুন এবং চালান। আপনি একটি অপরিবর্তনীয়তার ত্রুটি সম্পর্কিত একটি এরর বার্তা পাবেন, যেমনটি এই আউটপুটে দেখানো হয়েছে:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
এই উদাহরণটি দেখায় কীভাবে কম্পাইলার আপনাকে আপনার প্রোগ্রামের ত্রুটি খুঁজে পেতে সাহায্য করে। কম্পাইলার এরর হতাশাজনক হতে পারে, কিন্তু আসলে এর মানে শুধু এই যে আপনার প্রোগ্রাম এখনও নিরাপদে যা করতে চায় তা করছে না; এর মানে এই নয় যে আপনি একজন ভাল প্রোগ্রামার নন! অভিজ্ঞ রাস্টেশিয়ানরাও কম্পাইলার এরর পান।
আপনি cannot assign twice to immutable variable `x`
এরর বার্তাটি পেয়েছেন কারণ আপনি অপরিবর্তনীয় x
ভ্যারিয়েবলে দ্বিতীয়বার একটি মান অ্যাসাইন করার চেষ্টা করেছেন।
যখন আমরা একটি মান পরিবর্তন করার চেষ্টা করি যা অপরিবর্তনীয় হিসাবে চিহ্নিত করা হয়েছে, তখন কম্পাইল-টাইম এরর পাওয়া গুরুত্বপূর্ণ কারণ এই পরিস্থিতিটি বাগের কারণ হতে পারে। যদি আমাদের কোডের একটি অংশ এই অনুমানের উপর কাজ করে যে একটি মান কখনও পরিবর্তন হবে না এবং আমাদের কোডের অন্য একটি অংশ সেই মানটি পরিবর্তন করে, তবে এটি সম্ভব যে কোডের প্রথম অংশটি যা করার জন্য ডিজাইন করা হয়েছিল তা করবে না। এই ধরনের বাগের কারণ পরে খুঁজে বের করা কঠিন হতে পারে, বিশেষ করে যখন দ্বিতীয় কোডটি কেবল মাঝে মাঝে মান পরিবর্তন করে। রাস্ট কম্পাইলার গ্যারান্টি দেয় যে যখন আপনি বলেন যে একটি মান পরিবর্তন হবে না, তখন এটি সত্যিই পরিবর্তন হবে না, তাই আপনাকে নিজে এটি ট্র্যাক রাখতে হবে না। আপনার কোড তাই বোঝা সহজ হয়।
কিন্তু পরিবর্তনশীলতা খুব দরকারী হতে পারে, এবং কোড লেখা আরও সুবিধাজনক করে তুলতে পারে। যদিও ভ্যারিয়েবলগুলো ডিফল্টরূপে অপরিবর্তনীয়, আপনি অধ্যায় ২-এর মতো ভ্যারিয়েবলের নামের সামনে mut
যোগ করে সেগুলিকে পরিবর্তনযোগ্য করতে পারেন। mut
যোগ করা কোডের ভবিষ্যতের পাঠকদের কাছেও উদ্দেশ্য প্রকাশ করে, এটা নির্দেশ করে যে কোডের অন্যান্য অংশ এই ভ্যারিয়েবলের মান পরিবর্তন করবে।
উদাহরণস্বরূপ, চলুন src/main.rs পরিবর্তন করে নিম্নলিখিতটি করা যাক:
ফাইলের নাম: src/main.rs
fn main() { let mut x = 5; println!("The value of x is: {x}"); x = 6; println!("The value of x is: {x}"); }
যখন আমরা এখন প্রোগ্রামটি চালাই, আমরা এটি পাই:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
যখন mut
ব্যবহার করা হয়, তখন আমরা x
-এর সাথে বাইন্ড করা মান 5
থেকে 6
-এ পরিবর্তন করার অনুমতি পাই। পরিশেষে, পরিবর্তনশীলতা ব্যবহার করবেন কি না, তা আপনার উপর নির্ভর করে এবং সেই নির্দিষ্ট পরিস্থিতিতে কোনটি সবচেয়ে স্পষ্ট বলে আপনি মনে করেন তার উপর নির্ভর করে।
কনস্ট্যান্ট (Constants)
অপরিবর্তনীয় ভ্যারিয়েবলের মতো, কনস্ট্যান্ট হলো এমন মান যা একটি নামের সাথে বাইন্ড করা থাকে এবং পরিবর্তন করার অনুমতি নেই, তবে কনস্ট্যান্ট এবং ভ্যারিয়েবলের মধ্যে কয়েকটি পার্থক্য রয়েছে।
প্রথমত, আপনাকে কনস্ট্যান্টের সাথে mut
ব্যবহার করার অনুমতি নেই। কনস্ট্যান্ট শুধু ডিফল্টরূপে অপরিবর্তনীয় নয়—তারা সবসময়ই অপরিবর্তনীয়। আপনি let
কীওয়ার্ডের পরিবর্তে const
কীওয়ার্ড ব্যবহার করে কনস্ট্যান্ট ঘোষণা করেন, এবং মানের টাইপ অবশ্যই অ্যানোটেট করতে হবে। আমরা পরবর্তী বিভাগে, "ডেটা টাইপ"-এ, টাইপ এবং টাইপ অ্যানোটেশন নিয়ে আলোচনা করব, তাই এখনই বিস্তারিত নিয়ে চিন্তা করবেন না। শুধু জেনে রাখুন যে আপনাকে সবসময় টাইপ অ্যানোটেট করতে হবে।
কনস্ট্যান্ট যেকোনো স্কোপে, এমনকি গ্লোবাল স্কোপেও ঘোষণা করা যেতে পারে, যা তাদের সেইসব মানের জন্য দরকারী করে তোলে যা কোডের অনেক অংশের জানা প্রয়োজন।
শেষ পার্থক্য হল যে কনস্ট্যান্ট শুধুমাত্র একটি কনস্ট্যান্ট এক্সপ্রেশনে সেট করা যেতে পারে, এমন কোনো মানের ফলাফলে নয় যা কেবল রানটাইমে গণনা করা যেতে পারে।
এখানে একটি কনস্ট্যান্ট ঘোষণার উদাহরণ:
#![allow(unused)] fn main() { const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; }
কনস্ট্যান্টের নাম THREE_HOURS_IN_SECONDS
এবং এর মান 60 (এক মিনিটে সেকেন্ডের সংখ্যা) কে 60 (এক ঘন্টায় মিনিটের সংখ্যা) দিয়ে এবং তারপর 3 (এই প্রোগ্রামে আমরা যত ঘন্টা গণনা করতে চাই) দিয়ে গুণ করার ফলাফলে সেট করা হয়েছে। কনস্ট্যান্টের জন্য রাস্টের নামকরণের প্রথা হল সব বড় হাতের অক্ষর ব্যবহার করা এবং শব্দগুলোর মধ্যে আন্ডারস্কোর ব্যবহার করা। কম্পাইলার কম্পাইল করার সময় একটি সীমিত সেট অপারেশন মূল্যায়ন করতে সক্ষম, যা আমাদের এই মানটিকে এমনভাবে লেখার সুযোগ দেয় যা বোঝা এবং যাচাই করা সহজ, এই কনস্ট্যান্টটিকে 10,800 মানে সেট করার পরিবর্তে। কনস্ট্যান্ট ঘোষণা করার সময় কোন অপারেশনগুলো ব্যবহার করা যেতে পারে সে সম্পর্কে আরও তথ্যের জন্য রাস্ট রেফারেন্সের কনস্ট্যান্ট মূল্যায়ন সম্পর্কিত বিভাগ দেখুন।
কনস্ট্যান্টগুলো একটি প্রোগ্রাম চলার পুরো সময় জুড়ে বৈধ থাকে, যে স্কোপে তারা ঘোষণা করা হয়েছিল তার মধ্যে। এই বৈশিষ্ট্যটি কনস্ট্যান্টগুলোকে আপনার অ্যাপ্লিকেশনের ডোমেনের এমন মানগুলোর জন্য দরকারী করে তোলে যা প্রোগ্রামের একাধিক অংশের জানা প্রয়োজন হতে পারে, যেমন কোনো খেলোয়াড় সর্বোচ্চ যত পয়েন্ট অর্জন করতে পারে, বা আলোর গতি।
আপনার প্রোগ্রাম জুড়ে ব্যবহৃত হার্ডকোডেড মানগুলোকে কনস্ট্যান্ট হিসাবে নামকরণ করা কোডের ভবিষ্যতের রক্ষণাবেক্ষণকারীদের কাছে সেই মানের অর্থ বোঝাতে দরকারী। এটি আপনার কোডে কেবল একটি জায়গা রাখতেও সাহায্য করে যা ভবিষ্যতে হার্ডকোডেড মান আপডেট করার প্রয়োজন হলে পরিবর্তন করতে হবে।
শ্যাডোইং (Shadowing)
যেমন আপনি অধ্যায় ২-এর গেসিং গেম টিউটোরিয়ালে দেখেছেন, আপনি একটি পূর্ববর্তী ভ্যারিয়েবলের একই নামে একটি নতুন ভ্যারিয়েবল ঘোষণা করতে পারেন। রাস্টেশিয়ানরা বলেন যে প্রথম ভ্যারিয়েবলটি দ্বিতীয়টি দ্বারা শ্যাডো (shadowed) করা হয়েছে, যার মানে হল যে দ্বিতীয় ভ্যারিয়েবলটি হল যা কম্পাইলার দেখবে যখন আপনি ভ্যারিয়েবলের নামটি ব্যবহার করবেন। কার্যকরভাবে, দ্বিতীয় ভ্যারিয়েবলটি প্রথমটিকে ছাপিয়ে যায়, ভ্যারিয়েবলের নামের যেকোনো ব্যবহার নিজের দিকে নিয়ে নেয় যতক্ষণ না এটি নিজে শ্যাডো করা হয় বা স্কোপ শেষ হয়। আমরা একই ভ্যারিয়েবলের নাম ব্যবহার করে এবং let
কীওয়ার্ডের ব্যবহার পুনরাবৃত্তি করে একটি ভ্যারিয়েবলকে শ্যাডো করতে পারি, যেমনটি নিচে দেওয়া হল:
ফাইলের নাম: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {x}"); } println!("The value of x is: {x}"); }
এই প্রোগ্রামটি প্রথমে x
-কে 5
মানের সাথে বাইন্ড করে। তারপরে এটি let x =
পুনরাবৃত্তি করে একটি নতুন ভ্যারিয়েবল x
তৈরি করে, মূল মানটি নিয়ে এবং 1
যোগ করে যাতে x
-এর মান তখন 6
হয়। তারপর, কার্লি ব্র্যাকেট দিয়ে তৈরি একটি অভ্যন্তরীণ স্কোপের মধ্যে, তৃতীয় let
স্টেটমেন্টটিও x
-কে শ্যাডো করে এবং একটি নতুন ভ্যারিয়েবল তৈরি করে, পূর্ববর্তী মানকে 2
দিয়ে গুণ করে x
-কে 12
মান দেয়। যখন সেই স্কোপ শেষ হয়ে যায়, তখন অভ্যন্তরীণ শ্যাডোইং শেষ হয় এবং x
আবার 6
-এ ফিরে আসে। যখন আমরা এই প্রোগ্রামটি চালাই, তখন এটি নিম্নলিখিত আউটপুট দেবে:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
শ্যাডোইং একটি ভ্যারিয়েবলকে mut
হিসাবে চিহ্নিত করার থেকে ভিন্ন কারণ আমরা যদি let
কীওয়ার্ড ব্যবহার না করে ভুলবশত এই ভ্যারিয়েবলে পুনরায় অ্যাসাইন করার চেষ্টা করি তবে আমরা একটি কম্পাইল-টাইম এরর পাব। let
ব্যবহার করে, আমরা একটি মানের উপর কয়েকটি রূপান্তর করতে পারি কিন্তু সেই রূপান্তরগুলো সম্পন্ন হওয়ার পরে ভ্যারিয়েবলটি অপরিবর্তনীয় থাকে।
mut
এবং শ্যাডোইংয়ের মধ্যে অন্য পার্থক্য হল যে যেহেতু আমরা let
কীওয়ার্ডটি আবার ব্যবহার করার সময় কার্যকরভাবে একটি নতুন ভ্যারিয়েবল তৈরি করছি, তাই আমরা মানের টাইপ পরিবর্তন করতে পারি কিন্তু একই নাম পুনরায় ব্যবহার করতে পারি। উদাহরণস্বরূপ, ধরা যাক আমাদের প্রোগ্রাম একজন ব্যবহারকারীকে কিছু টেক্সটের মধ্যে কতগুলো স্পেস চায় তা স্পেস ক্যারেক্টার ইনপুট করে দেখাতে বলে, এবং তারপর আমরা সেই ইনপুটটিকে একটি সংখ্যা হিসাবে সংরক্ষণ করতে চাই:
fn main() { let spaces = " "; let spaces = spaces.len(); }
প্রথম spaces
ভ্যারিয়েবলটি একটি স্ট্রিং টাইপ এবং দ্বিতীয় spaces
ভ্যারিয়েবলটি একটি সংখ্যা টাইপ। শ্যাডোইং এইভাবে আমাদের spaces_str
এবং spaces_num
এর মতো বিভিন্ন নাম নিয়ে আসা থেকে বাঁচায়; পরিবর্তে, আমরা সহজ spaces
নামটি পুনরায় ব্যবহার করতে পারি। তবে, আমরা যদি এর জন্য mut
ব্যবহার করার চেষ্টা করি, যেমনটি এখানে দেখানো হয়েছে, আমরা একটি কম্পাইল-টাইম এরর পাব:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
এররটি বলছে যে আমাদের একটি ভ্যারিয়েবলের টাইপ মিউটেট করার অনুমতি নেই:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
এখন আমরা ভ্যারিয়েবলগুলো কীভাবে কাজ করে তা অন্বেষণ করেছি, চলুন দেখা যাক তাদের আর কী কী ডেটা টাইপ থাকতে পারে।
ডেটা টাইপ
রাস্ট-এর প্রতিটি ভ্যালুর একটি নির্দিষ্ট ডেটা টাইপ থাকে, যা রাস্টকে বলে দেয় কী ধরনের ডেটা নির্দিষ্ট করা হচ্ছে যাতে এটি সেই ডেটার সাথে কীভাবে কাজ করতে হবে তা জানতে পারে। আমরা দুটি ডেটা টাইপের উপসেট দেখব: স্কেলার (scalar) এবং কম্পাউন্ড (compound)।
মনে রাখবেন যে রাস্ট একটি স্ট্যাটিক্যালি টাইপড ল্যাঙ্গুয়েজ, যার মানে হল কম্পাইল করার সময় এটিকে অবশ্যই সমস্ত ভ্যারিয়েবলের টাইপ জানতে হবে। কম্পাইলার সাধারণত আমরা কোন টাইপ ব্যবহার করতে চাই তা ভ্যালু এবং আমরা কীভাবে এটি ব্যবহার করি তার উপর ভিত্তি করে অনুমান করতে পারে। যখন অনেকগুলো টাইপ সম্ভব হয়, যেমন অধ্যায় ২-এর "অনুমানের সাথে গোপন সংখ্যার তুলনা" বিভাগে আমরা যখন parse
ব্যবহার করে একটি String
-কে একটি নিউমেরিক টাইপে রূপান্তর করেছিলাম, তখন আমাদের অবশ্যই একটি টাইপ অ্যানোটেশন যোগ করতে হবে, যেমন:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); }
যদি আমরা উপরের কোডে দেখানো : u32
টাইপ অ্যানোটেশন যোগ না করি, রাস্ট নিম্নলিখিত এররটি প্রদর্শন করবে, যার মানে হল আমরা কোন টাইপ ব্যবহার করতে চাই তা জানার জন্য কম্পাইলারের আমাদের কাছ থেকে আরও তথ্য প্রয়োজন:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
আপনি অন্যান্য ডেটা টাইপের জন্য বিভিন্ন টাইপ অ্যানোটেশন দেখতে পাবেন।
স্কেলার টাইপ (Scalar Types)
একটি স্কেলার টাইপ একটি একক মান উপস্থাপন করে। রাস্টের চারটি প্রাথমিক স্কেলার টাইপ রয়েছে: ইন্টিজার (integers), ফ্লোটিং-পয়েন্ট নাম্বার (floating-point numbers), বুলিয়ান (Booleans) এবং ক্যারেক্টার (characters)। আপনি হয়তো অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজ থেকে এগুলোর সাথে পরিচিত। চলুন রাস্ট-এ এগুলো কীভাবে কাজ করে তা দেখা যাক।
ইন্টিজার টাইপ (Integer Types)
একটি ইন্টিজার হল একটি সংখ্যা যার কোনো ভগ্নাংশ নেই। আমরা অধ্যায় ২-এ একটি ইন্টিজার টাইপ, u32
টাইপ ব্যবহার করেছি। এই টাইপ ডিক্লারেশনটি নির্দেশ করে যে এর সাথে যুক্ত ভ্যালুটি একটি আনসাইন্ড ইন্টিজার (সাইনড ইন্টিজার টাইপগুলো u
এর পরিবর্তে i
দিয়ে শুরু হয়) যা ৩২ বিট জায়গা নেয়। সারণী ৩-১ রাস্টের বিল্ট-ইন ইন্টিজার টাইপগুলো দেখায়। আমরা একটি ইন্টিজার ভ্যালুর টাইপ ঘোষণা করতে এই ভ্যারিয়েন্টগুলোর যেকোনো একটি ব্যবহার করতে পারি।
সারণী ৩-১: রাস্ট-এ ইন্টিজার টাইপ
দৈর্ঘ্য (Length) | সাইনড (Signed) | আনসাইনড (Unsigned) |
---|---|---|
৮-বিট | i8 | u8 |
১৬-বিট | i16 | u16 |
৩২-বিট | i32 | u32 |
৬৪-বিট | i64 | u64 |
১২৮-বিট | i128 | u128 |
আর্কিটেকচার নির্ভর | isize | usize |
প্রতিটি ভ্যারিয়েন্ট সাইনড বা আনসাইনড হতে পারে এবং এর একটি সুস্পষ্ট আকার রয়েছে। সাইনড এবং আনসাইনড বলতে বোঝায় যে সংখ্যাটি ঋণাত্মক হওয়া সম্ভব কিনা—অন্য কথায়, সংখ্যাটির সাথে একটি চিহ্ন (সাইন) থাকার প্রয়োজন আছে কিনা (সাইনড) অথবা এটি কেবল ধনাত্মক হবে এবং তাই কোনো চিহ্ন ছাড়াই উপস্থাপন করা যেতে পারে (আনসাইনড)। এটা কাগজে সংখ্যা লেখার মতো: যখন চিহ্ন গুরুত্বপূর্ণ, তখন একটি সংখ্যা প্লাস বা মাইনাস চিহ্ন দিয়ে দেখানো হয়; তবে, যখন সংখ্যাটি ধনাত্মক বলে ধরে নেওয়া নিরাপদ, তখন এটি কোনো চিহ্ন ছাড়াই দেখানো হয়। সাইনড সংখ্যা two’s complement রিপ্রেজেন্টেশন ব্যবহার করে সংরক্ষণ করা হয়।
প্রতিটি সাইনড ভ্যারিয়েন্ট −(২n − ১) থেকে ২n − ১ − ১ পর্যন্ত সংখ্যা সংরক্ষণ করতে পারে, যেখানে n হল সেই ভ্যারিয়েন্টটি যে বিট সংখ্যা ব্যবহার করে। তাই একটি i8
−(২৭) থেকে ২৭ − ১ পর্যন্ত, যা −১২৮ থেকে ১২৭ এর সমান, সংখ্যা সংরক্ষণ করতে পারে। আনসাইনড ভ্যারিয়েন্টগুলো ০ থেকে ২n − ১ পর্যন্ত সংখ্যা সংরক্ষণ করতে পারে, তাই একটি u8
০ থেকে ২৮ − ১ পর্যন্ত, যা ০ থেকে ২৫৫ এর সমান, সংখ্যা সংরক্ষণ করতে পারে।
এছাড়াও, isize
এবং usize
টাইপগুলো আপনার প্রোগ্রাম যে কম্পিউটারের আর্কিটেকচারে চলছে তার উপর নির্ভর করে: ৬৪-বিট আর্কিটেকচারে থাকলে ৬৪ বিট এবং ৩২-বিট আর্কিটেকচারে থাকলে ৩২ বিট।
আপনি সারণী ৩-২-এ দেখানো যেকোনো ফর্মে ইন্টিজার লিটারেল লিখতে পারেন। মনে রাখবেন যে যেসব নাম্বার লিটারেল একাধিক নিউমেরিক টাইপ হতে পারে, সেগুলোতে টাইপ নির্দিষ্ট করার জন্য একটি টাইপ সাফিক্স, যেমন 57u8
, অনুমোদিত। নাম্বার লিটারেলগুলোতে সংখ্যাটি সহজে পড়ার জন্য একটি ভিজ্যুয়াল সেপারেটর হিসাবে _
ব্যবহার করা যেতে পারে, যেমন 1_000
, যার মান 1000
নির্দিষ্ট করার মতোই হবে।
সারণী ৩-২: রাস্ট-এ ইন্টিজার লিটারেল
নাম্বার লিটারেল | উদাহরণ |
---|---|
ডেসিমেল | 98_222 |
হেক্স | 0xff |
অক্টাল | 0o77 |
বাইনারি | 0b1111_0000 |
বাইট (u8 কেবল) | b'A' |
তাহলে আপনি কীভাবে জানবেন কোন ধরনের ইন্টিজার ব্যবহার করবেন? আপনি যদি অনিশ্চিত হন, রাস্টের ডিফল্টগুলো সাধারণত শুরু করার জন্য ভাল জায়গা: ইন্টিজার টাইপগুলো ডিফল্টভাবে i32
হয়। isize
বা usize
ব্যবহার করার প্রাথমিক পরিস্থিতি হল কোনো ধরনের কালেকশন ইনডেক্স করার সময়।
ইন্টিজার ওভারফ্লো (Integer Overflow)
ধরা যাক আপনার কাছে
u8
টাইপের একটি ভ্যারিয়েবল আছে যা ০ থেকে ২৫৫ এর মধ্যে মান ধারণ করতে পারে। আপনি যদি ভ্যারিয়েবলটিকে সেই পরিসরের বাইরের কোনো মানে পরিবর্তন করার চেষ্টা করেন, যেমন ২৫৬, তাহলে ইন্টিজার ওভারফ্লো ঘটবে, যা দুটি আচরণের একটির কারণ হতে পারে। আপনি যখন ডিবাগ মোডে কম্পাইল করছেন, তখন রাস্ট ইন্টিজার ওভারফ্লোর জন্য চেক অন্তর্ভুক্ত করে যা এই আচরণ ঘটলে আপনার প্রোগ্রামকে রানটাইমে প্যানিক (panic) করায়। রাস্ট প্যানিকিং শব্দটি ব্যবহার করে যখন একটি প্রোগ্রাম একটি এরর সহ বন্ধ হয়ে যায়; আমরা অধ্যায় ৯-এর "Unrecoverable Errors withpanic!
" বিভাগে প্যানিক নিয়ে আরও गहराई से আলোচনা করব।আপনি যখন
--release
ফ্ল্যাগ দিয়ে রিলিজ মোডে কম্পাইল করছেন, তখন রাস্ট প্যানিক সৃষ্টিকারী ইন্টিজার ওভারফ্লোর জন্য চেক অন্তর্ভুক্ত করে না। পরিবর্তে, যদি ওভারফ্লো ঘটে, রাস্ট টু'স কমপ্লিমেন্ট র্যাপিং (two’s complement wrapping) করে। সংক্ষেপে, টাইপটি যে সর্বোচ্চ মান ধারণ করতে পারে তার চেয়ে বড় মানগুলো "র্যাপ অ্যারাউন্ড" করে টাইপটি যে সর্বনিম্ন মান ধারণ করতে পারে সেখানে চলে আসে। একটিu8
-এর ক্ষেত্রে, মান ২৫৬ হয়ে যায় ০, মান ২৫৭ হয়ে যায় ১, এবং এভাবেই চলতে থাকে। প্রোগ্রামটি প্যানিক করবে না, কিন্তু ভ্যারিয়েবলটির একটি এমন মান থাকবে যা সম্ভবত আপনি যা আশা করেছিলেন তা নয়। ইন্টিজার ওভারফ্লোর র্যাপিং আচরণের উপর নির্ভর করা একটি এরর হিসাবে বিবেচিত হয়।ওভারফ্লোর সম্ভাবনা স্পষ্টভাবে পরিচালনা করার জন্য, আপনি প্রিমিটিভ নিউমেরিক টাইপগুলোর জন্য স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত এই মেথডগুলোর পরিবার ব্যবহার করতে পারেন:
wrapping_*
মেথড, যেমনwrapping_add
, দিয়ে সমস্ত মোডে র্যাপ করুন।checked_*
মেথড দিয়ে ওভারফ্লো হলেNone
মান প্রদান করুন।overflowing_*
মেথড দিয়ে মান এবং একটি বুলিয়ান যা নির্দেশ করে ওভারফ্লো হয়েছে কিনা তা প্রদান করুন।saturating_*
মেথড দিয়ে মানের সর্বনিম্ন বা সর্বোচ্চ মানে স্যাচুরেট করুন।
ফ্লোটিং-পয়েন্ট টাইপ (Floating-Point Types)
রাস্ট-এ ফ্লোটিং-পয়েন্ট নাম্বার, যা দশমিক বিন্দুযুক্ত সংখ্যা, এর জন্য দুটি প্রিমিটিভ টাইপও রয়েছে। রাস্টের ফ্লোটিং-পয়েন্ট টাইপগুলো হল f32
এবং f64
, যা যথাক্রমে ৩২ বিট এবং ৬৪ বিট আকারের। ডিফল্ট টাইপ হল f64
কারণ আধুনিক সিপিইউগুলিতে, এটি f32
-এর মতো প্রায় একই গতির কিন্তু আরও বেশি নির্ভুলতা (precision) দিতে সক্ষম। সমস্ত ফ্লোটিং-পয়েন্ট টাইপ সাইনড।
এখানে একটি উদাহরণ যা ফ্লোটিং-পয়েন্ট নাম্বারগুলোকে কার্যকর অবস্থায় দেখায়:
ফাইলের নাম: src/main.rs
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
ফ্লোটিং-পয়েন্ট নাম্বারগুলো IEEE-754 স্ট্যান্ডার্ড অনুযায়ী উপস্থাপিত হয়।
নিউমেরিক অপারেশনস (Numeric Operations)
রাস্ট সমস্ত নাম্বার টাইপের জন্য আপনার প্রত্যাশিত মৌলিক গাণিতিক অপারেশনগুলো সমর্থন করে: যোগ, বিয়োগ, গুণ, ভাগ এবং ভাগশেষ (remainder)। ইন্টিজার ডিভিশন নিকটতম পূর্ণসংখ্যার দিকে শূন্যের দিকে ছেঁটে (truncates) ফেলে। নিম্নলিখিত কোডটি দেখায় যে আপনি কীভাবে একটি let
স্টেটমেন্টে প্রতিটি নিউমেরিক অপারেশন ব্যবহার করবেন:
ফাইলের নাম: src/main.rs
fn main() { // addition let sum = 5 + 10; // subtraction let difference = 95.5 - 4.3; // multiplication let product = 4 * 30; // division let quotient = 56.7 / 32.2; let truncated = -5 / 3; // Results in -1 // remainder let remainder = 43 % 5; }
এই স্টেটমেন্টগুলোর প্রতিটি এক্সপ্রেশন একটি গাণিতিক অপারেটর ব্যবহার করে এবং একটি একক মানে মূল্যায়ন করে, যা তারপর একটি ভ্যারিয়েবলের সাথে বাইন্ড করা হয়। পরিশিষ্ট বি তে রাস্ট দ্বারা প্রদত্ত সমস্ত অপারেটরের একটি তালিকা রয়েছে।
বুলিয়ান টাইপ (The Boolean Type)
অন্যান্য বেশিরভাগ প্রোগ্রামিং ল্যাঙ্গুয়েজের মতোই, রাস্ট-এ একটি বুলিয়ান টাইপের দুটি সম্ভাব্য মান রয়েছে: true
এবং false
। বুলিয়ানগুলো এক বাইট আকারের। রাস্ট-এ বুলিয়ান টাইপ bool
ব্যবহার করে নির্দিষ্ট করা হয়। উদাহরণস্বরূপ:
ফাইলের নাম: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation }
বুলিয়ান মান ব্যবহার করার প্রধান উপায় হল কন্ডিশনাল, যেমন একটি if
এক্সপ্রেশন। আমরা "কন্ট্রোল ফ্লো" বিভাগে রাস্ট-এ if
এক্সপ্রেশন কীভাবে কাজ করে তা কভার করব।
ক্যারেক্টার টাইপ (The Character Type)
রাস্টের char
টাইপ হল ভাষার সবচেয়ে আদিম বর্ণানুক্রমিক টাইপ। এখানে char
মান ঘোষণা করার কিছু উদাহরণ দেওয়া হল:
ফাইলের নাম: src/main.rs
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
লক্ষ্য করুন যে আমরা char
লিটারেলগুলোকে একক উদ্ধৃতি (single quotes) দিয়ে নির্দিষ্ট করি, স্ট্রিং লিটারেলের বিপরীতে, যা দ্বৈত উদ্ধৃতি (double quotes) ব্যবহার করে। রাস্টের char
টাইপ চার বাইট আকারের এবং একটি ইউনিকোড স্কেলার মান (Unicode Scalar Value) উপস্থাপন করে, যার মানে এটি শুধু ASCII-এর চেয়ে অনেক বেশি কিছু উপস্থাপন করতে পারে। অ্যাকসেন্টেড অক্ষর; চীনা, জাপানি এবং কোরিয়ান অক্ষর; ইমোজি; এবং শূন্য-প্রস্থের স্পেস সবই রাস্ট-এ বৈধ char
মান। ইউনিকোড স্কেলার মান U+0000
থেকে U+D7FF
এবং U+E000
থেকে U+10FFFF
পর্যন্ত অন্তর্ভুক্ত। তবে, ইউনিকোডে "ক্যারেক্টার" realmente একটি ধারণা নয়, তাই "ক্যারেক্টার" বলতে আপনার মানবিক অনুভূতি রাস্ট-এ একটি char
যা তার সাথে নাও মিলতে পারে। আমরা এই বিষয়টি অধ্যায় ৮-এর "Storing UTF-8 Encoded Text with Strings" বিভাগে বিস্তারিতভাবে আলোচনা করব।
কম্পাউন্ড টাইপ (Compound Types)
কম্পাউন্ড টাইপ একাধিক মানকে একটি টাইপে গ্রুপ করতে পারে। রাস্টের দুটি প্রিমিটিভ কম্পাউন্ড টাইপ রয়েছে: টাপল (tuples) এবং অ্যারে (arrays)।
টাপল টাইপ (The Tuple Type)
একটি টাপল হল বিভিন্ন টাইপের একাধিক মানকে একটি কম্পাউন্ড টাইপে একত্রিত করার একটি সাধারণ উপায়। টাপলগুলোর একটি নির্দিষ্ট দৈর্ঘ্য থাকে: একবার ঘোষণা করা হলে, তারা আকারে বাড়তে বা কমতে পারে না।
আমরা প্রথম বন্ধনীর ভিতরে কমা দ্বারা পৃথক করা মানের একটি তালিকা লিখে একটি টাপল তৈরি করি। টাপলের প্রতিটি অবস্থানের একটি টাইপ থাকে, এবং টাপলের বিভিন্ন মানের টাইপ একই হতে হবে এমন কোনো কথা নেই। আমরা এই উদাহরণে ঐচ্ছিক টাইপ অ্যানোটেশন যোগ করেছি:
ফাইলের নাম: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
tup
ভ্যারিয়েবলটি পুরো টাপলের সাথে বাইন্ড করে কারণ একটি টাপলকে একটি একক কম্পাউন্ড উপাদান হিসাবে বিবেচনা করা হয়। একটি টাপল থেকে পৃথক মানগুলো পেতে, আমরা প্যাটার্ন ম্যাচিং ব্যবহার করে একটি টাপল মানকে ডিস্ট্রাকচার (destructure) করতে পারি, এভাবে:
ফাইলের নাম: src/main.rs
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}"); }
এই প্রোগ্রামটি প্রথমে একটি টাপল তৈরি করে এবং এটিকে tup
ভ্যারিয়েবলের সাথে বাইন্ড করে। তারপরে এটি let
এর সাথে একটি প্যাটার্ন ব্যবহার করে tup
কে নিয়ে তিনটি পৃথক ভ্যারিয়েবল, x
, y
, এবং z
তে পরিণত করে। একে ডিস্ট্রাকচারিং বলা হয় কারণ এটি একক টাপলকে তিনটি অংশে ভেঙে দেয়। অবশেষে, প্রোগ্রামটি y
-এর মান প্রিন্ট করে, যা 6.4
।
আমরা একটি পিরিয়ড (.
) এবং তারপরে আমরা যে মানটি অ্যাক্সেস করতে চাই তার ইনডেক্স ব্যবহার করে সরাসরি একটি টাপল উপাদান অ্যাক্সেস করতে পারি। উদাহরণস্বরূপ:
ফাইলের নাম: src/main.rs
fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; }
এই প্রোগ্রামটি x
টাপল তৈরি করে এবং তারপরে তাদের নিজ নিজ ইনডেক্স ব্যবহার করে টাপলের প্রতিটি উপাদান অ্যাক্সেস করে। বেশিরভাগ প্রোগ্রামিং ল্যাঙ্গুয়েজের মতোই, একটি টাপলের প্রথম ইনডেক্স হল ০।
কোনো মান ছাড়াই টাপলটির একটি বিশেষ নাম আছে, ইউনিট। এই মান এবং এর সংশ্লিষ্ট টাইপ উভয়ই ()
লেখা হয় এবং একটি খালি মান বা একটি খালি রিটার্ন টাইপ উপস্থাপন করে। এক্সপ্রেশনগুলো যদি অন্য কোনো মান রিটার্ন না করে তবে তারা অন্তর্নিহিতভাবে ইউনিট মান রিটার্ন করে।
অ্যারে টাইপ (The Array Type)
একাধিক মানের একটি সংগ্রহ থাকার আরেকটি উপায় হল একটি অ্যারে। একটি টাপলের বিপরীতে, একটি অ্যারের প্রতিটি উপাদানের একই টাইপ থাকতে হবে। অন্য কিছু ভাষার অ্যারের বিপরীতে, রাস্ট-এ অ্যারেগুলোর একটি নির্দিষ্ট দৈর্ঘ্য থাকে।
আমরা একটি অ্যারের মানগুলো বর্গাকার বন্ধনীর ভিতরে কমা দ্বারা পৃথক করা তালিকা হিসাবে লিখি:
ফাইলের নাম: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
অ্যারেগুলো দরকারী যখন আপনি চান আপনার ডেটা স্ট্যাকে (stack) বরাদ্দ করা হোক, যেমন আমরা এখন পর্যন্ত দেখেছি অন্যান্য টাইপের মতো, হিপের (heap) পরিবর্তে (আমরা অধ্যায় ৪-এ স্ট্যাক এবং হিপ নিয়ে আরও আলোচনা করব) অথবা যখন আপনি নিশ্চিত করতে চান যে আপনার কাছে সর্বদা একটি নির্দিষ্ট সংখ্যক উপাদান রয়েছে। একটি অ্যারে ভেক্টর টাইপের মতো নমনীয় নয়। একটি ভেক্টর হল স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত একটি অনুরূপ সংগ্রহ টাইপ যা আকারে বাড়তে বা কমতে পারে কারণ এর বিষয়বস্তু হিপে থাকে। আপনি যদি অ্যারে বা ভেক্টর ব্যবহার করবেন কিনা তা নিয়ে অনিশ্চিত হন, তবে সম্ভাবনা হল আপনার একটি ভেক্টর ব্যবহার করা উচিত। অধ্যায় ৮ ভেক্টর নিয়ে আরও বিস্তারিত আলোচনা করে।
তবে, অ্যারেগুলো আরও বেশি দরকারী যখন আপনি জানেন যে উপাদানের সংখ্যা পরিবর্তন করার প্রয়োজন হবে না। উদাহরণস্বরূপ, আপনি যদি একটি প্রোগ্রামে মাসের নাম ব্যবহার করেন, তবে আপনি সম্ভবত একটি ভেক্টরর পরিবর্তে একটি অ্যারে ব্যবহার করবেন কারণ আপনি জানেন যে এটিতে সর্বদা ১২টি উপাদান থাকবে:
#![allow(unused)] fn main() { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
আপনি একটি অ্যারের টাইপ বর্গাকার বন্ধনী ব্যবহার করে প্রতিটি উপাদানের টাইপ, একটি সেমিকোলন এবং তারপরে অ্যারের উপাদানের সংখ্যা দিয়ে লিখেন, এভাবে:
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }
এখানে, i32
প্রতিটি উপাদানের টাইপ। সেমিকোলনের পরে, 5
সংখ্যাটি নির্দেশ করে যে অ্যারেটিতে পাঁচটি উপাদান রয়েছে।
আপনি প্রতিটি উপাদানের জন্য একই মান ধারণ করার জন্য একটি অ্যারে ইনিশিয়ালাইজ করতে পারেন, প্রাথমিক মান নির্দিষ্ট করে, তারপর একটি সেমিকোলন, এবং তারপর বর্গাকার বন্ধনীতে অ্যারের দৈর্ঘ্য, যেমনটি এখানে দেখানো হয়েছে:
#![allow(unused)] fn main() { let a = [3; 5]; }
a
নামের অ্যারেটিতে 5
টি উপাদান থাকবে যা সব প্রাথমিকভাবে 3
মানে সেট করা হবে। এটি let a = [3, 3, 3, 3, 3];
লেখার মতোই কিন্তু আরও সংক্ষিপ্ত উপায়ে।
অ্যারে উপাদান অ্যাক্সেস করা
একটি অ্যারে হল একটি পরিচিত, নির্দিষ্ট আকারের মেমরির একক খণ্ড যা স্ট্যাকে বরাদ্দ করা যেতে পারে। আপনি ইনডেক্সিং ব্যবহার করে একটি অ্যারের উপাদান অ্যাক্সেস করতে পারেন, এভাবে:
ফাইলের নাম: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
এই উদাহরণে, first
নামের ভ্যারিয়েবলটি 1
মান পাবে কারণ এটি অ্যারের [0]
ইনডেক্সে থাকা মান। second
নামের ভ্যারিয়েবলটি অ্যারের [1]
ইনডেক্স থেকে 2
মান পাবে।
অবৈধ অ্যারে উপাদান অ্যাক্সেস
চলুন দেখি আপনি যদি একটি অ্যারের শেষ প্রান্তের বাইরের কোনো উপাদান অ্যাক্সেস করার চেষ্টা করেন তবে কী ঘটে। ধরা যাক আপনি অধ্যায় ২-এর অনুমান করার গেমের মতো এই কোডটি চালান, ব্যবহারকারীর কাছ থেকে একটি অ্যারে ইনডেক্স পেতে:
ফাইলের নাম: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
এই কোডটি সফলভাবে কম্পাইল হয়। আপনি যদি cargo run
ব্যবহার করে এই কোডটি চালান এবং 0
, 1
, 2
, 3
, বা 4
প্রবেশ করান, প্রোগ্রামটি অ্যারের সেই ইনডেক্সে থাকা সংশ্লিষ্ট মানটি প্রিন্ট করবে। আপনি যদি পরিবর্তে অ্যারের শেষের বাইরের কোনো সংখ্যা প্রবেশ করান, যেমন 10
, আপনি এই ধরনের আউটপুট দেখতে পাবেন:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
ইনডেক্সিং অপারেশনে একটি অবৈধ মান ব্যবহার করার সময় প্রোগ্রামটি একটি রানটাইম এররের সম্মুখীন হয়েছিল। প্রোগ্রামটি একটি এরর বার্তা দিয়ে প্রস্থান করেছে এবং চূড়ান্ত println!
স্টেটমেন্টটি কার্যকর করেনি। আপনি যখন ইনডেক্সিং ব্যবহার করে একটি উপাদান অ্যাক্সেস করার চেষ্টা করেন, রাস্ট পরীক্ষা করবে যে আপনি যে ইনডেক্সটি নির্দিষ্ট করেছেন তা অ্যারের দৈর্ঘ্যের চেয়ে কম কিনা। যদি ইনডেক্সটি দৈর্ঘ্যের চেয়ে বড় বা সমান হয়, রাস্ট প্যানিক করবে। এই পরীক্ষাটি রানটাইমে হতে হবে, বিশেষ করে এই ক্ষেত্রে, কারণ কম্পাইলারের পক্ষে সম্ভব নয় জানা যে একজন ব্যবহারকারী পরে কোডটি চালানোর সময় কী মান প্রবেশ করাবে।
এটি রাস্টের মেমরি সেফটি নীতির একটি উদাহরণ। অনেক নিম্ন-স্তরের ভাষায়, এই ধরনের পরীক্ষা করা হয় না, এবং যখন আপনি একটি ভুল ইনডেক্স প্রদান করেন, তখন অবৈধ মেমরি অ্যাক্সেস করা হতে পারে। রাস্ট আপনাকে এই ধরনের ত্রুটির বিরুদ্ধে সুরক্ষা দেয়, মেমরি অ্যাক্সেসের অনুমতি দিয়ে এবং চালিয়ে যাওয়ার পরিবর্তে অবিলম্বে প্রস্থান করে। অধ্যায় ৯ রাস্টের এরর হ্যান্ডলিং সম্পর্কে আরও আলোচনা করে এবং কীভাবে আপনি পঠনযোগ্য, নিরাপদ কোড লিখতে পারেন যা প্যানিকও করে না বা অবৈধ মেমরি অ্যাক্সেসের অনুমতিও দেয় না।
ফাংশন (Functions)
রাস্ট কোডে ফাংশনের ব্যাপক ব্যবহার দেখা যায়। আপনি ইতিমধ্যে ভাষার সবচেয়ে গুরুত্বপূর্ণ ফাংশনগুলোর মধ্যে একটি দেখেছেন: main
ফাংশন, যা অনেক প্রোগ্রামের প্রবেশ বিন্দু (entry point)। আপনি fn
কীওয়ার্ডও দেখেছেন, যা আপনাকে নতুন ফাংশন ঘোষণা করার সুযোগ দেয়।
রাস্ট কোড ফাংশন এবং ভ্যারিয়েবলের নামের জন্য প্রচলিত শৈলী হিসাবে স্নেক কেস (snake case) ব্যবহার করে, যেখানে সমস্ত অক্ষর ছোট হাতের হয় এবং শব্দগুলোকে আন্ডারস্কোর দিয়ে আলাদা করা হয়। এখানে একটি প্রোগ্রাম রয়েছে যাতে একটি উদাহরণ ফাংশন সংজ্ঞা রয়েছে:
ফাইলের নাম: src/main.rs
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); }
আমরা রাস্ট-এ একটি ফাংশন সংজ্ঞায়িত করি fn
লিখে, তারপরে ফাংশনের নাম এবং একজোড়া প্রথম বন্ধনী। কোঁকড়া বন্ধনী (curly brackets) কম্পাইলারকে বলে দেয় যে ফাংশন বডি কোথায় শুরু এবং শেষ হয়েছে।
আমরা আমাদের সংজ্ঞায়িত যেকোনো ফাংশনকে তার নাম এবং তারপরে একজোড়া প্রথম বন্ধনী লিখে কল করতে পারি। যেহেতু another_function
প্রোগ্রামে সংজ্ঞায়িত করা হয়েছে, তাই এটি main
ফাংশনের ভিতর থেকে কল করা যেতে পারে। লক্ষ্য করুন যে আমরা সোর্স কোডে main
ফাংশনের পরে another_function
সংজ্ঞায়িত করেছি; আমরা এটি আগেও সংজ্ঞায়িত করতে পারতাম। রাস্ট আপনার ফাংশনগুলো কোথায় সংজ্ঞায়িত করা হয়েছে তা নিয়ে চিন্তা করে না, কেবল এটি দেখে যে সেগুলো কলারের দ্বারা দেখা যায় এমন একটি স্কোপে সংজ্ঞায়িত করা হয়েছে কিনা।
চলুন ফাংশন সম্পর্কে আরও অন্বেষণ করতে functions নামে একটি নতুন বাইনারি প্রজেক্ট শুরু করি। src/main.rs ফাইলে another_function
উদাহরণটি রাখুন এবং এটি চালান। আপনার নিম্নলিখিত আউটপুট দেখতে পাওয়া উচিত:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
লাইনগুলো main
ফাংশনে যেভাবে প্রদর্শিত হয়েছে সেই ক্রমে কার্যকর হয়। প্রথমে "Hello, world!" বার্তাটি প্রিন্ট হয়, এবং তারপর another_function
কল করা হয় এবং এর বার্তাটি প্রিন্ট হয়।
প্যারামিটার (Parameters)
আমরা ফাংশনগুলোকে প্যারামিটার (parameters) সহ সংজ্ঞায়িত করতে পারি, যা ফাংশনের সিগনেচারের অংশ হিসাবে বিশেষ ভ্যারিয়েবল। যখন একটি ফাংশনে প্যারামিটার থাকে, আপনি সেই প্যারামিটারগুলোর জন্য এটিকে নির্দিষ্ট মান (concrete values) সরবরাহ করতে পারেন। প্রযুক্তিগতভাবে, নির্দিষ্ট মানগুলোকে আর্গুমেন্ট (arguments) বলা হয়, কিন্তু সাধারণ কথোপকথনে, লোকেরা প্যারামিটার এবং আর্গুমেন্ট শব্দ দুটিকে ফাংশনের সংজ্ঞায় ভ্যারিয়েবল বা ফাংশন কল করার সময় পাস করা নির্দিষ্ট মানগুলোর জন্য अदলবদল করে ব্যবহার করে।
another_function
-এর এই সংস্করণে আমরা একটি প্যারামিটার যোগ করি:
ফাইলের নাম: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {x}"); }
এই প্রোগ্রামটি চালানোর চেষ্টা করুন; আপনার নিম্নলিখিত আউটপুট পাওয়া উচিত:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_function
-এর ঘোষণায় x
নামে একটি প্যারামিটার রয়েছে। x
-এর টাইপ i32
হিসাবে নির্দিষ্ট করা হয়েছে। যখন আমরা another_function
-এ 5
পাস করি, println!
ম্যাক্রো ফরম্যাট স্ট্রিং-এ x
ধারণকারী কোঁকড়া বন্ধনীর জোড়ার জায়গায় 5
রাখে।
ফাংশনের সিগনেচারে, আপনাকে অবশ্যই প্রতিটি প্যারামিটারের টাইপ ঘোষণা করতে হবে। এটি রাস্টের ডিজাইনের একটি ইচ্ছাকৃত সিদ্ধান্ত: ফাংশন সংজ্ঞায় টাইপ অ্যানোটেশন প্রয়োজন হওয়ায় কম্পাইলারকে প্রায় কখনোই কোডের অন্য কোথাও আপনার কী টাইপ বোঝাতে চাইছেন তা বের করতে আপনার সাহায্য নিতে হয় না। কম্পাইলার আরও সহায়ক এরর বার্তা দিতে সক্ষম হয় যদি সে জানে ফাংশনটি কোন টাইপ আশা করছে।
একাধিক প্যারামিটার সংজ্ঞায়িত করার সময়, প্যারামিটার ঘোষণাগুলোকে কমা দিয়ে আলাদা করুন, যেমন:
ফাইলের নাম: src/main.rs
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {value}{unit_label}"); }
এই উদাহরণটি print_labeled_measurement
নামে একটি ফাংশন তৈরি করে যার দুটি প্যারামিটার রয়েছে। প্রথম প্যারামিটারের নাম value
এবং এটি একটি i32
। দ্বিতীয়টির নাম unit_label
এবং এর টাইপ char
। ফাংশনটি তারপর value
এবং unit_label
উভয়ই ধারণকারী টেক্সট প্রিন্ট করে।
চলুন এই কোডটি চালানোর চেষ্টা করি। আপনার functions প্রজেক্টের src/main.rs ফাইলের বর্তমান প্রোগ্রামটিকে পূর্ববর্তী উদাহরণ দিয়ে প্রতিস্থাপন করুন এবং cargo run
ব্যবহার করে এটি চালান:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h```
যেহেতু আমরা `value`-এর জন্য `5` এবং `unit_label`-এর জন্য `'h'` মান দিয়ে ফাংশনটি কল করেছি, তাই প্রোগ্রামের আউটপুটে সেই মানগুলো রয়েছে।
### স্টেটমেন্ট এবং এক্সপ্রেশন (Statements and Expressions)
ফাংশন বডি একটি সিরিজের স্টেটমেন্ট (statements) দিয়ে গঠিত যা ঐচ্ছিকভাবে একটি এক্সপ্রেশন (expression) দিয়ে শেষ হতে পারে। এখন পর্যন্ত, আমরা যে ফাংশনগুলো দেখেছি সেগুলিতে একটি সমাপ্তি এক্সপ্রেশন অন্তর্ভুক্ত ছিল না, তবে আপনি একটি স্টেটমেন্টের অংশ হিসাবে একটি এক্সপ্রেশন দেখেছেন। যেহেতু রাস্ট একটি এক্সপ্রেশন-ভিত্তিক ভাষা, তাই এটি বোঝা একটি গুরুত্বপূর্ণ পার্থক্য। অন্যান্য ভাষার একই পার্থক্য নেই, তাই চলুন দেখি স্টেটমেন্ট এবং এক্সপ্রেশন কী এবং তাদের পার্থক্য ফাংশনের বডিকে কীভাবে প্রভাবিত করে।
- স্টেটমেন্ট হলো এমন নির্দেশ যা কোনো কাজ সম্পাদন করে এবং কোনো মান রিটার্ন করে না।
- এক্সপ্রেশন একটি ফলস্বরূপ মানে (resultant value) রূপান্তরিত হয়।
চলুন কিছু উদাহরণ দেখি।
আমরা আসলে ইতিমধ্যে স্টেটমেন্ট এবং এক্সপ্রেশন ব্যবহার করেছি। `let` কীওয়ার্ড দিয়ে একটি ভ্যারিয়েবল তৈরি করা এবং তাতে একটি মান অ্যাসাইন করা একটি স্টেটমেন্ট। লিস্টিং ৩-১-এ, `let y = 6;` একটি স্টেটমেন্ট।
<Listing number="3-1" file-name="src/main.rs" caption="একটি স্টেটমেন্ট ধারণকারী `main` ফাংশনের ঘোষণা">
```rust
fn main() {
let y = 6;
}
ফাংশন সংজ্ঞাও স্টেটমেন্ট; পুরো পূর্ববর্তী উদাহরণটি নিজেই একটি স্টেটমেন্ট। (যেমন আমরা নিচে দেখব, একটি ফাংশন কল করা একটি স্টেটমেন্ট নয়, যদিও।)
স্টেটমেন্ট মান রিটার্ন করে না। অতএব, আপনি একটি let
স্টেটমেন্টকে অন্য ভ্যারিয়েবলে অ্যাসাইন করতে পারবেন না, যেমন নিম্নলিখিত কোডটি করার চেষ্টা করে; আপনি একটি এরর পাবেন:
ফাইলের নাম: src/main.rs
fn main() {
let x = (let y = 6);
}
যখন আপনি এই প্রোগ্রামটি চালান, তখন আপনি যে এররটি পাবেন তা এইরকম দেখায়:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
let y = 6
স্টেটমেন্টটি কোনো মান রিটার্ন করে না, তাই x
-এর বাইন্ড করার জন্য কিছু নেই। এটি অন্যান্য ভাষায় যা ঘটে তার থেকে ভিন্ন, যেমন C এবং Ruby, যেখানে অ্যাসাইনমেন্টটি অ্যাসাইনমেন্টের মান রিটার্ন করে। সেই ভাষাগুলিতে, আপনি x = y = 6
লিখতে পারেন এবং x
এবং y
উভয়েরই মান 6
হতে পারে; রাস্ট-এ তা হয় না।
এক্সপ্রেশন একটি মানে রূপান্তরিত হয় এবং আপনি রাস্ট-এ যে বাকি কোড লিখবেন তার বেশিরভাগই তৈরি করে। একটি গণিত অপারেশন বিবেচনা করুন, যেমন 5 + 6
, যা একটি এক্সপ্রেশন যা 11
মানে রূপান্তরিত হয়। এক্সপ্রেশন স্টেটমেন্টের অংশ হতে পারে: লিস্টিং ৩-১-এ, let y = 6;
স্টেটমেন্টের 6
একটি এক্সপ্রেশন যা 6
মানে রূপান্তরিত হয়। একটি ফাংশন কল করা একটি এক্সপ্রেশন। একটি ম্যাক্রো কল করা একটি এক্সপ্রেশন। কোঁকড়া বন্ধনী দিয়ে তৈরি একটি নতুন স্কোপ ব্লক একটি এক্সপ্রেশন, উদাহরণস্বরূপ:
ফাইলের নাম: src/main.rs
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {y}"); }
এই এক্সপ্রেশনটি:
{
let x = 3;
x + 1
}
একটি ব্লক যা, এই ক্ষেত্রে, 4
-এ রূপান্তরিত হয়। সেই মানটি let
স্টেটমেন্টের অংশ হিসাবে y
-এর সাথে বাইন্ড করা হয়। লক্ষ্য করুন যে x + 1
লাইনের শেষে একটি সেমিকোলন নেই, যা আপনি এখন পর্যন্ত দেখা বেশিরভাগ লাইনের থেকে ভিন্ন। এক্সপ্রেশনের শেষে সেমিকোলন থাকে না। যদি আপনি একটি এক্সপ্রেশনের শেষে সেমিকোলন যোগ করেন, তবে আপনি এটিকে একটি স্টেটমেন্টে পরিণত করেন, এবং এটি তখন কোনো মান রিটার্ন করবে না। ফাংশন রিটার্ন ভ্যালু এবং এক্সপ্রেশন অন্বেষণ করার সময় এটি মনে রাখবেন।
রিটার্ন ভ্যালু সহ ফাংশন (Functions with Return Values)
ফাংশনগুলো কল করা কোডে মান রিটার্ন করতে পারে। আমরা রিটার্ন ভ্যালুর নাম দিই না, তবে আমাদের অবশ্যই একটি তীরচিহ্ন (->
) এর পরে তাদের টাইপ ঘোষণা করতে হবে। রাস্ট-এ, ফাংশনের রিটার্ন ভ্যালু ফাংশনের বডির ব্লকের চূড়ান্ত এক্সপ্রেশনের মানের সমার্থক। আপনি return
কীওয়ার্ড ব্যবহার করে এবং একটি মান নির্দিষ্ট করে একটি ফাংশন থেকে আগেভাগে রিটার্ন করতে পারেন, তবে বেশিরভাগ ফাংশন শেষ এক্সপ্রেশনটি অন্তর্নিহিতভাবে রিটার্ন করে। এখানে একটি ফাংশনের উদাহরণ দেওয়া হল যা একটি মান রিটার্ন করে:
ফাইলের নাম: src/main.rs
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {x}"); }
five
ফাংশনে কোনো ফাংশন কল, ম্যাক্রো, বা এমনকি let
স্টেটমেন্টও নেই—শুধু 5
সংখ্যাটি নিজেই। এটি রাস্ট-এ একটি সম্পূর্ণ বৈধ ফাংশন। লক্ষ্য করুন যে ফাংশনের রিটার্ন টাইপও -> i32
হিসাবে নির্দিষ্ট করা হয়েছে। এই কোডটি চালানোর চেষ্টা করুন; আউটপুটটি এইরকম হওয়া উচিত:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
five
-এর 5
হল ফাংশনের রিটার্ন ভ্যালু, যার কারণে রিটার্ন টাইপ i32
। চলুন এটি আরও বিস্তারিতভাবে পরীক্ষা করি। দুটি গুরুত্বপূর্ণ বিষয় রয়েছে: প্রথমত, let x = five();
লাইনটি দেখায় যে আমরা একটি ফাংশনের রিটার্ন ভ্যালু ব্যবহার করে একটি ভ্যারিয়েবল ইনিশিয়ালাইজ করছি। যেহেতু five
ফাংশনটি একটি 5
রিটার্ন করে, তাই সেই লাইনটি নিম্নলিখিতটির সমান:
#![allow(unused)] fn main() { let x = 5; }
দ্বিতীয়ত, five
ফাংশনের কোনো প্যারামিটার নেই এবং রিটার্ন ভ্যালুর টাইপ সংজ্ঞায়িত করে, কিন্তু ফাংশনের বডিটি একটি একাকী 5
যার কোনো সেমিকোলন নেই কারণ এটি একটি এক্সপ্রেশন যার মান আমরা রিটার্ন করতে চাই।
চলুন আরেকটি উদাহরণ দেখি:
ফাইলের নাম: src/main.rs
fn main() { let x = plus_one(5); println!("The value of x is: {x}"); } fn plus_one(x: i32) -> i32 { x + 1 }
এই কোডটি চালালে The value of x is: 6
প্রিন্ট হবে। কিন্তু যদি আমরা x + 1
ধারণকারী লাইনের শেষে একটি সেমিকোলন রাখি, এটিকে একটি এক্সপ্রেশন থেকে একটি স্টেটমেন্টে পরিবর্তন করে, আমরা একটি এরর পাব:
ফাইলের নাম: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
এই কোডটি কম্পাইল করলে একটি এরর উৎপন্ন হয়, যেমন:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
প্রধান এরর বার্তা, mismatched types
(বেমানান টাইপ), এই কোডের মূল সমস্যাটি প্রকাশ করে। plus_one
ফাংশনের সংজ্ঞা বলছে যে এটি একটি i32
রিটার্ন করবে, কিন্তু স্টেটমেন্টগুলো কোনো মানে রূপান্তরিত হয় না, যা ()
(ইউনিট টাইপ) দ্বারা প্রকাশ করা হয়। অতএব, কিছুই রিটার্ন করা হয় না, যা ফাংশন সংজ্ঞার সাথে বিরোধিতা করে এবং একটি এররের কারণ হয়। এই আউটপুটে, রাস্ট সম্ভবত এই সমস্যাটি সংশোধন করতে সাহায্য করার জন্য একটি বার্তা প্রদান করে: এটি সেমিকোলনটি সরানোর পরামর্শ দেয়, যা এররটি ঠিক করবে।
কমেন্ট (Comments)
সব প্রোগ্রামাররাই তাদের কোডকে সহজবোধ্য করার চেষ্টা করেন, কিন্তু কখনও কখনও অতিরিক্ত ব্যাখ্যার প্রয়োজন হয়। এইসব ক্ষেত্রে, প্রোগ্রামাররা তাদের সোর্স কোডে কমেন্ট (comments) রেখে যান যা কম্পাইলার উপেক্ষা করবে কিন্তু যারা সোর্স কোড পড়বেন তাদের জন্য সহায়ক হতে পারে।
এখানে একটি সাধারণ কমেন্ট দেওয়া হলো:
#![allow(unused)] fn main() { // হ্যালো, ওয়ার্ল্ড }
রাস্ট-এ, প্রচলিত কমেন্ট স্টাইল দুটি স্ল্যাশ দিয়ে শুরু হয়, এবং কমেন্টটি লাইনের শেষ পর্যন্ত চলতে থাকে। যেসব কমেন্ট একাধিক লাইনে বিস্তৃত হয়, সেগুলোর জন্য আপনাকে প্রতিটি লাইনে //
অন্তর্ভুক্ত করতে হবে, যেমন:
#![allow(unused)] fn main() { // আমরা এখানে একটি জটিল কাজ করছি, যা এত দীর্ঘ যে আমাদের প্রয়োজন // এটি করার জন্য একাধিক লাইনের কমেন্ট! যাক! আশা করি, এই কমেন্টটি // ব্যাখ্যা করবে যে এখানে কী ঘটছে। }
কমেন্ট কোড ধারণকারী লাইনের শেষেও রাখা যেতে পারে:
ফাইলের নাম: src/main.rs
fn main() { let lucky_number = 7; // I'm feeling lucky today }
কিন্তু আপনি প্রায়শই সেগুলোকে এই ফর্ম্যাটে ব্যবহৃত হতে দেখবেন, যেখানে কমেন্টটি যে কোডকে ব্যাখ্যা করছে তার উপরের একটি পৃথক লাইনে থাকে:
ফাইলের নাম: src/main.rs
fn main() { // I'm feeling lucky today let lucky_number = 7; }
রাস্ট-এ আরও এক ধরনের কমেন্ট আছে, ডকুমেন্টেশন কমেন্ট, যা আমরা অধ্যায় ১৪-এর “Crates.io-তে একটি ক্রেইট পাবলিশ করা” বিভাগে আলোচনা করব।
কন্ট্রোল ফ্লো (Control Flow)
একটি শর্ত true
হলে কিছু কোড চালানো এবং একটি শর্ত true
থাকা অবস্থায় কিছু কোড বারবার চালানোর ক্ষমতা বেশিরভাগ প্রোগ্রামিং ভাষার মৌলিক ভিত্তি। রাস্ট কোডের এক্সিকিউশন ফ্লো নিয়ন্ত্রণ করার জন্য সবচেয়ে সাধারণ কনস্ট্রাক্টগুলো হল if
এক্সপ্রেশন এবং লুপ।
if
এক্সপ্রেশন (if Expressions)
একটি if
এক্সপ্রেশন আপনাকে শর্তের উপর ভিত্তি করে আপনার কোডকে বিভিন্ন শাখায় বিভক্ত করার সুযোগ দেয়। আপনি একটি শর্ত প্রদান করেন এবং তারপর বলেন, "যদি এই শর্তটি পূরণ হয়, তবে এই কোড ব্লকটি চালান। যদি শর্তটি পূরণ না হয়, তবে এই কোড ব্লকটি চালাবেন না।"
if
এক্সপ্রেশন অন্বেষণ করতে আপনার projects ডিরেক্টরিতে branches নামে একটি নতুন প্রজেক্ট তৈরি করুন। src/main.rs ফাইলে, নিম্নলিখিতটি ইনপুট করুন:
ফাইলের নাম: src/main.rs
fn main() { let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); } }
সমস্ত if
এক্সপ্রেশন if
কীওয়ার্ড দিয়ে শুরু হয়, তারপরে একটি শর্ত থাকে। এই ক্ষেত্রে, শর্তটি পরীক্ষা করে যে number
ভ্যারিয়েবলের মান ৫ এর চেয়ে কম কিনা। শর্তটি true
হলে কার্যকর করার জন্য কোড ব্লকটি আমরা শর্তের পরেই কোঁকড়া বন্ধনীর ভিতরে রাখি। if
এক্সপ্রেশনের শর্তগুলোর সাথে যুক্ত কোড ব্লকগুলোকে কখনও কখনও arms বলা হয়, ঠিক যেমন অধ্যায় ২-এর "অনুমানের সাথে গোপন সংখ্যার তুলনা" বিভাগে আলোচনা করা match
এক্সপ্রেশনের arm-গুলোর মতো।
ঐচ্ছিকভাবে, আমরা একটি else
এক্সপ্রেশনও অন্তর্ভুক্ত করতে পারি, যা আমরা এখানে করেছি, প্রোগ্রামটিকে একটি বিকল্প কোড ব্লক দেওয়ার জন্য যা শর্তটি false
হলে কার্যকর হবে। আপনি যদি একটি else
এক্সপ্রেশন সরবরাহ না করেন এবং শর্তটি false
হয়, প্রোগ্রামটি কেবল if
ব্লকটি এড়িয়ে যাবে এবং পরবর্তী কোডে চলে যাবে।
এই কোডটি চালানোর চেষ্টা করুন; আপনার নিম্নলিখিত আউটপুট দেখতে পাওয়া উচিত:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
চলুন number
-এর মান পরিবর্তন করে এমন একটি মান দিই যা শর্তটিকে false
করে তোলে, দেখি কী হয়:
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}```
প্রোগ্রামটি আবার চালান, এবং আউটপুট দেখুন:
```console
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false```
এটিও উল্লেখ করার মতো যে এই কোডের শর্তটি _অবশ্যই_ একটি `bool` হতে হবে। যদি শর্তটি `bool` না হয়, আমরা একটি এরর পাব। উদাহরণস্বরূপ, নিম্নলিখিত কোডটি চালানোর চেষ্টা করুন:
<span class="filename">ফাইলের নাম: src/main.rs</span>
```rust,ignore,does_not_compile
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
if
-এর শর্তটি এবার 3
মান দেয়, এবং রাস্ট একটি এরর দেখায়:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
এররটি নির্দেশ করে যে রাস্ট একটি bool
আশা করেছিল কিন্তু একটি ইন্টিজার পেয়েছে। রুবি এবং জাভাস্ক্রিপ্টের মতো ভাষার বিপরীতে, রাস্ট স্বয়ংক্রিয়ভাবে নন-বুলিয়ান টাইপকে বুলিয়ানে রূপান্তর করার চেষ্টা করবে না। আপনাকে অবশ্যই সুস্পষ্ট হতে হবে এবং if
-কে সর্বদা একটি বুলিয়ান শর্ত হিসাবে সরবরাহ করতে হবে। যদি আমরা চাই যে if
কোড ব্লকটি কেবল তখনই চলুক যখন একটি সংখ্যা 0
-এর সমান না হয়, উদাহরণস্বরূপ, আমরা if
এক্সপ্রেশনটিকে নিম্নলিখিতভাবে পরিবর্তন করতে পারি:
ফাইলের নাম: src/main.rs
fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); } }
এই কোডটি চালালে number was something other than zero
প্রিন্ট হবে।
else if
দিয়ে একাধিক শর্ত পরিচালনা করা
আপনি if
এবং else
-কে একটি else if
এক্সপ্রেশনে একত্রিত করে একাধিক শর্ত ব্যবহার করতে পারেন। উদাহরণস্বরূপ:
ফাইলের নাম: src/main.rs
fn main() { let number = 6; if number % 4 == 0 { println!("number is divisible by 4"); } else if number % 3 == 0 { println!("number is divisible by 3"); } else if number % 2 == 0 { println!("number is divisible by 2"); } else { println!("number is not divisible by 4, 3, or 2"); } }
এই প্রোগ্রামের চারটি সম্ভাব্য পথ রয়েছে। এটি চালানোর পরে, আপনার নিম্নলিখিত আউটপুট দেখতে পাওয়া উচিত:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
যখন এই প্রোগ্রামটি কার্যকর হয়, এটি প্রতিটি if
এক্সপ্রেশন পর্যায়ক্রমে পরীক্ষা করে এবং প্রথম যেটির শর্ত true
হয়, সেটির বডি কার্যকর করে। লক্ষ্য করুন যে যদিও 6, 2 দ্বারা বিভাজ্য, আমরা number is divisible by 2
আউটপুটটি দেখতে পাই না, বা আমরা else
ব্লক থেকে number is not divisible by 4, 3, or 2
টেক্সটটিও দেখতে পাই না। এর কারণ হল রাস্ট কেবল প্রথম true
শর্তের জন্য ব্লকটি কার্যকর করে, এবং একবার এটি একটি খুঁজে পেলে, এটি বাকিগুলো আর পরীক্ষাই করে না।
অনেক বেশি else if
এক্সপ্রেশন ব্যবহার করলে আপনার কোড অগোছালো হয়ে যেতে পারে, তাই আপনার যদি একাধিক থাকে, তবে আপনি আপনার কোড রিফ্যাক্টর করতে চাইতে পারেন। অধ্যায় ৬ এই ধরনের ক্ষেত্রে match
নামে একটি শক্তিশালী রাস্ট ব্রাঞ্চিং কনস্ট্রাক্ট বর্ণনা করে।
একটি let
স্টেটমেন্টে if
ব্যবহার করা
যেহেতু if
একটি এক্সপ্রেশন, তাই আমরা এটিকে একটি let
স্টেটমেন্টের ডানদিকে ব্যবহার করে ফলাফলটি একটি ভ্যারিয়েবলে অ্যাসাইন করতে পারি, যেমনটি লিস্টিং ৩-২-এ দেখানো হয়েছে।
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {number}"); }
number
ভ্যারিয়েবলটি if
এক্সপ্রেশনের ফলাফলের উপর ভিত্তি করে একটি মানের সাথে বাইন্ড করা হবে। কী ঘটে তা দেখতে এই কোডটি চালান:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
মনে রাখবেন যে কোড ব্লকগুলো তাদের শেষ এক্সপ্রেশনে রূপান্তরিত হয়, এবং সংখ্যাগুলো নিজেরাও এক্সপ্রেশন। এই ক্ষেত্রে, পুরো if
এক্সপ্রেশনের মান নির্ভর করে কোন কোড ব্লকটি কার্যকর হয় তার উপর। এর মানে হল if
-এর প্রতিটি arm থেকে ফলাফল হিসাবে আসার সম্ভাবনা থাকা মানগুলোর টাইপ অবশ্যই একই হতে হবে; লিস্টিং ৩-২-এ, if
arm এবং else
arm উভয়ের ফলাফলই i32
ইন্টিজার ছিল। যদি টাইপগুলো বেমানান হয়, যেমন নিম্নলিখিত উদাহরণে, আমরা একটি এরর পাব:
ফাইলের নাম: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
যখন আমরা এই কোডটি কম্পাইল করার চেষ্টা করব, আমরা একটি এরর পাব। if
এবং else
arm-গুলোর মানের টাইপ বেমানান, এবং রাস্ট প্রোগ্রামের ঠিক কোথায় সমস্যাটি খুঁজে পাওয়া যাবে তা নির্দেশ করে:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
if
ব্লকের এক্সপ্রেশনটি একটি ইন্টিজারে রূপান্তরিত হয়, এবং else
ব্লকের এক্সপ্রেশনটি একটি স্ট্রিং-এ রূপান্তরিত হয়। এটি কাজ করবে না কারণ ভ্যারিয়েবলগুলোর অবশ্যই একটি একক টাইপ থাকতে হবে, এবং রাস্টকে কম্পাইল করার সময় নির্দিষ্টভাবে জানতে হবে number
ভ্যারিয়েবলের টাইপ কী। number
-এর টাইপ জানা থাকলে কম্পাইলার যাচাই করতে পারে যে আমরা যেখানেই number
ব্যবহার করি সেখানে টাইপটি বৈধ। রাস্ট এটি করতে পারত না যদি number
-এর টাইপ কেবল রানটাইমে নির্ধারিত হত; কম্পাইলার আরও জটিল হত এবং কোড সম্পর্কে কম গ্যারান্টি দিত যদি তাকে যেকোনো ভ্যারিয়েবলের জন্য একাধিক কাল্পনিক টাইপের হিসাব রাখতে হত।
লুপ দিয়ে পুনরাবৃত্তি (Repetition with Loops)
প্রায়শই একটি কোড ব্লক একাধিকবার চালানো দরকার হয়। এই কাজের জন্য, রাস্ট বিভিন্ন লুপ (loops) সরবরাহ করে, যা লুপ বডির ভিতরের কোড শেষ পর্যন্ত চালাবে এবং তারপরে অবিলম্বে শুরুতে ফিরে যাবে। লুপ নিয়ে পরীক্ষা করার জন্য, চলুন loops নামে একটি নতুন প্রজেক্ট তৈরি করি।
রাস্টের তিন ধরনের লুপ রয়েছে: loop
, while
, এবং for
। চলুন প্রতিটি চেষ্টা করি।
loop
দিয়ে কোডের পুনরাবৃত্তি করা
loop
কীওয়ার্ড রাস্টকে বলে একটি কোড ব্লক বারবার চালাতে, চিরকালের জন্য অথবা যতক্ষণ না আপনি স্পষ্টভাবে এটিকে থামাতে বলেন।
উদাহরণস্বরূপ, আপনার loops ডিরেক্টরিতে src/main.rs ফাইলটি পরিবর্তন করে এইরকম করুন:
ফাইলের নাম: src/main.rs
fn main() {
loop {
println!("again!");
}
}
যখন আমরা এই প্রোগ্রামটি চালাব, আমরা দেখব again!
বারবার প্রিন্ট হচ্ছে যতক্ষণ না আমরা প্রোগ্রামটি ম্যানুয়ালি থামাই। বেশিরভাগ টার্মিনাল একটি অবিরাম লুপে আটকে থাকা প্রোগ্রামকে বাধা দেওয়ার জন্য কীবোর্ড শর্টকাট ctrl-c সমর্থন করে। চেষ্টা করে দেখুন:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
^C
প্রতীকটি নির্দেশ করে যেখানে আপনি ctrl-c চেপেছিলেন।
আপনি ^C
-এর পরে again!
শব্দটি দেখতেও পারেন বা নাও দেখতে পারেন, এটি নির্ভর করে কোডটি লুপের কোথায় ছিল যখন এটি ইন্টারাপ্ট সিগন্যাল পেয়েছিল।
সৌভাগ্যবশত, রাস্ট কোড ব্যবহার করে একটি লুপ থেকে বেরিয়ে আসার একটি উপায়ও সরবরাহ করে। আপনি লুপের মধ্যে break
কীওয়ার্ডটি রাখতে পারেন প্রোগ্রামকে বলতে যে কখন লুপ চালানো বন্ধ করতে হবে। মনে রাখবেন যে আমরা অধ্যায় ২-এর "সঠিক অনুমান করার পরে খেলা শেষ করা" বিভাগে এটি করেছিলাম, যখন ব্যবহারকারী সঠিক সংখ্যা অনুমান করে গেমটি জিতেছিল তখন প্রোগ্রাম থেকে বেরিয়ে আসার জন্য।
আমরা গেসিং গেমে continue
ব্যবহার করেছিলাম, যা একটি লুপে প্রোগ্রামকে বলে যে এই পুনরাবৃত্তির বাকি কোড এড়িয়ে গিয়ে পরবর্তী পুনরাবৃত্তিতে যেতে।
লুপ থেকে মান রিটার্ন করা
একটি loop
-এর একটি ব্যবহার হল এমন একটি অপারেশন পুনরায় চেষ্টা করা যা আপনি জানেন যে ব্যর্থ হতে পারে, যেমন একটি থ্রেড তার কাজ শেষ করেছে কিনা তা পরীক্ষা করা। আপনার সেই অপারেশনের ফলাফলটি লুপ থেকে বের করে আপনার বাকি কোডে পাস করারও প্রয়োজন হতে পারে। এটি করার জন্য, আপনি লুপ থামানোর জন্য যে break
এক্সপ্রেশন ব্যবহার করেন তার পরে আপনি যে মানটি রিটার্ন করতে চান তা যোগ করতে পারেন; সেই মানটি লুপ থেকে রিটার্ন করা হবে যাতে আপনি এটি ব্যবহার করতে পারেন, যেমনটি এখানে দেখানো হয়েছে:
fn main() { let mut counter = 0; let result = loop { counter += 1; if counter == 10 { break counter * 2; } }; println!("The result is {result}"); }
লুপের আগে, আমরা counter
নামে একটি ভ্যারিয়েবল ঘোষণা করি এবং এটিকে 0
দিয়ে ইনিশিয়ালাইজ করি। তারপরে আমরা লুপ থেকে রিটার্ন করা মানটি ধরে রাখার জন্য result
নামে একটি ভ্যারিয়েবল ঘোষণা করি। লুপের প্রতিটি পুনরাবৃত্তিতে, আমরা counter
ভ্যারিয়েবলে 1
যোগ করি, এবং তারপরে পরীক্ষা করি যে counter
10-এর সমান কিনা। যখন এটি হয়, আমরা counter * 2
মান সহ break
কীওয়ার্ড ব্যবহার করি। লুপের পরে, আমরা একটি সেমিকোলন ব্যবহার করি result
-এ মান অ্যাসাইন করা স্টেটমেন্টটি শেষ করতে। অবশেষে, আমরা result
-এর মান প্রিন্ট করি, যা এই ক্ষেত্রে 20
।
আপনি একটি লুপের ভিতর থেকে return
করতেও পারেন। যেখানে break
কেবল বর্তমান লুপ থেকে বেরিয়ে যায়, return
সর্বদা বর্তমান ফাংশন থেকে বেরিয়ে যায়।
একাধিক লুপের মধ্যে পার্থক্য করতে লুপ লেবেল
যদি আপনার লুপের ভিতরে লুপ থাকে, break
এবং continue
সেই মুহূর্তে সবচেয়ে ভিতরের লুপে প্রযোজ্য হয়। আপনি ঐচ্ছিকভাবে একটি লুপে একটি লুপ লেবেল (loop label) নির্দিষ্ট করতে পারেন যা আপনি তখন break
বা continue
এর সাথে ব্যবহার করতে পারেন নির্দিষ্ট করতে যে সেই কীওয়ার্ডগুলো সবচেয়ে ভিতরের লুপের পরিবর্তে লেবেলযুক্ত লুপে প্রযোজ্য। লুপ লেবেল অবশ্যই একটি একক উদ্ধৃতি চিহ্ন দিয়ে শুরু হতে হবে। এখানে দুটি নেস্টেড লুপের একটি উদাহরণ রয়েছে:
fn main() { let mut count = 0; 'counting_up: loop { println!("count = {count}"); let mut remaining = 10; loop { println!("remaining = {remaining}"); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {count}"); }
বাইরের লুপটির লেবেল 'counting_up
, এবং এটি 0 থেকে 2 পর্যন্ত গণনা করবে। লেবেল ছাড়া ভিতরের লুপটি 10 থেকে 9 পর্যন্ত গণনা করবে। প্রথম break
যা একটি লেবেল নির্দিষ্ট করে না, কেবল ভিতরের লুপ থেকে বেরিয়ে যাবে। break 'counting_up;
স্টেটমেন্টটি বাইরের লুপ থেকে বেরিয়ে যাবে। এই কোডটি প্রিন্ট করে:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while
দিয়ে শর্তাধীন লুপ
একটি প্রোগ্রামে প্রায়শই একটি লুপের মধ্যে একটি শর্ত মূল্যায়ন করার প্রয়োজন হয়। যতক্ষণ শর্তটি true
থাকে, লুপ চলে। যখন শর্তটি true
থাকা বন্ধ হয়ে যায়, প্রোগ্রামটি break
কল করে, লুপটি থামিয়ে দেয়। loop
, if
, else
, এবং break
-এর সংমিশ্রণ ব্যবহার করে এই ধরনের আচরণ বাস্তবায়ন করা সম্ভব; আপনি চাইলে এখন একটি প্রোগ্রামে এটি চেষ্টা করতে পারেন। তবে, এই প্যাটার্নটি এত সাধারণ যে রাস্টের এটির জন্য একটি অন্তর্নির্মিত ভাষা কনস্ট্রাক্ট রয়েছে, যাকে while
লুপ বলা হয়। লিস্টিং ৩-৩-এ, আমরা while
ব্যবহার করি প্রোগ্রামটিকে তিনবার লুপ করতে, প্রতিবার গণনা করে, এবং তারপরে, লুপের পরে, একটি বার্তা প্রিন্ট করে এবং প্রস্থান করে।
fn main() { let mut number = 3; while number != 0 { println!("{number}!"); number -= 1; } println!("LIFTOFF!!!"); }
এই কনস্ট্রাক্টটি অনেক নেস্টিং দূর করে যা আপনি যদি loop
, if
, else
, এবং break
ব্যবহার করতেন তবে প্রয়োজন হত, এবং এটি আরও স্পষ্ট। যতক্ষণ একটি শর্ত true
থাকে, কোড চলে; অন্যথায়, এটি লুপ থেকে বেরিয়ে যায়।
for
দিয়ে একটি কালেকশনের মাধ্যমে লুপিং করা
আপনি একটি কালেকশনের উপাদানগুলোর উপর লুপ করার জন্য while
কনস্ট্রাক্ট ব্যবহার করতে পারেন, যেমন একটি অ্যারে। উদাহরণস্বরূপ, লিস্টিং ৩-৪-এর লুপটি a
অ্যারের প্রতিটি উপাদান প্রিন্ট করে।
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { println!("the value is: {}", a[index]); index += 1; } }
এখানে, কোডটি অ্যারের উপাদানগুলোর মাধ্যমে গণনা করে। এটি ইনডেক্স 0
থেকে শুরু হয়, এবং তারপরে লুপ করে যতক্ষণ না এটি অ্যারের চূড়ান্ত ইনডেক্সে পৌঁছায় (অর্থাৎ, যখন index < 5
আর true
থাকে না)। এই কোডটি চালালে অ্যারের প্রতিটি উপাদান প্রিন্ট হবে:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
প্রত্যাশিত হিসাবে, টার্মিনালে পাঁচটি অ্যারের মানই প্রদর্শিত হয়। যদিও index
কোনো এক সময় 5
মানে পৌঁছাবে, লুপটি অ্যারে থেকে ষষ্ঠ মান আনার চেষ্টা করার আগেই কার্যকর হওয়া বন্ধ করে দেয়।
তবে, এই পদ্ধতিটি ত্রুটিপ্রবণ; আমরা ইনডেক্স মান বা পরীক্ষার শর্ত ভুল হলে প্রোগ্রামটিকে প্যানিক করাতে পারি। উদাহরণস্বরূপ, যদি আপনি a
অ্যারের সংজ্ঞা পরিবর্তন করে চারটি উপাদান করেন কিন্তু শর্তটি while index < 4
-এ আপডেট করতে ভুলে যান, কোডটি প্যানিক করবে। এটি ধীরও, কারণ কম্পাইলার লুপের প্রতিটি পুনরাবৃত্তিতে অ্যারের সীমার মধ্যে ইনডেক্স আছে কিনা তা পরীক্ষা করার জন্য রানটাইম কোড যোগ করে।
একটি আরও সংক্ষিপ্ত বিকল্প হিসাবে, আপনি একটি for
লুপ ব্যবহার করতে পারেন এবং একটি কালেকশনের প্রতিটি আইটেমের জন্য কিছু কোড কার্যকর করতে পারেন। একটি for
লুপ লিস্টিং ৩-৫-এর কোডের মতো দেখায়।
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {element}"); } }
যখন আমরা এই কোডটি চালাব, আমরা লিস্টিং ৩-৪-এর মতো একই আউটপুট দেখতে পাব। আরও গুরুত্বপূর্ণভাবে, আমরা এখন কোডের নিরাপত্তা বাড়িয়েছি এবং অ্যারের শেষের বাইরে যাওয়া বা যথেষ্ট দূরে না গিয়ে কিছু আইটেম বাদ দেওয়ার ফলে হতে পারে এমন বাগের সম্ভাবনা দূর করেছি। for
লুপ থেকে তৈরি মেশিন কোড আরও কার্যকর হতে পারে, কারণ প্রতিটি পুনরাবৃত্তিতে ইনডেক্সকে অ্যারের দৈর্ঘ্যের সাথে তুলনা করার প্রয়োজন হয় না।
for
লুপ ব্যবহার করলে, আপনি যদি অ্যারের মানের সংখ্যা পরিবর্তন করেন তবে অন্য কোনো কোড পরিবর্তন করার কথা মনে রাখার প্রয়োজন হবে না, যেমনটি লিস্টিং ৩-৪-এ ব্যবহৃত পদ্ধতিতে করতে হত।
for
লুপের নিরাপত্তা এবং সংক্ষিপ্ততা তাদের রাস্ট-এ সবচেয়ে বেশি ব্যবহৃত লুপ কনস্ট্রাক্ট করে তুলেছে। এমনকি যে পরিস্থিতিতে আপনি একটি নির্দিষ্ট সংখ্যক বার কিছু কোড চালাতে চান, যেমন লিস্টিং ৩-৩-এ while
লুপ ব্যবহার করা কাউন্টডাউন উদাহরণে, বেশিরভাগ রাস্টেশিয়ান একটি for
লুপ ব্যবহার করবে। এটি করার উপায় হল একটি Range
ব্যবহার করা, যা স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা হয়, যা একটি সংখ্যা থেকে শুরু করে এবং অন্য একটি সংখ্যার আগে শেষ হওয়া পর্যন্ত ক্রমানুসারে সমস্ত সংখ্যা তৈরি করে।
এখানে for
লুপ এবং আরেকটি মেথড যা আমরা এখনও আলোচনা করিনি, rev
(রেঞ্জটি উল্টো করার জন্য) ব্যবহার করে কাউন্টডাউনটি কেমন দেখাবে:
ফাইলের নাম: src/main.rs
fn main() { for number in (1..4).rev() { println!("{number}!"); } println!("LIFTOFF!!!"); }
এই কোডটি একটু সুন্দর, তাই না?
সারাংশ
আপনি পেরেছেন! এটি একটি বড় অধ্যায় ছিল: আপনি ভ্যারিয়েবল, স্কেলার এবং কম্পাউন্ড ডেটা টাইপ, ফাংশন, কমেন্ট, if
এক্সপ্রেশন এবং লুপ সম্পর্কে শিখেছেন! এই অধ্যায়ে আলোচিত ধারণাগুলো অনুশীলন করার জন্য, নিম্নলিখিত কাজগুলো করার জন্য প্রোগ্রাম তৈরি করার চেষ্টা করুন:
- ফারেনহাইট এবং সেলসিয়াসের মধ্যে তাপমাত্রা রূপান্তর করুন।
- n-তম ফিবোনাচি সংখ্যা তৈরি করুন।
- ক্রিসমাস ক্যারোল "The Twelve Days of Christmas"-এর লিরিক্স প্রিন্ট করুন, গানের পুনরাবৃত্তির সুবিধা নিয়ে।
যখন আপনি এগিয়ে যাওয়ার জন্য প্রস্তুত হবেন, আমরা রাস্টের এমন একটি ধারণা নিয়ে কথা বলব যা অন্যান্য প্রোগ্রামিং ভাষায় সাধারণত বিদ্যমান নেই: ওনারশিপ (ownership)।
মালিকানা বোঝা (Understanding Ownership)
মালিকানা (Ownership) হলো রাস্টের সবচেয়ে স্বতন্ত্র একটি বৈশিষ্ট্য এবং এর প্রভাব পুরো প্রোগ্রামিং ভাষার উপর গভীরভাবে পড়ে। এই বৈশিষ্ট্যের কারণেই রাস্ট কোনো গার্বেজ কালেক্টর (garbage collector) ছাড়াই মেমোরি সুরক্ষার (memory safety) নিশ্চয়তা দিতে পারে, তাই মালিকানার কার্যপদ্ধতি বোঝা অত্যন্ত গুরুত্বপূর্ণ। এই অধ্যায়ে, আমরা মালিকানার পাশাপাশি এর সাথে সম্পর্কিত আরও কয়েকটি ধারণা নিয়ে আলোচনা করব: ধার করা (borrowing), স্লাইস (slices), এবং রাস্ট যেভাবে মেমোরিতে ডেটা বিন্যাস করে।
মালিকানা (Ownership) কী?
Ownership হলো নিয়মের একটি সেট যা একটি রাস্ট প্রোগ্রাম কীভাবে মেমরি পরিচালনা (manage) করে তা নিয়ন্ত্রণ করে। সমস্ত প্রোগ্রামকে চলার সময় কম্পিউটারের মেমরি ব্যবহারের পদ্ধতি পরিচালনা করতে হয়। কিছু ভাষায় গার্বেজ কালেকশন (garbage collection) থাকে যা প্রোগ্রাম চলার সময় নিয়মিতভাবে অব্যবহৃত মেমরি খুঁজে বের করে; অন্য ভাষাগুলোতে, প্রোগ্রামারকে অবশ্যই স্পষ্টভাবে মেমরি বরাদ্দ (allocate) এবং মুক্ত (free) করতে হয়। রাস্ট তৃতীয় একটি পদ্ধতি ব্যবহার করে: মেমরি একটি মালিকানা সিস্টেমের (system of ownership) মাধ্যমে পরিচালিত হয়, যেখানে কম্পাইলার কিছু নিয়ম পরীক্ষা করে। যদি কোনো নিয়ম লঙ্ঘন করা হয়, প্রোগ্রামটি কম্পাইল হবে না। মালিকানার কোনো বৈশিষ্ট্যই আপনার প্রোগ্রাম চলার সময় এটিকে ধীর করবে না।
যেহেতু অনেক প্রোগ্রামারের জন্য মালিকানা একটি নতুন ধারণা, তাই এতে অভ্যস্ত হতে কিছুটা সময় লাগে। ভালো খবর হলো, আপনি রাস্ট এবং মালিকানা সিস্টেমের নিয়মগুলোর সাথে যত বেশি অভিজ্ঞ হবেন, তত সহজে আপনি স্বাভাবিকভাবেই নিরাপদ এবং কার্যকর কোড তৈরি করতে পারবেন। চেষ্টা চালিয়ে যান!
যখন আপনি মালিকানা বুঝতে পারবেন, তখন রাস্টকে স্বতন্ত্র করে তোলা বৈশিষ্ট্যগুলো বোঝার জন্য আপনার একটি শক্ত ভিত্তি তৈরি হবে। এই অধ্যায়ে, আপনি একটি খুব সাধারণ ডেটা স্ট্রাকচার—স্ট্রিং—এর উপর ভিত্তি করে কিছু উদাহরণের মাধ্যমে মালিকানা শিখবেন।
স্ট্যাক (Stack) এবং হীপ (Heap)
অনেক প্রোগ্রামিং ভাষায় আপনাকে স্ট্যাক এবং হীপ নিয়ে খুব বেশি ভাবতে হয় না। কিন্তু রাস্টের মতো একটি সিস্টেমস প্রোগ্রামিং ভাষায়, কোনো মান (value) স্ট্যাকে নাকি হীপে আছে, তা ভাষার আচরণকে প্রভাবিত করে এবং আপনাকে কেন নির্দিষ্ট সিদ্ধান্ত নিতে হবে তা নির্ধারণ করে। এই অধ্যায়ের পরে মালিকানার কিছু অংশ স্ট্যাক এবং হীপের সাথে সম্পর্কিত করে বর্ণনা করা হবে, তাই প্রস্তুতির জন্য এখানে একটি সংক্ষিপ্ত ব্যাখ্যা দেওয়া হলো।
স্ট্যাক এবং হীপ উভয়ই মেমরির অংশ যা আপনার কোড রানটাইমে ব্যবহার করতে পারে, তবে তাদের গঠন ভিন্ন। স্ট্যাক মানগুলোকে যে ক্রমে পায় সেই ক্রমে সংরক্ষণ করে এবং ঠিক তার বিপরীত ক্রমে মানগুলো সরিয়ে দেয়। একে লাস্ট ইন, ফার্স্ট আউট (last in, first out) বলা হয়। একটি প্লেটের স্ট্যাকের কথা ভাবুন: যখন আপনি আরও প্লেট যোগ করেন, তখন আপনি সেগুলোকে গাদার উপরে রাখেন, এবং যখন আপনার একটি প্লেট দরকার হয়, তখন আপনি উপর থেকে একটি তুলে নেন। মাঝখান থেকে বা নিচ থেকে প্লেট যোগ করা বা সরানো ঠিকভাবে কাজ করবে না! ডেটা যোগ করাকে বলা হয় পুশিং অনটু দ্য স্ট্যাক (pushing onto the stack), এবং ডেটা সরানোকে বলা হয় পপিং অফ দ্য স্ট্যাক (popping off the stack)। স্ট্যাকে সংরক্ষিত সমস্ত ডেটার একটি পরিচিত, নির্দিষ্ট আকার (known, fixed size) থাকতে হবে। কম্পাইলের সময় অজানা আকারের ডেটা বা যে ডেটার আকার পরিবর্তন হতে পারে, তা অবশ্যই হীপে সংরক্ষণ করতে হবে।
হীপ কম গোছানো: যখন আপনি হীপে ডেটা রাখেন, তখন আপনি নির্দিষ্ট পরিমাণ জায়গা চান। মেমরি অ্যালোকেটর (memory allocator) হীপে একটি যথেষ্ট বড় খালি জায়গা খুঁজে বের করে, এটিকে ব্যবহৃত হিসেবে চিহ্নিত করে এবং একটি পয়েন্টার (pointer) ফেরত দেয়, যা সেই অবস্থানের ঠিকানা। এই প্রক্রিয়াটিকে অ্যালোকেটিং অন দ্য হীপ (allocating on the heap) বলা হয় এবং কখনও কখনও সংক্ষেপে শুধু অ্যালোকেটিং (allocating) বলা হয় (স্ট্যাকে মান push করাকে allocating হিসাবে বিবেচনা করা হয় না)। যেহেতু হীপের পয়েন্টারটির একটি পরিচিত, নির্দিষ্ট আকার রয়েছে, তাই আপনি পয়েন্টারটি স্ট্যাকে সংরক্ষণ করতে পারেন, কিন্তু যখন আপনার আসল ডেটা প্রয়োজন হবে, তখন আপনাকে সেই পয়েন্টারটি অনুসরণ করতে হবে। একটি রেস্তোরাঁয় বসার কথা ভাবুন। যখন আপনি প্রবেশ করেন, আপনি আপনার দলের সদস্য সংখ্যা বলেন, এবং হোস্ট এমন একটি খালি টেবিল খুঁজে বের করে যেখানে সবাই বসতে পারে এবং আপনাকে সেখানে নিয়ে যায়। যদি আপনার দলের কেউ দেরিতে আসে, তবে সে আপনাকে খুঁজে বের করার জন্য জিজ্ঞাসা করতে পারে যে আপনাকে কোথায় বসানো হয়েছে।
স্ট্যাকে push করা হীপে allocate করার চেয়ে দ্রুত, কারণ অ্যালোকেটরকে নতুন ডেটা সংরক্ষণের জন্য জায়গা খুঁজতে হয় না; সেই অবস্থানটি সবসময় স্ট্যাকের শীর্ষে থাকে। তুলনামূলকভাবে, হীপে জায়গা allocate করতে বেশি কাজ করতে হয় কারণ অ্যালোকেটরকে প্রথমে ডেটা রাখার জন্য যথেষ্ট বড় একটি জায়গা খুঁজে বের করতে হবে এবং তারপরে পরবর্তী allocation-এর জন্য হিসাব রাখতে হবে।
হীপে ডেটা অ্যাক্সেস করা সাধারণত স্ট্যাকের ডেটা অ্যাক্সেস করার চেয়ে ধীর, কারণ সেখানে পৌঁছানোর জন্য আপনাকে একটি পয়েন্টার অনুসরণ করতে হয়। আধুনিক প্রসেসরগুলো দ্রুত কাজ করে যদি তারা মেমরিতে কম লাফালাফি করে। উপমাটি চালিয়ে গেলে, একটি রেস্তোরাঁর সার্ভারের কথা ভাবুন जो অনেক টেবিল থেকে অর্ডার নিচ্ছে। পরবর্তী টেবিলে যাওয়ার আগে একটি টেবিলের সমস্ত অর্ডার নেওয়া সবচেয়ে কার্যকর। টেবিল A থেকে একটি অর্ডার নেওয়া, তারপর টেবিল B থেকে একটি অর্ডার, তারপর আবার A থেকে একটি, এবং তারপর আবার B থেকে একটি নেওয়া অনেক ধীর প্রক্রিয়া হবে। একইভাবে, একটি প্রসেসর সাধারণত তার কাজ ভালোভাবে করতে পারে যদি এটি কাছাকাছি থাকা ডেটার উপর কাজ করে (যেমনটি স্ট্যাকে থাকে) বরং দূরে থাকা ডেটার (যেমনটি হীপে থাকতে পারে) চেয়ে।
যখন আপনার কোড একটি ফাংশন কল করে, তখন ফাংশনে পাস করা মানগুলো (সম্ভাব্যভাবে, হীপের ডেটার পয়েন্টার সহ) এবং ফাংশনের লোকাল ভ্যারিয়েবলগুলো স্ট্যাকে push করা হয়। ফাংশন শেষ হয়ে গেলে, সেই মানগুলো স্ট্যাক থেকে pop করা হয়।
কোডের কোন অংশ হীপের কোন ডেটা ব্যবহার করছে তার হিসাব রাখা, হীপের ডুপ্লিকেট ডেটার পরিমাণ কমানো, এবং অব্যবহৃত ডেটা পরিষ্কার করা যাতে আপনার জায়গার অভাব না হয়—এই সমস্ত সমস্যার সমাধান মালিকানা করে। একবার আপনি মালিকানা বুঝে গেলে, আপনাকে স্ট্যাক এবং হীপ নিয়ে খুব বেশি ভাবতে হবে না, তবে মালিকানার মূল উদ্দেশ্য যে হীপের ডেটা পরিচালনা করা, তা জানলে এটি কেন এভাবে কাজ করে তা বুঝতে সাহায্য করতে পারে।
মালিকানার নিয়ম (Ownership Rules)
প্রথমে, আসুন মালিকানার নিয়মগুলো দেখে নেওয়া যাক। উদাহরণগুলো নিয়ে কাজ করার সময় এই নিয়মগুলো মনে রাখবেন:
- রাস্টে প্রতিটি মানের (value) একজন মালিক (owner) থাকে।
- একবারে কেবল একজনই মালিক থাকতে পারে।
- যখন মালিক স্কোপের (scope) বাইরে চলে যায়, তখন মানটি ড্রপ (dropped) হয়ে যাবে।
ভ্যারিয়েবলের স্কোপ (Variable Scope)
এখন যেহেতু আমরা রাস্টের প্রাথমিক সিনট্যাক্স পার করে এসেছি, আমরা উদাহরণগুলোতে আর সম্পূর্ণ fn main() {
কোড অন্তর্ভুক্ত করব না। তাই, আপনি যদি অনুসরণ করেন, তবে নিশ্চিত করুন যে আপনি নিম্নলিখিত উদাহরণগুলো একটি main
ফাংশনের ভিতরে নিজে থেকেই রেখেছেন। ফলস্বরূপ, আমাদের উদাহরণগুলো আরও সংক্ষিপ্ত হবে, যা আমাদের মূল বিবরণের উপর মনোযোগ দিতে সাহায্য করবে।
মালিকানার প্রথম উদাহরণ হিসেবে, আমরা কিছু ভ্যারিয়েবলের স্কোপ (scope) দেখব। একটি স্কোপ হলো প্রোগ্রামের সেই পরিসর যার মধ্যে একটি আইটেম বৈধ (valid) থাকে। নিচের ভ্যারিয়েবলটি বিবেচনা করুন:
#![allow(unused)] fn main() { let s = "hello"; }
s
ভ্যারিয়েবলটি একটি স্ট্রিং লিটারেলকে (string literal) নির্দেশ করে, যেখানে স্ট্রিংয়ের মানটি আমাদের প্রোগ্রামের টেক্সটে হার্ডকোড করা আছে। ভ্যারিয়েবলটি যে মুহূর্তে ঘোষণা করা হয়, সেই মুহূর্ত থেকে বর্তমান স্কোপের শেষ পর্যন্ত বৈধ থাকে। তালিকা ৪-১ এমন একটি প্রোগ্রাম দেখাচ্ছে যেখানে কমেন্টের মাধ্যমে s
ভ্যারিয়েবলটি কোথায় বৈধ থাকবে তা চিহ্নিত করা হয়েছে।
fn main() { { // s is not valid here, since it's not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid }
অন্য কথায়, এখানে দুটি গুরুত্বপূর্ণ সময় রয়েছে:
- যখন
s
স্কোপের মধ্যে আসে, তখন এটি বৈধ। - এটি স্কোপের বাইরে না যাওয়া পর্যন্ত বৈধ থাকে।
এই মুহূর্তে, স্কোপ এবং ভ্যারিয়েবল কখন বৈধ থাকে, তার মধ্যে সম্পর্কটি অন্যান্য প্রোগ্রামিং ভাষার মতোই। এখন আমরা String
টাইপটি প্রবর্তন করে এই ধারণার উপর ভিত্তি করে আরও কিছু তৈরি করব।
String
টাইপ
মালিকানার নিয়মগুলো ব্যাখ্যা করার জন্য, আমাদের এমন একটি ডেটা টাইপ প্রয়োজন যা অধ্যায় ৩-এর "ডেটা টাইপস" বিভাগে আলোচনা করা টাইপগুলোর চেয়ে বেশি জটিল। পূর্বে আলোচনা করা টাইপগুলোর আকার নির্দিষ্ট থাকে, এগুলো স্ট্যাকে সংরক্ষণ করা যায় এবং স্কোপ শেষ হলে স্ট্যাক থেকে পপ করা যায়, এবং কোডের অন্য কোনো অংশে একই মান ভিন্ন স্কোপে ব্যবহার করার প্রয়োজন হলে দ্রুত ও সহজভাবে একটি নতুন, স্বাধীন ইনস্ট্যান্স তৈরি করা যায়। কিন্তু আমরা এমন ডেটা দেখতে চাই যা হীপে সংরক্ষিত হয় এবং রাস্ট কীভাবে সেই ডেটা পরিষ্কার করার সময় জানে তা অন্বেষণ করতে চাই, এবং String
টাইপটি এর একটি চমৎকার উদাহরণ।
আমরা String
-এর সেই অংশগুলোর উপর মনোযোগ দেব যা মালিকানার সাথে সম্পর্কিত। এই দিকগুলো অন্যান্য জটিল ডেটা টাইপের ক্ষেত্রেও প্রযোজ্য, তা স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা হোক বা আপনার নিজের তৈরি করা হোক। আমরা অধ্যায় ৮-এ String
নিয়ে আরও গভীরভাবে আলোচনা করব।
আমরা ইতিমধ্যে স্ট্রিং লিটারেল দেখেছি, যেখানে একটি স্ট্রিং মান আমাদের প্রোগ্রামে হার্ডকোড করা থাকে। স্ট্রিং লিটারেলগুলো সুবিধাজনক, কিন্তু আমরা যে সমস্ত পরিস্থিতিতে টেক্সট ব্যবহার করতে চাই তার জন্য উপযুক্ত নয়। একটি কারণ হলো সেগুলো অপরিবর্তনীয় (immutable)। আরেকটি কারণ হলো, কোড লেখার সময় প্রতিটি স্ট্রিংয়ের মান জানা সম্ভব নাও হতে পারে: উদাহরণস্বরূপ, যদি আমরা ব্যবহারকারীর ইনপুট নিয়ে তা সংরক্ষণ করতে চাই? এই ধরনের পরিস্থিতির জন্য, রাস্টের দ্বিতীয় একটি স্ট্রিং টাইপ আছে, String
। এই টাইপটি হীপে বরাদ্দ করা ডেটা পরিচালনা করে এবং তাই কম্পাইলের সময় অজানা পরিমাণ টেক্সট সংরক্ষণ করতে সক্ষম। আপনি from
ফাংশন ব্যবহার করে একটি স্ট্রিং লিটারেল থেকে String
তৈরি করতে পারেন, যেমন:
#![allow(unused)] fn main() { let s = String::from("hello"); }
ডাবল কোলন ::
অপারেটরটি আমাদের এই নির্দিষ্ট from
ফাংশনটিকে String
টাইপের অধীনে নেমস্পেস করতে দেয়, string_from
-এর মতো কোনো নাম ব্যবহার করার পরিবর্তে। আমরা এই সিনট্যাক্স সম্পর্কে অধ্যায় ৫-এর "মেথড সিনট্যাক্স" বিভাগে এবং অধ্যায় ৭-এর "মডিউল ট্রি-তে একটি আইটেম রেফার করার জন্য পাথ" বিভাগে মডিউলসহ নেমস্পেসিং নিয়ে আলোচনা করার সময় আরও জানব।
এই ধরনের স্ট্রিং পরিবর্তন (mutated) করা যেতে পারে:
fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{s}"); // this will print `hello, world!` }
তাহলে, এখানে পার্থক্যটা কী? কেন String
পরিবর্তন করা যায় কিন্তু লিটারেলগুলো যায় না? পার্থক্যটি হলো এই দুটি টাইপ কীভাবে মেমরি নিয়ে কাজ করে।
মেমরি এবং অ্যালোকেশন (Memory and Allocation)
একটি স্ট্রিং লিটারেলের ক্ষেত্রে, আমরা কম্পাইলের সময় বিষয়বস্তু জানি, তাই টেক্সটটি সরাসরি চূড়ান্ত এক্সিকিউটেবলে হার্ডকোড করা থাকে। এই কারণেই স্ট্রিং লিটারেলগুলো দ্রুত এবং কার্যকর। কিন্তু এই বৈশিষ্ট্যগুলো শুধুমাত্র স্ট্রিং লিটারেলের অপরিবর্তনীয়তা (immutability) থেকে আসে। দুর্ভাগ্যবশত, আমরা প্রতিটি টেক্সট, যার আকার কম্পাইলের সময় অজানা এবং প্রোগ্রাম চলার সময় আকার পরিবর্তন হতে পারে, তার জন্য বাইনারিতে মেমরির একটি অংশ রাখতে পারি না।
String
টাইপের সাথে, একটি পরিবর্তনযোগ্য (mutable), প্রসারণযোগ্য (growable) টেক্সট সমর্থন করার জন্য, আমাদের হীপে একটি পরিমাণ মেমরি allocate করতে হবে, যা কম্পাইলের সময় অজানা, বিষয়বস্তু ধারণ করার জন্য। এর মানে হলো:
- রানটাইমে মেমরি অ্যালোকেটরের কাছ থেকে মেমরির জন্য অনুরোধ করতে হবে।
- আমাদের
String
নিয়ে কাজ শেষ হলে এই মেমরিটি অ্যালোকেটরকে ফেরত দেওয়ার একটি উপায় প্রয়োজন।
প্রথম অংশটি আমরা করি: যখন আমরা String::from
কল করি, তখন এর ইমপ্লিমেন্টেশন প্রয়োজনীয় মেমরির জন্য অনুরোধ করে। এটি প্রোগ্রামিং ভাষাগুলোতে প্রায় সর্বজনীন।
তবে, দ্বিতীয় অংশটি ভিন্ন। গার্বেজ কালেক্টর (GC) সহ ভাষাগুলোতে, GC সেই মেমরির ট্র্যাক রাখে এবং পরিষ্কার করে যা আর ব্যবহৃত হচ্ছে না, এবং আমাদের এটি নিয়ে ভাবতে হবে না। GC ছাড়া বেশিরভাগ ভাষায়, কখন মেমরি আর ব্যবহৃত হচ্ছে না তা চিহ্নিত করা এবং এটি স্পষ্টভাবে মুক্ত (free) করার জন্য কোড কল করা আমাদের দায়িত্ব, ঠিক যেমনটি আমরা এটি অনুরোধ করার জন্য করেছিলাম। ঐতিহাসিকভাবে এটি সঠিকভাবে করা একটি কঠিন প্রোগ্রামিং সমস্যা। যদি আমরা ভুলে যাই, আমরা মেমরি নষ্ট করব। যদি আমরা এটি খুব তাড়াতাড়ি করি, আমাদের একটি অবৈধ ভ্যারিয়েবল থাকবে। যদি আমরা এটি দুবার করি, সেটাও একটি বাগ। আমাদের ঠিক একটি allocate
-এর সাথে ঠিক একটি free
যুক্ত করতে হবে।
রাস্ট একটি ভিন্ন পথ নেয়: যে ভ্যারিয়েবলটির মালিকানায় মেমরিটি থাকে, সেটি স্কোপের বাইরে চলে গেলে মেমরি স্বয়ংক্রিয়ভাবে ফেরত দেওয়া হয়। এখানে তালিকা ৪-১ থেকে আমাদের স্কোপের উদাহরণের একটি সংস্করণ রয়েছে যা স্ট্রিং লিটারেলের পরিবর্তে একটি String
ব্যবহার করে:
fn main() { { let s = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no // longer valid }
আমাদের String
-এর প্রয়োজনীয় মেমরি অ্যালোকেটরকে ফেরত দেওয়ার একটি স্বাভাবিক মুহূর্ত আছে: যখন s
স্কোপের বাইরে চলে যায়। যখন একটি ভ্যারিয়েবল স্কোপের বাইরে যায়, রাস্ট আমাদের জন্য একটি বিশেষ ফাংশন কল করে। এই ফাংশনটিকে বলা হয় drop
, এবং এখানেই String
-এর লেখক মেমরি ফেরত দেওয়ার কোড রাখতে পারেন। রাস্ট স্বয়ংক্রিয়ভাবে কার্লি ব্র্যাকেট বন্ধ করার সময় drop
কল করে।
দ্রষ্টব্য: C++ এ, একটি আইটেমের জীবনকালের শেষে রিসোর্স ডিঅ্যালোকেট করার এই প্যাটার্নটিকে কখনও কখনও রিসোর্স অ্যাকুইজিশন ইজ ইনিশিয়ালাইজেশন (RAII) বলা হয়। আপনি যদি RAII প্যাটার্ন ব্যবহার করে থাকেন তবে রাস্টের
drop
ফাংশনটি আপনার কাছে পরিচিত মনে হবে।
এই প্যাটার্নটি রাস্ট কোড লেখার পদ্ধতিতে গভীর প্রভাব ফেলে। এটি এখন সহজ মনে হতে পারে, কিন্তু যখন আমরা হীপে বরাদ্দ করা ডেটা একাধিক ভ্যারিয়েবল ব্যবহার করতে চাই, তখন আরও জটিল পরিস্থিতিতে কোডের আচরণ অপ্রত্যাশিত হতে পারে। আসুন এখন সেই পরিস্থিতিগুলোর কয়েকটি অন্বেষণ করি।
Move এর মাধ্যমে ভ্যারিয়েবল এবং ডেটার মিথস্ক্রিয়া
রাস্টে একাধিক ভ্যারিয়েবল একই ডেটার সাথে বিভিন্ন উপায়ে মিথস্ক্রিয়া করতে পারে। আসুন তালিকা ৪-২-এ একটি পূর্ণসংখ্যা (integer) ব্যবহার করে একটি উদাহরণ দেখি।
fn main() { let x = 5; let y = x; }
আমরা সম্ভবত অনুমান করতে পারি এটি কী করছে: "x
-এ 5
মানটি বাইন্ড কর; তারপর x
-এর মানের একটি কপি তৈরি কর এবং এটি y
-কে বাইন্ড কর।" এখন আমাদের দুটি ভ্যারিয়েবল আছে, x
এবং y
, এবং উভয়ই 5
-এর সমান। এটি আসলেই ঘটছে, কারণ পূর্ণসংখ্যাগুলো একটি জানা, নির্দিষ্ট আকারের সরল মান, এবং এই দুটি 5
মান স্ট্যাকে পুশ করা হয়।
এখন String
সংস্করণটি দেখা যাক:
fn main() { let s1 = String::from("hello"); let s2 = s1; }
এটি দেখতে খুব একই রকম, তাই আমরা ধরে নিতে পারি যে এটি কাজ করার পদ্ধতিও একই হবে: অর্থাৎ, দ্বিতীয় লাইনটি s1
-এর মানের একটি কপি তৈরি করবে এবং এটিকে s2
-তে বাইন্ড করবে। কিন্তু এটি ঠিক যা ঘটে তা নয়।
String
-এর আড়ালে কী ঘটছে তা দেখতে চিত্র ৪-১ দেখুন। একটি String
তিনটি অংশ নিয়ে গঠিত, যা বাম দিকে দেখানো হয়েছে: স্ট্রিংয়ের বিষয়বস্তু ধারণকারী মেমরির একটি পয়েন্টার, একটি দৈর্ঘ্য (length) এবং একটি ধারণক্ষমতা (capacity)। এই ডেটার গ্রুপটি স্ট্যাকে সংরক্ষণ করা হয়। ডানদিকে হীপে থাকা মেমরি রয়েছে যা বিষয়বস্তু ধারণ করে।
চিত্র ৪-১: s1
-এ বাইন্ড করা "hello"
মান ধারণকারী একটি String
-এর মেমরিতে উপস্থাপনা
দৈর্ঘ্য হলো String
-এর বিষয়বস্তু বর্তমানে কত বাইট মেমরি ব্যবহার করছে। ধারণক্ষমতা হলো String
অ্যালোকেটরের কাছ থেকে মোট কত বাইট মেমরি পেয়েছে। দৈর্ঘ্য এবং ধারণক্ষমতার মধ্যে পার্থক্য গুরুত্বপূর্ণ, কিন্তু এই প্রসঙ্গে নয়, তাই আপাতত, ধারণক্ষমতা উপেক্ষা করা ঠিক আছে।
যখন আমরা s1
-কে s2
-তে অ্যাসাইন করি, তখন String
ডেটা কপি করা হয়, যার অর্থ আমরা স্ট্যাকে থাকা পয়েন্টার, দৈর্ঘ্য এবং ধারণক্ষমতা কপি করি। আমরা পয়েন্টারটি যে হীপের ডেটাকে নির্দেশ করে তা কপি করি না। অন্য কথায়, মেমরিতে ডেটার উপস্থাপনা চিত্র ৪-২-এর মতো দেখায়।
চিত্র ৪-২: s2
ভ্যারিয়েবলের মেমরিতে উপস্থাপনা যা s1
-এর পয়েন্টার, দৈর্ঘ্য এবং ধারণক্ষমতার একটি কপি ধারণ করে
উপস্থাপনাটি চিত্র ৪-৩ এর মতো দেখায় না, যা মেমরির চিত্র হতো যদি রাস্ট হীপের ডেটাও কপি করত। যদি রাস্ট এটি করত, তবে s2 = s1
অপারেশনটি রানটাইম পারফরম্যান্সের দিক থেকে খুব ব্যয়বহুল হতে পারত যদি হীপের ডেটা বড় হতো।
চিত্র ৪-৩: s2 = s1
কী করতে পারে তার আরেকটি সম্ভাবনা যদি রাস্ট হীপের ডেটাও কপি করত
আগে, আমরা বলেছিলাম যে যখন একটি ভ্যারিয়েবল স্কোপের বাইরে চলে যায়, তখন রাস্ট স্বয়ংক্রিয়ভাবে drop
ফাংশন কল করে এবং সেই ভ্যারিয়েবলের জন্য হীপ মেমরি পরিষ্কার করে। কিন্তু চিত্র ৪-২ দেখাচ্ছে যে উভয় ডেটা পয়েন্টার একই অবস্থানে নির্দেশ করছে। এটি একটি সমস্যা: যখন s2
এবং s1
স্কোপের বাইরে চলে যাবে, তারা উভয়ই একই মেমরি মুক্ত করার চেষ্টা করবে। এটি একটি ডাবল ফ্রি (double free) ত্রুটি হিসাবে পরিচিত এবং এটি আমরা আগে উল্লেখ করা মেমরি সুরক্ষা বাগগুলোর মধ্যে একটি। দুবার মেমরি মুক্ত করা মেমরি করাপশনের কারণ হতে পারে, যা সম্ভাব্যভাবে নিরাপত্তা দুর্বলতার কারণ হতে পারে।
মেমরি সুরক্ষা নিশ্চিত করার জন্য, let s2 = s1;
লাইনের পরে, রাস্ট s1
-কে আর বৈধ বলে মনে করে না। অতএব, s1
স্কোপের বাইরে চলে গেলে রাস্টকে কিছুই মুক্ত করতে হবে না। s2
তৈরি হওয়ার পরে s1
ব্যবহার করার চেষ্টা করলে কী হয় তা দেখুন; এটি কাজ করবে না:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}```
আপনি এই ধরনের একটি ত্রুটি পাবেন কারণ রাস্ট আপনাকে অবৈধ রেফারেন্স ব্যবহার করতে বাধা দেয়:
```console
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:15
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
আপনি যদি অন্য ভাষায় কাজ করার সময় শ্যালো কপি (shallow copy) এবং ডিপ কপি (deep copy) শব্দগুলো শুনে থাকেন, তবে ডেটা কপি না করে পয়েন্টার, দৈর্ঘ্য এবং ধারণক্ষমতা কপি করার ধারণাটি সম্ভবত একটি শ্যালো কপির মতো শোনাচ্ছে। কিন্তু যেহেতু রাস্ট প্রথম ভ্যারিয়েবলটিকেও অবৈধ করে দেয়, তাই একে শ্যালো কপি না বলে মুভ (move) বলা হয়। এই উদাহরণে, আমরা বলব যে s1
কে s2
তে মুভ করা হয়েছে। সুতরাং, যা আসলে ঘটে তা চিত্র ৪-৪-এ দেখানো হয়েছে।
চিত্র ৪-৪: s1
অবৈধ হওয়ার পর মেমরিতে উপস্থাপনা
এটি আমাদের সমস্যার সমাধান করে! শুধুমাত্র s2
বৈধ হওয়ায়, যখন এটি স্কোপের বাইরে চলে যাবে তখন এটি একাই মেমরি মুক্ত করবে, এবং আমাদের কাজ শেষ।
এছাড়াও, এর মধ্যে একটি ডিজাইন পছন্দ নিহিত রয়েছে: রাস্ট কখনও স্বয়ংক্রিয়ভাবে আপনার ডেটার "ডিপ" কপি তৈরি করবে না। অতএব, যেকোনো স্বয়ংক্রিয় কপি করাকে রানটাইম পারফরম্যান্সের দিক থেকে সাশ্রয়ী বলে ধরে নেওয়া যেতে পারে।
স্কোপ এবং অ্যাসাইনমেন্ট (Scope and Assignment)
এর বিপরীতটিও স্কোপিং, মালিকানা এবং drop
ফাংশনের মাধ্যমে মেমরি মুক্ত হওয়ার সম্পর্কের জন্য সত্য। যখন আপনি একটি বিদ্যমান ভ্যারিয়েবলে একটি সম্পূর্ণ নতুন মান অ্যাসাইন করেন, তখন রাস্ট drop
কল করবে এবং মূল মানের মেমরি অবিলম্বে মুক্ত করবে। উদাহরণস্বরূপ, এই কোডটি বিবেচনা করুন:
fn main() { let mut s = String::from("hello"); s = String::from("ahoy"); println!("{s}, world!"); }
আমরা প্রথমে একটি ভ্যারিয়েবল s
ঘোষণা করি এবং এটিকে "hello"
মান সহ একটি String
-এ বাইন্ড করি। তারপরে আমরা অবিলম্বে "ahoy"
মান সহ একটি নতুন String
তৈরি করি এবং এটিকে s
-এ অ্যাসাইন করি। এই মুহূর্তে, হীপের মূল মানটিকে কিছুই নির্দেশ করছে না।
চিত্র ৪-৫: মূল মানটি সম্পূর্ণরূপে প্রতিস্থাপিত হওয়ার পরে মেমরিতে উপস্থাপনা।
মূল স্ট্রিংটি তাই অবিলম্বে স্কোপের বাইরে চলে যায়। রাস্ট এটির উপর drop
ফাংশন চালাবে এবং এর মেমরি সঙ্গে সঙ্গে মুক্ত হয়ে যাবে। যখন আমরা শেষে মানটি প্রিন্ট করব, তখন এটি "ahoy, world!"
হবে।
Clone এর মাধ্যমে ভ্যারিয়েবল এবং ডেটার মিথস্ক্রিয়া
যদি আমরা String
-এর হীপ ডেটা গভীরভাবে কপি করতে চাই, শুধু স্ট্যাক ডেটা নয়, আমরা clone
নামে একটি সাধারণ মেথড ব্যবহার করতে পারি। আমরা অধ্যায় ৫-এ মেথড সিনট্যাক্স নিয়ে আলোচনা করব, কিন্তু যেহেতু মেথডগুলো অনেক প্রোগ্রামিং ভাষায় একটি সাধারণ বৈশিষ্ট্য, আপনি সম্ভবত সেগুলি আগে দেখেছেন।
এখানে clone
মেথডের একটি উদাহরণ দেওয়া হল:
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
এটি ঠিকঠাক কাজ করে এবং স্পষ্টভাবে চিত্র ৪-৩-এ দেখানো আচরণ তৈরি করে, যেখানে হীপ ডেটা সত্যিই কপি করা হয়।
যখন আপনি clone
-এর একটি কল দেখেন, আপনি জানেন যে কিছু নির্বিচারে কোড কার্যকর করা হচ্ছে এবং সেই কোড ব্যয়বহুল হতে পারে। এটি একটি চাক্ষুষ সূচক যে কিছু ভিন্ন ঘটছে।
শুধুমাত্র-স্ট্যাক ডেটা: কপি (Copy)
আরেকটি জটিলতা আছে যা আমরা এখনো আলোচনা করিনি। পূর্ণসংখ্যা ব্যবহার করা এই কোডটি—যার একটি অংশ তালিকা ৪-২-এ দেখানো হয়েছিল—কাজ করে এবং বৈধ:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); }
কিন্তু এই কোডটি আমরা যা শিখেছি তার সাথে সাংঘর্ষিক বলে মনে হচ্ছে: আমাদের clone
-এর কোনো কল নেই, কিন্তু x
এখনও বৈধ এবং y
-তে মুভ করা হয়নি।
এর কারণ হলো, পূর্ণসংখ্যার মতো টাইপগুলো যাদের কম্পাইলের সময় একটি নির্দিষ্ট আকার থাকে, সেগুলো সম্পূর্ণরূপে স্ট্যাকে সংরক্ষিত হয়, তাই আসল মানগুলোর কপি তৈরি করা দ্রুত হয়। এর মানে হলো, y
ভ্যারিয়েবল তৈরি করার পরে x
-কে বৈধ থাকা থেকে বিরত রাখার কোনো কারণ নেই। অন্য কথায়, এখানে ডিপ এবং শ্যালো কপি করার মধ্যে কোনো পার্থক্য নেই, তাই clone
কল করা স্বাভাবিক শ্যালো কপি করার থেকে ভিন্ন কিছু করত না, এবং আমরা এটি বাদ দিতে পারি।
রাস্টের একটি বিশেষ টীকা আছে যার নাম Copy
ট্রেইট (trait) যা আমরা স্ট্যাকে সংরক্ষিত টাইপগুলোর উপর রাখতে পারি, যেমন পূর্ণসংখ্যাগুলো (আমরা অধ্যায় ১০-এ ট্রেইট সম্পর্কে আরও কথা বলব)। যদি একটি টাইপ Copy
ট্রেইট ইমপ্লিমেন্ট করে, তবে এটি ব্যবহারকারী ভ্যারিয়েবলগুলো মুভ হয় না, বরং সহজভাবে কপি করা হয়, যা তাদের অন্য ভ্যারিয়েবলে অ্যাসাইনমেন্টের পরেও বৈধ রাখে।
রাস্ট আমাদের কোনো টাইপকে Copy
দিয়ে টীকা দিতে দেবে না যদি সেই টাইপ বা এর কোনো অংশ, Drop
ট্রেইট ইমপ্লিমেন্ট করে থাকে। যদি মানটি স্কোপের বাইরে চলে গেলে টাইপটির জন্য বিশেষ কিছু ঘটার প্রয়োজন হয় এবং আমরা সেই টাইপে Copy
টীকা যোগ করি, আমরা একটি কম্পাইল-টাইম ত্রুটি পাব। আপনার টাইপে Copy
ট্রেইট ইমপ্লিমেন্ট করার জন্য Copy
টীকা কীভাবে যোগ করবেন তা জানতে, পরিশিষ্ট C-এর "ডিরাইভেবল ট্রেইটস" দেখুন।
তাহলে, কোন টাইপগুলো Copy
ট্রেইট ইমপ্লিমেন্ট করে? আপনি নিশ্চিত হতে প্রদত্ত টাইপের ডকুমেন্টেশন দেখতে পারেন, কিন্তু একটি সাধারণ নিয়ম হিসাবে, যেকোনো সরল স্কেলার মানের গ্রুপ Copy
ইমপ্লিমেন্ট করতে পারে, এবং যা কিছু অ্যালোকেশন প্রয়োজন বা কোনো ধরনের রিসোর্স, তা Copy
ইমপ্লিমেন্ট করতে পারে না। এখানে কিছু টাইপ রয়েছে যা Copy
ইমপ্লিমেন্ট করে:
- সমস্ত পূর্ণসংখ্যার টাইপ, যেমন
u32
। - বুলিয়ান টাইপ,
bool
,true
এবংfalse
মান সহ। - সমস্ত ফ্লোটিং-পয়েন্ট টাইপ, যেমন
f64
। - ক্যারেক্টার টাইপ,
char
। - টাপল (Tuples), যদি তারা শুধুমাত্র এমন টাইপ ধারণ করে যা
Copy
ইমপ্লিমেন্ট করে। উদাহরণস্বরূপ,(i32, i32)
Copy
ইমপ্লিমেন্ট করে, কিন্তু(i32, String)
করে না।
মালিকানা এবং ফাংশন (Ownership and Functions)
একটি ফাংশনে মান পাস করার পদ্ধতি একটি ভ্যারিয়েবলে মান অ্যাসাইন করার মতোই। একটি ফাংশনে একটি ভ্যারিয়েবল পাস করা মুভ বা কপি করবে, ঠিক যেমন অ্যাসাইনমেন্ট করে। তালিকা ৪-৩-এ কিছু টীকাসহ একটি উদাহরণ রয়েছে যা দেখায় কোথায় ভ্যারিয়েবলগুলো স্কোপের ভিতরে এবং বাইরে যায়।
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // Because i32 implements the Copy trait, // x does NOT move into the function, // so it's okay to use x afterward. } // Here, x goes out of scope, then s. However, because s's value was moved, // nothing special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
যদি আমরা takes_ownership
কল করার পরে s
ব্যবহার করার চেষ্টা করি, রাস্ট একটি কম্পাইল-টাইম ত্রুটি দেবে। এই স্ট্যাটিক চেকগুলো আমাদের ভুল থেকে রক্ষা করে। main
-এ কোড যোগ করে s
এবং x
ব্যবহার করে দেখুন কোথায় আপনি সেগুলি ব্যবহার করতে পারেন এবং কোথায় মালিকানার নিয়ম আপনাকে তা করতে বাধা দেয়।
রিটার্ন ভ্যালু এবং স্কোপ (Return Values and Scope)
মান ফেরত দেওয়াও মালিকানা হস্তান্তর করতে পারে। তালিকা ৪-৪ এমন একটি ফাংশনের উদাহরণ দেখাচ্ছে যা কিছু মান ফেরত দেয়, তালিকা ৪-৩-এর মতো একই টীকাসহ।
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns a String. fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
একটি ভ্যারিয়েবলের মালিকানা প্রতিবার একই প্যাটার্ন অনুসরণ করে: অন্য একটি ভ্যারিয়েবলে একটি মান অ্যাসাইন করা এটিকে মুভ করে। যখন একটি ভ্যারিয়েবল যা হীপে ডেটা অন্তর্ভুক্ত করে স্কোপের বাইরে যায়, তখন মানটি drop
দ্বারা পরিষ্কার করা হবে যদি না ডেটার মালিকানা অন্য ভ্যারিয়েবলে মুভ করা হয়ে থাকে।
যদিও এটি কাজ করে, প্রতিটি ফাংশনের সাথে মালিকানা নেওয়া এবং তারপর মালিকানা ফেরত দেওয়া কিছুটা ক্লান্তিকর। কী হবে যদি আমরা একটি ফাংশনকে একটি মান ব্যবহার করতে দিতে চাই কিন্তু মালিকানা নিতে না চাই? এটা বেশ বিরক্তিকর যে আমরা যা কিছু পাস করি তা আমাদের আবার ফেরত পাঠাতে হবে যদি আমরা এটি আবার ব্যবহার করতে চাই, ফাংশনের বডি থেকে প্রাপ্ত কোনো ডেটা ছাড়াও যা আমরা ফেরত দিতে চাই।
রাস্ট আমাদের একটি টাপল (tuple) ব্যবহার করে একাধিক মান ফেরত দেওয়ার অনুমতি দেয়, যেমন তালিকা ৪-৫-এ দেখানো হয়েছে।
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }
কিন্তু এটি একটি সাধারণ ধারণার জন্য অনেক বেশি আনুষ্ঠানিকতা এবং অনেক কাজ। ভাগ্যক্রমে, রাস্টের একটি বৈশিষ্ট্য আছে যা মালিকানা হস্তান্তর না করে একটি মান ব্যবহার করার জন্য, যার নাম রেফারেন্স (references)।
রেফারেন্স এবং ধার করা (References and Borrowing)
তালিকা ৪-৫ এর টাপল (tuple) কোডের সমস্যাটি হলো, calculate_length
ফাংশনে String
পাস করার পর সেটির মালিকানা চলে যায়, তাই ফাংশন কলের পরেও String
ব্যবহার করতে হলে আমাদের আবার সেটি কলিং ফাংশনে ফেরত পাঠাতে হয়। এর পরিবর্তে, আমরা String
মানটির একটি রেফারেন্স (reference) পাস করতে পারি। একটি রেফারেন্স অনেকটা পয়েন্টারের মতো, কারণ এটি একটি ঠিকানা যা অনুসরণ করে আমরা সেই ঠিকানায় থাকা ডেটা অ্যাক্সেস করতে পারি; সেই ডেটার মালিক অন্য কোনো ভ্যারিয়েবল। পয়েন্টারের সাথে এর পার্থক্য হলো, একটি রেফারেন্স তার জীবনকাল পর্যন্ত একটি নির্দিষ্ট টাইপের বৈধ মানের দিকে নির্দেশ করার নিশ্চয়তা দেয়।
এখানে দেখানো হলো কীভাবে আপনি একটি calculate_length
ফাংশন তৈরি ও ব্যবহার করতে পারেন যা মানের মালিকানা না নিয়ে বরং একটি অবজেক্টের রেফারেন্স প্যারামিটার হিসেবে গ্রহণ করে:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { s.len() }
প্রথমত, লক্ষ্য করুন যে ভ্যারিয়েবল ঘোষণা এবং ফাংশনের রিটার্ন ভ্যালু থেকে সমস্ত টাপল কোড চলে গেছে। দ্বিতীয়ত, লক্ষ্য করুন যে আমরা calculate_length
ফাংশনে &s1
পাস করেছি এবং ফাংশনের সংজ্ঞায় String
-এর পরিবর্তে &String
নিয়েছি। এই অ্যামপারস্যান্ড (&
) চিহ্নগুলো রেফারেন্স বোঝায়, এবং এগুলো আপনাকে কোনো মানের মালিকানা না নিয়েই সেটিকে নির্দেশ করতে দেয়। চিত্র ৪-৬ এই ধারণাটি চিত্রিত করে।
চিত্র ৪-৬: &String s
যেভাবে String s1
-কে নির্দেশ করে তার একটি চিত্র
দ্রষ্টব্য:
&
ব্যবহার করে রেফারেন্সিং করার বিপরীতটি হলো ডি-রেফারেন্সিং (dereferencing), যা ডি-রেফারেন্স অপারেটর,*
দিয়ে করা হয়। আমরা অধ্যায় ৮-এ ডি-রেফারেন্স অপারেটরের কিছু ব্যবহার দেখব এবং অধ্যায় ১৫-এ ডি-রেফারেন্সিংয়ের বিস্তারিত আলোচনা করব।
আসুন এখানকার ফাংশন কলটি আরও ঘনিষ্ঠভাবে দেখি:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { s.len() }
&s1
সিনট্যাক্সটি আমাদের একটি রেফারেন্স তৈরি করতে দেয় যা s1
-এর মানকে নির্দেশ (refers) করে কিন্তু এর মালিকানা নেয় না। যেহেতু রেফারেন্সটির মালিকানা নেই, তাই রেফারেন্সটি ব্যবহার শেষ হয়ে গেলে এটি যে মানটিকে নির্দেশ করে তা ড্রপ (dropped) করা হবে না।
একইভাবে, ফাংশনের সিগনেচার &
ব্যবহার করে নির্দেশ করে যে প্যারামিটার s
-এর টাইপ একটি রেফারেন্স। আসুন কিছু ব্যাখ্যামূলক টীকা যোগ করি:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{s1}' is {len}."); } fn calculate_length(s: &String) -> usize { // s is a reference to a String s.len() } // Here, s goes out of scope. But because s does not have ownership of what // it refers to, the String is not dropped.
যে স্কোপে s
ভ্যারিয়েবলটি বৈধ, তা যেকোনো ফাংশন প্যারামিটারের স্কোপের মতোই, কিন্তু s
-এর ব্যবহার শেষ হলে রেফারেন্স দ্বারা নির্দেশিত মানটি ড্রপ করা হয় না, কারণ s
-এর মালিকানা নেই। যখন ফাংশনগুলো আসল মানের পরিবর্তে রেফারেন্সকে প্যারামিটার হিসাবে ব্যবহার করে, তখন মালিকানা ফিরিয়ে দেওয়ার জন্য আমাদের মানগুলো ফেরত পাঠাতে হবে না, কারণ আমাদের কখনোই মালিকানা ছিল না।
একটি রেফারেন্স তৈরি করার এই প্রক্রিয়াকে আমরা ধার করা (borrowing) বলি। বাস্তব জীবনের মতোই, যদি কোনো ব্যক্তির কোনো কিছুর মালিকানা থাকে, আপনি তাদের কাছ থেকে তা ধার নিতে পারেন। আপনার কাজ শেষ হলে, আপনাকে তা ফিরিয়ে দিতে হবে। আপনি সেটির মালিক নন।
তাহলে, আমরা যদি ধার করা কোনো কিছু পরিবর্তন করার চেষ্টা করি তাহলে কী হবে? তালিকা ৪-৬-এর কোডটি চেষ্টা করুন। স্পয়লার অ্যালার্ট: এটি কাজ করবে না!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
এখানে এররটি হলো:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
ভ্যারিয়েবলগুলো যেমন ডিফল্টভাবে অপরিবর্তনীয় (immutable), রেফারেন্সও তেমনি। আমাদের কাছে কোনো কিছুর রেফারেন্স থাকলে আমরা তা পরিবর্তন করার অনুমতি পাই না।
পরিবর্তনযোগ্য রেফারেন্স (Mutable References)
আমরা তালিকা ৪-৬ এর কোডটিকে কয়েকটি ছোট পরিবর্তনের মাধ্যমে ঠিক করতে পারি, যাতে আমরা একটি ধার করা মান পরিবর্তন করতে পারি। এর জন্য আমরা একটি পরিবর্তনযোগ্য রেফারেন্স (mutable reference) ব্যবহার করব:
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
প্রথমে আমরা s
-কে mut
করি। তারপর আমরা change
ফাংশন কল করার সময় &mut s
দিয়ে একটি পরিবর্তনযোগ্য রেফারেন্স তৈরি করি, এবং ফাংশন সিগনেচার আপডেট করে some_string: &mut String
দিয়ে একটি পরিবর্তনযোগ্য রেফারেন্স গ্রহণ করি। এটি খুব স্পষ্ট করে দেয় যে change
ফাংশনটি তার ধার করা মানটিকে পরিবর্তন করবে।
পরিবর্তনযোগ্য রেফারেন্সের একটি বড় সীমাবদ্ধতা আছে: যদি আপনার কাছে একটি মানের একটি পরিবর্তনযোগ্য রেফারেন্স থাকে, তবে সেই মানের অন্য কোনো রেফারেন্স আপনার কাছে থাকতে পারবে না। s
-এর জন্য দুটি পরিবর্তনযোগ্য রেফারেন্স তৈরি করার এই কোডটি ব্যর্থ হবে:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}```
</Listing>
এখানে এররটি হলো:
```console
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| ---- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
এই এরর বলছে যে এই কোডটি অবৈধ কারণ আমরা একবারে একাধিকবার s
-কে পরিবর্তনযোগ্য হিসাবে ধার করতে পারি না। প্রথম পরিবর্তনযোগ্য ধার r1
-এ রয়েছে এবং এটি println!
-এ ব্যবহৃত না হওয়া পর্যন্ত স্থায়ী হতে হবে, কিন্তু সেই পরিবর্তনযোগ্য রেফারেন্স তৈরির এবং তার ব্যবহারের মধ্যে, আমরা r2
-তে আরেকটি পরিবর্তনযোগ্য রেফারেন্স তৈরি করার চেষ্টা করেছি যা r1
-এর মতো একই ডেটা ধার করে।
একই ডেটার একাধিক পরিবর্তনযোগ্য রেফারেন্স থাকার সীমাবদ্ধতাটি পরিবর্তন করার সুযোগ দেয় কিন্তু খুব নিয়ন্ত্রিত উপায়ে। নতুন রাস্টেশিয়ানদের (Rustaceans) জন্য এটি একটি সমস্যা কারণ বেশিরভাগ ভাষা আপনাকে যখন ইচ্ছা পরিবর্তন করতে দেয়। এই সীমাবদ্ধতার সুবিধা হলো রাস্ট কম্পাইলের সময় ডেটা রেস (data races) প্রতিরোধ করতে পারে। একটি ডেটা রেস রেস কন্ডিশনের মতোই এবং এটি ঘটে যখন এই তিনটি আচরণ ঘটে:
- দুই বা ততোধিক পয়েন্টার একই সময়ে একই ডেটা অ্যাক্সেস করে।
- অন্তত একটি পয়েন্টার ডেটাতে লেখার জন্য ব্যবহৃত হচ্ছে।
- ডেটা অ্যাক্সেস সিঙ্ক্রোনাইজ করার জন্য কোনো ব্যবস্থা ব্যবহার করা হচ্ছে না।
ডেটা রেসগুলো অনির্ধারিত আচরণের (undefined behavior) কারণ হয় এবং রানটাইমে এগুলো খুঁজে বের করার চেষ্টা করার সময় নির্ণয় এবং ঠিক করা কঠিন হতে পারে; রাস্ট ডেটা রেসসহ কোড কম্পাইল করতে অস্বীকার করে এই সমস্যাটি প্রতিরোধ করে!
বরাবরের মতোই, আমরা কার্লি ব্র্যাকেট ব্যবহার করে একটি নতুন স্কোপ তৈরি করতে পারি, যা একাধিক পরিবর্তনযোগ্য রেফারেন্সের অনুমতি দেয়, শুধু একই সাথে নয়:
fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1 goes out of scope here, so we can make a new reference with no problems. let r2 = &mut s; }
রাস্ট পরিবর্তনযোগ্য এবং অপরিবর্তনীয় রেফারেন্স একত্রিত করার জন্যও একটি অনুরূপ নিয়ম প্রয়োগ করে। এই কোডটি একটি এরর তৈরি করে:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
}
এখানে এররটি হলো:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
আমরা যখন একটি মানের একটি অপরিবর্তনীয় রেফারেন্স রাখি, তখন আমরা একটি পরিবর্তনযোগ্য রেফারেন্সও রাখতে পারি না।
অপরিবর্তনীয় রেফারেন্সের ব্যবহারকারীরা আশা করেন না যে মানটি হঠাৎ করে তাদের অজান্তেই পরিবর্তিত হয়ে যাবে! তবে, একাধিক অপরিবর্তনীয় রেফারেন্স অনুমোদিত কারণ যারা কেবল ডেটা পড়ছে তাদের কেউই অন্য কারো ডেটা পড়াকে প্রভাবিত করতে পারে না।
লক্ষ্য করুন যে একটি রেফারেন্সের স্কোপ যেখানে এটি তৈরি হয় সেখান থেকে শুরু হয় এবং সেই রেফারেন্সটি শেষবার ব্যবহৃত হওয়া পর্যন্ত চলতে থাকে। উদাহরণস্বরূপ, এই কোডটি কম্পাইল হবে কারণ অপরিবর্তনীয় রেফারেন্সগুলোর শেষ ব্যবহার হয় println!
-এ, পরিবর্তনযোগ্য রেফারেন্সটি চালু করার আগে:
fn main() { let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem println!("{r1} and {r2}"); // Variables r1 and r2 will not be used after this point. let r3 = &mut s; // no problem println!("{r3}"); }
অপরিবর্তনীয় রেফারেন্স r1
এবং r2
-এর স্কোপ println!
-এর পরে শেষ হয়, যেখানে সেগুলি শেষবার ব্যবহৃত হয়, যা পরিবর্তনযোগ্য রেফারেন্স r3
তৈরি হওয়ার আগে। এই স্কোপগুলো ওভারল্যাপ করে না, তাই এই কোডটি অনুমোদিত: কম্পাইলার বলতে পারে যে স্কোপ শেষ হওয়ার আগেই রেফারেন্সটি আর ব্যবহৃত হচ্ছে না।
যদিও ধার করার এররগুলো কখনও কখনও হতাশাজনক হতে পারে, মনে রাখবেন যে এটি রাস্ট কম্পাইলার যা একটি সম্ভাব্য বাগ প্রথম দিকেই (রানটাইমের পরিবর্তে কম্পাইল টাইমে) নির্দেশ করছে এবং আপনাকে ঠিক কোথায় সমস্যাটি তা দেখাচ্ছে। তাহলে আপনাকে আর খুঁজে বের করতে হবে না কেন আপনার ডেটা আপনি যা ভেবেছিলেন তা নয়।
ড্যাংলিং রেফারেন্স (Dangling References)
পয়েন্টারসহ ভাষাগুলোতে, ভুলবশত একটি ড্যাংলিং পয়েন্টার (dangling pointer)—এমন একটি পয়েন্টার যা মেমরির এমন একটি অবস্থানকে নির্দেশ করে যা হয়তো অন্য কাউকে দেওয়া হয়েছে—তৈরি করা সহজ, কোনো মেমরি মুক্ত করার সময় সেই মেমরির একটি পয়েন্টার সংরক্ষণ করে। এর বিপরীতে, রাস্টে, কম্পাইলার গ্যারান্টি দেয় যে রেফারেন্সগুলো কখনই ড্যাংলিং রেফারেন্স হবে না: যদি আপনার কাছে কিছু ডেটার একটি রেফারেন্স থাকে, কম্পাইলার নিশ্চিত করবে যে ডেটার রেফারেন্সের আগে ডেটা স্কোপের বাইরে যাবে না।
আসুন একটি ড্যাংলিং রেফারেন্স তৈরি করার চেষ্টা করি যাতে দেখা যায় রাস্ট কীভাবে একটি কম্পাইল-টাইম এরর দিয়ে এগুলো প্রতিরোধ করে:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
এখানে এররটি হলো:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:8:5
|
8 | &s
| ^^ returns a reference to data owned by the current function
Some errors have detailed explanations: E0106, E0515.
For more information about an error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 2 previous errors
এই এরর বার্তাটি এমন একটি বৈশিষ্ট্য উল্লেখ করে যা আমরা এখনও আলোচনা করিনি: লাইফটাইম (lifetimes)। আমরা অধ্যায় ১০-এ লাইফটাইম নিয়ে বিস্তারিত আলোচনা করব। কিন্তু, আপনি যদি লাইফটাইম সম্পর্কিত অংশগুলো উপেক্ষা করেন, বার্তাটিতে এই কোডটি কেন একটি সমস্যা তার মূল চাবিকাঠি রয়েছে:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
আসুন আমাদের dangle
কোডের প্রতিটি পর্যায়ে ঠিক কী ঘটছে তা আরও ঘনিষ্ঠভাবে দেখি:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
যেহেতু s
dangle
এর ভিতরে তৈরি করা হয়েছে, তাই dangle
-এর কোড শেষ হয়ে গেলে, s
ডিঅ্যালোকেট (deallocated) করা হবে। কিন্তু আমরা এটির একটি রেফারেন্স ফেরত দেওয়ার চেষ্টা করেছি। এর মানে এই রেফারেন্সটি একটি অবৈধ String
-কে নির্দেশ করবে। এটা মোটেও ভালো না! রাস্ট আমাদের এটা করতে দেবে না।
এখানের সমাধান হলো String
সরাসরি ফেরত দেওয়া:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
এটি কোনো সমস্যা ছাড়াই কাজ করে। মালিকানা মুভ (moved out) হয়ে যায়, এবং কিছুই ডিঅ্যালোকেট করা হয় না।
রেফারেন্সের নিয়মাবলী (The Rules of References)
আসুন আমরা রেফারেন্স সম্পর্কে যা আলোচনা করেছি তা সংক্ষেপে দেখে নিই:
- যেকোনো নির্দিষ্ট সময়ে, আপনার কাছে হয় একটি পরিবর্তনযোগ্য রেফারেন্স অথবা যেকোনো সংখ্যক অপরিবর্তনীয় রেফারেন্স থাকতে পারে।
- রেফারেন্স অবশ্যই সবসময় বৈধ হতে হবে।
এর পরে, আমরা একটি ভিন্ন ধরনের রেফারেন্স দেখব: স্লাইস (slices)।
স্লাইস টাইপ (The Slice Type)
স্লাইস (Slices) আপনাকে একটি কালেকশনের মধ্যে থাকা উপাদানগুলোর একটি অবিচ্ছিন্ন ক্রমকে (contiguous sequence) রেফারেন্স করতে দেয়। স্লাইস এক ধরনের রেফারেন্স, তাই এর কোনো মালিকানা (ownership) নেই।
এখানে একটি ছোট প্রোগ্রামিং সমস্যা দেওয়া হলো: একটি ফাংশন লিখুন যা স্পেস দ্বারা বিভক্ত শব্দযুক্ত একটি স্ট্রিং নেয় এবং সেই স্ট্রিংয়ের প্রথম শব্দটি রিটার্ন করে। যদি ফাংশনটি স্ট্রিংয়ের মধ্যে কোনো স্পেস খুঁজে না পায়, তার মানে পুরো স্ট্রিংটিই একটি শব্দ, তাই সম্পূর্ণ স্ট্রিংটিই রিটার্ন করা উচিত।
দ্রষ্টব্য: স্ট্রিং স্লাইস পরিচিত করানোর উদ্দেশ্যে, আমরা এই বিভাগে শুধুমাত্র ASCII ধরে নিচ্ছি; অধ্যায় ৮-এর "স্ট্রিং সহ UTF-8 এনকোডেড টেক্সট সংরক্ষণ" বিভাগে UTF-8 হ্যান্ডলিং নিয়ে আরও বিস্তারিত আলোচনা করা হয়েছে।
আসুন দেখি স্লাইস ব্যবহার না করে আমরা এই ফাংশনের সিগনেচার কীভাবে লিখতাম, যাতে স্লাইস যে সমস্যার সমাধান করবে তা বোঝা যায়:
fn first_word(s: &String) -> ?
first_word
ফাংশনটির একটি প্যারামিটার আছে যার টাইপ &String
। আমাদের মালিকানার প্রয়োজন নেই, তাই এটি ঠিক আছে। (প্রচলিত রাস্ট কোডে, ফাংশনগুলো প্রয়োজন না হলে তাদের আর্গুমেন্টের মালিকানা নেয় না, এবং এর কারণগুলো আমরা যত এগোব তত স্পষ্ট হবে।) কিন্তু আমাদের কী রিটার্ন করা উচিত? আমাদের কাছে একটি স্ট্রিংয়ের অংশ নিয়ে কথা বলার কোনো উপায় নেই। তবে, আমরা শব্দের শেষের ইনডেক্সটি রিটার্ন করতে পারি, যা একটি স্পেস দ্বারা নির্দেশিত। আসুন তালিকা ৪-৭-এ দেখানো উপায়ে চেষ্টা করি।
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
যেহেতু আমাদের String
এর প্রতিটি উপাদান ধরে ধরে পরীক্ষা করতে হবে এবং দেখতে হবে কোনো মান স্পেস কিনা, তাই আমরা as_bytes
মেথড ব্যবহার করে আমাদের String
-কে একটি বাইট অ্যারেতে রূপান্তর করব।
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
এরপরে, আমরা iter
মেথড ব্যবহার করে বাইট অ্যারের উপর একটি ইটারেটর (iterator) তৈরি করি:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
আমরা অধ্যায় ১৩-এ ইটারেটর নিয়ে আরও বিস্তারিত আলোচনা করব। আপাতত, জেনে রাখুন যে iter
একটি মেথড যা একটি কালেকশনের প্রতিটি উপাদান রিটার্ন করে এবং enumerate
iter
-এর ফলাফলকে মুড়িয়ে (wrap) প্রতিটি উপাদানকে একটি টাপলের অংশ হিসাবে রিটার্ন করে। enumerate
থেকে রিটার্ন করা টাপলের প্রথম উপাদানটি হলো ইনডেক্স, এবং দ্বিতীয় উপাদানটি হলো উপাদানের একটি রেফারেন্স। এটি আমাদের নিজেদের ইনডেক্স গণনা করার চেয়ে একটু বেশি সুবিধাজনক।
যেহেতু enumerate
মেথডটি একটি টাপল রিটার্ন করে, আমরা সেই টাপলটিকে ডিস্ট্রাকচার (destructure) করতে প্যাটার্ন ব্যবহার করতে পারি। আমরা অধ্যায় ৬-এ প্যাটার্ন নিয়ে আরও আলোচনা করব। for
লুপে, আমরা একটি প্যাটার্ন নির্দিষ্ট করি যেখানে টাপলের ইনডেক্সের জন্য i
এবং একক বাইটের জন্য &item
রয়েছে। যেহেতু আমরা .iter().enumerate()
থেকে উপাদানের একটি রেফারেন্স পাই, তাই আমরা প্যাটার্নে &
ব্যবহার করি।
for
লুপের ভিতরে, আমরা বাইট লিটারেল সিনট্যাক্স ব্যবহার করে স্পেস প্রতিনিধিত্বকারী বাইটটি অনুসন্ধান করি। যদি আমরা একটি স্পেস খুঁজে পাই, আমরা অবস্থানটি রিটার্ন করি। অন্যথায়, আমরা s.len()
ব্যবহার করে স্ট্রিংয়ের দৈর্ঘ্য রিটার্ন করি।
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
এখন আমাদের কাছে স্ট্রিংয়ের প্রথম শব্দের শেষের ইনডেক্স খুঁজে বের করার একটি উপায় আছে, কিন্তু একটি সমস্যা আছে। আমরা শুধু একটি usize
রিটার্ন করছি, কিন্তু এটি শুধুমাত্র &String
-এর প্রেক্ষাপটেই একটি অর্থপূর্ণ সংখ্যা। অন্য কথায়, যেহেতু এটি String
থেকে একটি পৃথক মান, তাই ভবিষ্যতে এটি বৈধ থাকবে এমন কোনো নিশ্চয়তা নেই। তালিকা ৪-৮-এর প্রোগ্রামটি বিবেচনা করুন যা তালিকা ৪-৭-এর first_word
ফাংশনটি ব্যবহার করে।
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 s.clear(); // this empties the String, making it equal to "" // word still has the value 5 here, but s no longer has any content that we // could meaningfully use with the value 5, so word is now totally invalid! }
এই প্রোগ্রামটি কোনো এরর ছাড়াই কম্পাইল হয় এবং s.clear()
কল করার পরে word
ব্যবহার করলেও তা হতো। যেহেতু word
s
-এর অবস্থার সাথে একেবারেই সংযুক্ত নয়, word
-এ এখনও 5
মানটি রয়েছে। আমরা s
ভ্যারিয়েবলের সাথে সেই 5
মানটি ব্যবহার করে প্রথম শব্দটি বের করার চেষ্টা করতে পারতাম, কিন্তু এটি একটি বাগ হতো কারণ word
-এ 5
সংরক্ষণ করার পর s
-এর বিষয়বস্তু পরিবর্তিত হয়েছে।
s
-এর ডেটার সাথে word
-এর ইনডেক্সটি অসামঞ্জস্যপূর্ণ হয়ে যাওয়ার চিন্তা করাটা ক্লান্তিকর এবং ভুল-প্রবণ! এই ইনডেক্সগুলো পরিচালনা করা আরও ভঙ্গুর হয়ে যায় যদি আমরা একটি second_word
ফাংশন লিখি। এর সিগনেচারটি এমন হতে হবে:
fn second_word(s: &String) -> (usize, usize) {
এখন আমরা একটি শুরুর এবং একটি শেষের ইনডেক্স ট্র্যাক করছি, এবং আমাদের কাছে আরও বেশি মান রয়েছে যা একটি নির্দিষ্ট অবস্থার ডেটা থেকে গণনা করা হয়েছে কিন্তু সেই অবস্থার সাথে মোটেই আবদ্ধ নয়। আমাদের তিনটি असंबंधित ভ্যারিয়েবল রয়েছে যা সিঙ্কে রাখতে হবে।
ভাগ্যক্রমে, রাস্টের এই সমস্যার একটি সমাধান আছে: স্ট্রিং স্লাইস।
স্ট্রিং স্লাইস (String Slices)
একটি স্ট্রিং স্লাইস (string slice) হলো একটি String
-এর উপাদানগুলোর একটি অবিচ্ছিন্ন ক্রমের রেফারেন্স, এবং এটি দেখতে এইরকম:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
সম্পূর্ণ String
-এর রেফারেন্সের পরিবর্তে, hello
হলো String
-এর একটি অংশের রেফারেন্স, যা অতিরিক্ত [0..5]
অংশে নির্দিষ্ট করা হয়েছে। আমরা ব্র্যাকেটের মধ্যে একটি রেঞ্জ ব্যবহার করে স্লাইস তৈরি করি, [starting_index..ending_index]
নির্দিষ্ট করে, যেখানে starting_index
হলো স্লাইসের প্রথম অবস্থান এবং ending_index
হলো স্লাইসের শেষ অবস্থানের চেয়ে এক বেশি। অভ্যন্তরীণভাবে, স্লাইস ডেটা স্ট্রাকচারটি স্লাইসের শুরুর অবস্থান এবং দৈর্ঘ্য সংরক্ষণ করে, যা ending_index
বিয়োগ starting_index
-এর সাথে মিলে যায়। সুতরাং, let world = &s[6..11];
-এর ক্ষেত্রে, world
এমন একটি স্লাইস হবে যা s
-এর ইনডেক্স ৬-এর বাইটের একটি পয়েন্টার এবং ৫ দৈর্ঘ্যের একটি মান ধারণ করবে।
চিত্র ৪-৭ এটি একটি ডায়াগ্রামে দেখাচ্ছে।
চিত্র ৪-৭: একটি String
-এর অংশকে নির্দেশকারী স্ট্রিং স্লাইস
রাস্টের ..
রেঞ্জ সিনট্যাক্সের সাথে, আপনি যদি ইনডেক্স ০ থেকে শুরু করতে চান, আপনি দুটি পিরিয়ডের আগের মানটি বাদ দিতে পারেন। অন্য কথায়, এগুলো সমান:
#![allow(unused)] fn main() { let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; }
একইভাবে, যদি আপনার স্লাইস String
-এর শেষ বাইট অন্তর্ভুক্ত করে, আপনি শেষের সংখ্যাটি বাদ দিতে পারেন। এর মানে হলো এগুলো সমান:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; }
আপনি সম্পূর্ণ স্ট্রিংয়ের একটি স্লাইস নিতে উভয় মানই বাদ দিতে পারেন। সুতরাং এগুলো সমান:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; }
দ্রষ্টব্য: স্ট্রিং স্লাইস রেঞ্জের ইনডেক্স অবশ্যই বৈধ UTF-8 ক্যারেক্টার সীমানায় হতে হবে। আপনি যদি একটি মাল্টিবাইট ক্যারেক্টারের মাঝখানে একটি স্ট্রিং স্লাইস তৈরি করার চেষ্টা করেন, আপনার প্রোগ্রাম একটি এরর সহ বন্ধ হয়ে যাবে।
এই সমস্ত তথ্য মাথায় রেখে, আসুন first_word
কে একটি স্লাইস রিটার্ন করার জন্য পুনরায় লিখি। যে টাইপটি “স্ট্রিং স্লাইস” বোঝায় তা &str
হিসাবে লেখা হয়:
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
আমরা তালিকা ৪-৭-এর মতোই শব্দের শেষের জন্য ইনডেক্সটি পাই, একটি স্পেসের প্রথম উপস্থিতির সন্ধান করে। যখন আমরা একটি স্পেস খুঁজে পাই, আমরা স্ট্রিংয়ের শুরু এবং স্পেসের ইনডেক্সকে শুরুর এবং শেষের ইনডেক্স হিসাবে ব্যবহার করে একটি স্ট্রিং স্লাইস রিটার্ন করি।
এখন যখন আমরা first_word
কল করি, আমরা একটি একক মান ফেরত পাই যা অন্তর্নিহিত ডেটার সাথে আবদ্ধ। মানটি স্লাইসের শুরুর পয়েন্টের একটি রেফারেন্স এবং স্লাইসের উপাদানগুলোর সংখ্যা নিয়ে গঠিত।
একটি স্লাইস রিটার্ন করা second_word
ফাংশনের জন্যও কাজ করবে:
fn second_word(s: &String) -> &str {
এখন আমাদের একটি সহজবোধ্য API আছে যা ভুল করা অনেক কঠিন কারণ কম্পাইলার নিশ্চিত করবে যে String
-এর রেফারেন্সগুলো বৈধ থাকবে। তালিকা ৪-৮-এর প্রোগ্রামের বাগটি মনে আছে, যখন আমরা প্রথম শব্দের শেষের ইনডেক্স পেয়েছিলাম কিন্তু তারপর স্ট্রিংটি খালি করে দিয়েছিলাম যাতে আমাদের ইনডেক্সটি অবৈধ হয়ে যায়? সেই কোডটি যৌক্তিকভাবে ভুল ছিল কিন্তু কোনো তাৎক্ষণিক এরর দেখায়নি। সমস্যাগুলো পরে দেখা যেত যদি আমরা খালি স্ট্রিংয়ের সাথে প্রথম শব্দের ইনডেক্স ব্যবহার করার চেষ্টা চালিয়ে যেতাম। স্লাইস এই বাগটিকে অসম্ভব করে তোলে এবং আমাদের কোডের সমস্যা সম্পর্কে অনেক আগে জানিয়ে দেয়। first_word
-এর স্লাইস সংস্করণ ব্যবহার করলে একটি কম্পাইল-টাইম এরর আসবে:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {word}");
}
এখানে কম্পাইলার এররটি হলো:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {word}");
| ------ immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
ধার করার নিয়ম থেকে মনে করুন, যদি আমাদের কোনো কিছুর একটি অপরিবর্তনীয় রেফারেন্স থাকে, আমরা একই সাথে একটি পরিবর্তনযোগ্য রেফারেন্স নিতে পারি না। যেহেতু clear
-কে String
-কে ছোট করতে হয়, তাই এর একটি পরিবর্তনযোগ্য রেফারেন্স প্রয়োজন। clear
কলের পরে println!
word
-এর রেফারেন্স ব্যবহার করে, তাই সেই সময়ে অপরিবর্তনীয় রেফারেন্সটি অবশ্যই সক্রিয় থাকতে হবে। রাস্ট clear
-এর পরিবর্তনযোগ্য রেফারেন্স এবং word
-এর অপরিবর্তনীয় রেফারেন্সকে একই সময়ে বিদ্যমান থাকতে দেয় না, এবং কম্পাইলেশন ব্যর্থ হয়। রাস্ট কেবল আমাদের API ব্যবহার করা সহজ করেনি, বরং এটি কম্পাইলের সময় একটি সম্পূর্ণ শ্রেণীর এররও দূর করেছে!
স্ট্রিং লিটারেলস (String Literals) হলো স্লাইস
মনে করুন আমরা বলেছিলাম যে স্ট্রিং লিটারেলগুলো বাইনারির ভিতরে সংরক্ষিত থাকে। এখন যেহেতু আমরা স্লাইস সম্পর্কে জানি, আমরা স্ট্রিং লিটারেলগুলো সঠিকভাবে বুঝতে পারি:
#![allow(unused)] fn main() { let s = "Hello, world!"; }
s
-এর টাইপ এখানে &str
: এটি বাইনারির সেই নির্দিষ্ট পয়েন্টের দিকে নির্দেশকারী একটি স্লাইস। এই কারণেই স্ট্রিং লিটারেলগুলো অপরিবর্তনীয় (immutable); &str
একটি অপরিবর্তনীয় রেফারেন্স।
প্যারামিটার হিসাবে স্ট্রিং স্লাইস
আপনি যে লিটারেল এবং String
মানগুলোর স্লাইস নিতে পারেন তা জানার ফলে আমরা first_word
-এ আরও একটি উন্নতি করতে পারি, এবং তা হলো এর সিগনেচার:
fn first_word(s: &String) -> &str {
একজন আরও অভিজ্ঞ রাস্টেশিয়ান (Rustacean) তালিকা ৪-৯-এ দেখানো সিগনেচারটি লিখবেন কারণ এটি আমাদের &String
মান এবং &str
মান উভয়ের উপর একই ফাংশন ব্যবহার করতে দেয়।
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
যদি আমাদের একটি স্ট্রিং স্লাইস থাকে, আমরা সরাসরি সেটি পাস করতে পারি। যদি আমাদের একটি String
থাকে, আমরা String
-এর একটি স্লাইস বা String
-এর একটি রেফারেন্স পাস করতে পারি। এই নমনীয়তা ডেরিফ কোয়ারশনস (deref coercions)-এর সুবিধা নেয়, যা আমরা অধ্যায় ১৫-এর "ফাংশন এবং মেথডের সাথে ইমপ্লিসিট ডেরিফ কোয়ারশনস" বিভাগে আলোচনা করব।
একটি String
-এর রেফারেন্সের পরিবর্তে একটি স্ট্রিং স্লাইস নেওয়ার জন্য একটি ফাংশন ডিফাইন করা আমাদের API-কে কোনো কার্যকারিতা না হারিয়ে আরও সাধারণ এবং দরকারী করে তোলে:
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // `first_word` works on slices of `String`s, whether partial or whole. let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // `first_word` also works on references to `String`s, which are equivalent // to whole slices of `String`s. let word = first_word(&my_string); let my_string_literal = "hello world"; // `first_word` works on slices of string literals, whether partial or // whole. let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
অন্যান্য স্লাইস (Other Slices)
স্ট্রিং স্লাইস, যেমন আপনি কল্পনা করতে পারেন, স্ট্রিংয়ের জন্য নির্দিষ্ট। কিন্তু আরও একটি সাধারণ স্লাইস টাইপও আছে। এই অ্যারেটি বিবেচনা করুন:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
ঠিক যেমন আমরা একটি স্ট্রিংয়ের অংশকে রেফার করতে চাই, আমরা একটি অ্যারের অংশকে রেফার করতে চাইতে পারি। আমরা এটি এভাবে করব:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]); }
এই স্লাইসটির টাইপ &[i32]
। এটি স্ট্রিং স্লাইসের মতোই কাজ করে, প্রথম উপাদানের একটি রেফারেন্স এবং একটি দৈর্ঘ্য সংরক্ষণ করে। আপনি এই ধরনের স্লাইস সব ধরনের অন্যান্য কালেকশনের জন্য ব্যবহার করবেন। আমরা অধ্যায় ৮-এ ভেক্টর নিয়ে আলোচনা করার সময় এই কালেকশনগুলো নিয়ে বিস্তারিত আলোচনা করব।
সারসংক্ষেপ (Summary)
মালিকানা, ধার করা এবং স্লাইসের ধারণাগুলো রাস্ট প্রোগ্রামে কম্পাইলের সময় মেমরি সুরক্ষা নিশ্চিত করে। রাস্ট ভাষা আপনাকে অন্যান্য সিস্টেমস প্রোগ্রামিং ভাষার মতো আপনার মেমরি ব্যবহারের উপর নিয়ন্ত্রণ দেয়, কিন্তু ডেটার মালিক স্কোপের বাইরে চলে গেলে স্বয়ংক্রিয়ভাবে সেই ডেটা পরিষ্কার করার মানে হলো আপনাকে এই নিয়ন্ত্রণ পেতে অতিরিক্ত কোড লিখতে এবং ডিবাগ করতে হবে না।
মালিকানা রাস্টের অন্যান্য অনেক অংশ কীভাবে কাজ করে তা প্রভাবিত করে, তাই আমরা বইয়ের বাকি অংশে এই ধারণাগুলো নিয়ে আরও আলোচনা করব। চলুন অধ্যায় ৫-এ যাই এবং struct
-এ ডেটার অংশগুলোকে একত্রিত করা দেখি।
সম্পর্কিত ডেটা গঠন করার জন্য struct
ব্যবহার
একটি struct
বা স্ট্রাকচার (structure) হলো একটি কাস্টম ডেটা টাইপ, যা আপনাকে একাধিক সম্পর্কিত মানকে একসাথে একটি অর্থপূর্ণ গ্রুপ হিসাবে প্যাকেজ করতে এবং সেগুলোকে নাম দিতে সাহায্য করে। আপনি যদি কোনো অবজেক্ট-ওরিয়েন্টেড (object-oriented) ভাষার সাথে পরিচিত হন, তবে একটি struct
হলো একটি অবজেক্টের ডেটা অ্যাট্রিবিউটের (data attributes) মতো। এই অধ্যায়ে, আমরা টাপল (tuples) এবং struct
-এর মধ্যে তুলনা ও পার্থক্য তুলে ধরব, যাতে আপনার পূর্ববর্তী জ্ঞানের উপর ভিত্তি করে দেখানো যায় কখন ডেটা গ্রুপ করার জন্য struct
একটি ভালো উপায়।
আমরা দেখাব কীভাবে struct
ডিফাইন (define) এবং ইনস্ট্যানশিয়েট (instantiate) করতে হয়। একটি struct
টাইপের সাথে সম্পর্কিত আচরণ (behavior) নির্দিষ্ট করার জন্য, আমরা অ্যাসোসিয়েটেড ফাংশন (associated functions) ডিফাইন করার পদ্ধতি নিয়ে আলোচনা করব, বিশেষ করে সেই ফাংশনগুলো যা মেথড (methods) নামে পরিচিত। struct
এবং enum
(অধ্যায় ৬-এ আলোচিত) হলো আপনার প্রোগ্রামের ডোমেইনে নতুন টাইপ তৈরি করার মূল বিল্ডিং ব্লক (building blocks), যা আপনাকে রাস্টের কম্পাইল-টাইম টাইপ চেকিং (compile-time type checking) এর সম্পূর্ণ সুবিধা নিতে সাহায্য করে।
struct
ডিফাইন এবং ইনস্ট্যানশিয়েট করা (Defining and Instantiating Structs)
struct
অনেকটা টাপলের (tuple) মতোই, যা "টাপল টাইপ" বিভাগে আলোচনা করা হয়েছে, কারণ উভয়ই একাধিক সম্পর্কিত মান ধারণ করে। টাপলের মতো, struct
-এর অংশগুলো বিভিন্ন টাইপের হতে পারে। তবে টাপলের সাথে এর পার্থক্য হলো, struct
-এ আপনি প্রতিটি ডেটার একটি নাম দেবেন যাতে মানগুলোর অর্থ স্পষ্ট হয়। এই নামগুলো যোগ করার ফলে struct
টাপলের চেয়ে বেশি নমনীয় হয়: আপনাকে কোনো ইনস্ট্যান্সের মান নির্দিষ্ট বা অ্যাক্সেস করার জন্য ডেটার ক্রমের উপর নির্ভর করতে হয় না।
একটি struct
ডিফাইন করার জন্য, আমরা struct
কীওয়ার্ডটি লিখি এবং পুরো struct
-টির একটি নাম দিই। একটি struct
-এর নাম এমন হওয়া উচিত যা একসাথে গ্রুপ করা ডেটার গুরুত্ব বর্ণনা করে। তারপর, কার্লি ব্র্যাকেটের ভিতরে, আমরা ডেটার অংশগুলোর নাম এবং টাইপ ডিফাইন করি, যেগুলোকে আমরা ফিল্ড (fields) বলি। উদাহরণস্বরূপ, তালিকা ৫-১ একটি struct
দেখাচ্ছে যা একটি ব্যবহারকারীর অ্যাকাউন্ট সম্পর্কে তথ্য সংরক্ষণ করে।
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() {}
একটি struct
ডিফাইন করার পর এটি ব্যবহার করার জন্য, আমরা প্রতিটি ফিল্ডের জন্য সুনির্দিষ্ট মান নির্দিষ্ট করে সেই struct
-এর একটি ইনস্ট্যান্স (instance) তৈরি করি। আমরা struct
-এর নাম উল্লেখ করে একটি ইনস্ট্যান্স তৈরি করি এবং তারপর কার্লি ব্র্যাকেটের মধ্যে key: value
জোড়া যোগ করি, যেখানে কী-গুলো (keys) হলো ফিল্ডগুলোর নাম এবং ভ্যালুগুলো (values) হলো সেই ডেটা যা আমরা সেই ফিল্ডগুলোতে সংরক্ষণ করতে চাই। আমাদের ফিল্ডগুলোকে struct
-এ যে ক্রমে ঘোষণা করা হয়েছে সেই একই ক্রমে নির্দিষ্ট করতে হবে না। অন্য কথায়, struct
-এর সংজ্ঞাটি টাইপের জন্য একটি সাধারণ টেমপ্লেটের মতো, এবং ইনস্ট্যান্সগুলো সেই টেমপ্লেটটি নির্দিষ্ট ডেটা দিয়ে পূরণ করে টাইপের মান তৈরি করে। উদাহরণস্বরূপ, আমরা তালিকা ৫-২-এ দেখানো উপায়ে একটি নির্দিষ্ট ব্যবহারকারী ঘোষণা করতে পারি।
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; }
একটি struct
থেকে একটি নির্দিষ্ট মান পেতে, আমরা ডট নোটেশন (dot notation) ব্যবহার করি। উদাহরণস্বরূপ, এই ব্যবহারকারীর ইমেল ঠিকানা অ্যাক্সেস করতে, আমরা user1.email
ব্যবহার করি। যদি ইনস্ট্যান্সটি পরিবর্তনযোগ্য (mutable) হয়, আমরা ডট নোটেশন ব্যবহার করে এবং একটি নির্দিষ্ট ফিল্ডে মান অ্যাসাইন করে একটি মান পরিবর্তন করতে পারি। তালিকা ৫-৩ দেখাচ্ছে কীভাবে একটি পরিবর্তনযোগ্য User
ইনস্ট্যান্সের email
ফিল্ডের মান পরিবর্তন করতে হয়।
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let mut user1 = User { active: true, username: String::from("someusername123"), email: String::from("someone@example.com"), sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
মনে রাখবেন যে পুরো ইনস্ট্যান্সটি অবশ্যই পরিবর্তনযোগ্য হতে হবে; রাস্ট আমাদের শুধুমাত্র নির্দিষ্ট কিছু ফিল্ডকে পরিবর্তনযোগ্য হিসাবে চিহ্নিত করার অনুমতি দেয় না। যেকোনো এক্সপ্রেশনের মতো, আমরা ফাংশন বডির শেষ এক্সপ্রেশন হিসাবে struct
-এর একটি নতুন ইনস্ট্যান্স তৈরি করে সেই নতুন ইনস্ট্যান্সটি উহ্যভাবে (implicitly) রিটার্ন করতে পারি।
তালিকা ৫-৪ একটি build_user
ফাংশন দেখাচ্ছে যা প্রদত্ত ইমেল এবং ব্যবহারকারীর নাম সহ একটি User
ইনস্ট্যান্স রিটার্ন করে। active
ফিল্ডটি true
মান পায়, এবং sign_in_count
একটি মান 1
পায়।
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn build_user(email: String, username: String) -> User { User { active: true, username: username, email: email, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("someone@example.com"), String::from("someusername123"), ); }``` </Listing> ফাংশন প্যারামিটারগুলোকে `struct` ফিল্ডগুলোর একই নামে নামকরণ করা যুক্তিযুক্ত, কিন্তু `email` এবং `username` ফিল্ডের নাম এবং ভ্যারিয়েবলগুলো পুনরাবৃত্তি করা কিছুটা ক্লান্তিকর। যদি `struct`-এ আরও ফিল্ড থাকত, তবে প্রতিটি নামের পুনরাবৃত্তি আরও বিরক্তিকর হয়ে উঠত। ভাগ্যক্রমে, একটি সুবিধাজনক সংক্ষিপ্ত উপায় আছে! <!-- Old heading. Do not remove or links may break. --> <a id="using-the-field-init-shorthand-when-variables-and-fields-have-the-same-name"></a> ## ফিল্ড ইনিশিয়ালাইজেশন শর্টহ্যান্ড ব্যবহার (Using the Field Init Shorthand) যেহেতু তালিকা ৫-৪-এ প্যারামিটারের নাম এবং `struct` ফিল্ডের নাম ঠিক একই, তাই আমরা `build_user` ফাংশনটিকে পুনরায় লেখার জন্য _ফিল্ড ইনিশিয়ালাইজেশন শর্টহ্যান্ড_ (field init shorthand) সিনট্যাক্স ব্যবহার করতে পারি যাতে এটি ঠিক একইভাবে আচরণ করে কিন্তু `username` এবং `email`-এর পুনরাবৃত্তি না থাকে, যেমনটি তালিকা ৫-৫-এ দেখানো হয়েছে। <Listing number="5-5" file-name="src/main.rs" caption="একটি `build_user` ফাংশন যা ফিল্ড ইনিশিয়ালাইজেশন শর্টহ্যান্ড ব্যবহার করে কারণ `username` এবং `email` প্যারামিটারগুলোর নাম `struct` ফিল্ডগুলোর নামের সমান"> ```rust struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn build_user(email: String, username: String) -> User { User { active: true, username, email, sign_in_count: 1, } } fn main() { let user1 = build_user( String::from("someone@example.com"), String::from("someusername123"), ); }
এখানে, আমরা User
struct
-এর একটি নতুন ইনস্ট্যান্স তৈরি করছি, যার email
নামে একটি ফিল্ড রয়েছে। আমরা email
ফিল্ডের মান build_user
ফাংশনের email
প্যারামিটারের মানে সেট করতে চাই। যেহেতু email
ফিল্ড এবং email
প্যারামিটারের নাম একই, আমাদের কেবল email
লিখতে হবে, email: email
লেখার পরিবর্তে।
স্ট্রাকট আপডেট সিনট্যাক্স দিয়ে অন্য ইনস্ট্যান্স থেকে ইনস্ট্যান্স তৈরি
প্রায়শই একই ধরনের অন্য একটি ইনস্ট্যান্সের বেশিরভাগ মান ব্যবহার করে একটি struct
-এর নতুন ইনস্ট্যান্স তৈরি করা দরকারী হয়, কিন্তু কিছু মান পরিবর্তন করতে হয়। আপনি এটি স্ট্রাকট আপডেট সিনট্যাক্স (struct update syntax) ব্যবহার করে করতে পারেন।
প্রথমে, তালিকা ৫-৬-এ আমরা আপডেট সিনট্যাক্স ছাড়া নিয়মিতভাবে user2
-তে একটি নতুন User
ইনস্ট্যান্স কীভাবে তৈরি করতে হয় তা দেখাই। আমরা email
-এর জন্য একটি নতুন মান সেট করি কিন্তু অন্যথায় তালিকা ৫-২-এ তৈরি করা user1
-এর একই মান ব্যবহার করি।
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { // --snip-- let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; }
স্ট্রাকট আপডেট সিনট্যাক্স ব্যবহার করে, আমরা কম কোডে একই ফলাফল অর্জন করতে পারি, যেমনটি তালিকা ৫-৭-এ দেখানো হয়েছে। ..
সিনট্যাক্সটি নির্দিষ্ট করে যে বাকি ফিল্ডগুলো যা স্পষ্টভাবে সেট করা হয়নি সেগুলোর মান প্রদত্ত ইনস্ট্যান্সের ফিল্ডগুলোর মতোই হবে।
struct User { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { // --snip-- let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("another@example.com"), ..user1 }; }
তালিকা ৫-৭-এর কোডটি user2
-তে একটি ইনস্ট্যান্স তৈরি করে যার email
-এর জন্য একটি ভিন্ন মান রয়েছে কিন্তু user1
-এর username
, active
এবং sign_in_count
ফিল্ডগুলোর মান একই। ..user1
অবশ্যই শেষে আসতে হবে যাতে নির্দিষ্ট করা যায় যে কোনো বাকি ফিল্ড তাদের মান user1
-এর সংশ্লিষ্ট ফিল্ড থেকে পাবে, কিন্তু আমরা struct
-এর সংজ্ঞায় ফিল্ডগুলোর ক্রম নির্বিশেষে যেকোনো ক্রমে যতগুলো ফিল্ডের জন্য মান নির্দিষ্ট করতে পারি।
মনে রাখবেন যে স্ট্রাকট আপডেট সিনট্যাক্স একটি অ্যাসাইনমেন্টের মতো =
ব্যবহার করে; এর কারণ এটি ডেটা মুভ (moves) করে, যেমনটি আমরা "Move এর মাধ্যমে ভ্যারিয়েবল এবং ডেটার মিথস্ক্রিয়া" বিভাগে দেখেছি। এই উদাহরণে, user2
তৈরি করার পরে আমরা আর user1
ব্যবহার করতে পারি না কারণ user1
-এর username
ফিল্ডের String
user2
-তে মুভ করা হয়েছে। যদি আমরা user2
-কে email
এবং username
উভয়ের জন্য নতুন String
মান দিতাম, এবং এইভাবে user1
থেকে শুধুমাত্র active
এবং sign_in_count
মান ব্যবহার করতাম, তাহলে user2
তৈরি করার পরেও user1
বৈধ থাকত। active
এবং sign_in_count
উভয়ই এমন টাইপ যা Copy
ট্রেইট ইমপ্লিমেন্ট করে, তাই "শুধুমাত্র-স্ট্যাক ডেটা: কপি" বিভাগে আলোচনা করা আচরণ প্রযোজ্য হবে। আমরা এই উদাহরণে এখনও user1.email
ব্যবহার করতে পারি, কারণ এর মান user1
থেকে মুভ করা হয়নি।
বিভিন্ন টাইপ তৈরি করতে নামবিহীন ফিল্ড সহ টাপল স্ট্রাকট ব্যবহার
রাস্ট টাপলের মতো দেখতে struct
সমর্থন করে, যাকে টাপল স্ট্রাকট (tuple structs) বলা হয়। টাপল স্ট্রাকটগুলোতে struct
নামের অতিরিক্ত অর্থ থাকে তবে তাদের ফিল্ডগুলোর সাথে কোনো নাম যুক্ত থাকে না; বরং, তাদের কেবল ফিল্ডগুলোর টাইপ থাকে। টাপল স্ট্রাকটগুলো দরকারী যখন আপনি পুরো টাপলটিকে একটি নাম দিতে চান এবং টাপলটিকে অন্য টাপল থেকে একটি ভিন্ন টাইপ করতে চান, এবং যখন একটি নিয়মিত struct
-এর মতো প্রতিটি ফিল্ডের নামকরণ করা ভার্বোস (verbose) বা অপ্রয়োজনীয় (redundant) হবে।
একটি টাপল স্ট্রাকট ডিফাইন করতে, struct
কীওয়ার্ড এবং struct
-এর নাম দিয়ে শুরু করুন এবং তারপরে টাপলের টাইপগুলো দিন। উদাহরণস্বরূপ, এখানে আমরা Color
এবং Point
নামে দুটি টাপল স্ট্রাকট ডিফাইন এবং ব্যবহার করি:
struct Color(i32, i32, i32); struct Point(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
মনে রাখবেন যে black
এবং origin
মানগুলো ভিন্ন টাইপের কারণ সেগুলো ভিন্ন টাপল স্ট্রাকটের ইনস্ট্যান্স। আপনি যে প্রতিটি struct
ডিফাইন করেন তা তার নিজস্ব টাইপ, যদিও struct
-এর ভিতরের ফিল্ডগুলোর টাইপ একই হতে পারে। উদাহরণস্বরূপ, একটি ফাংশন যা Color
টাইপের একটি প্যারামিটার নেয় তা আর্গুমেন্ট হিসাবে একটি Point
নিতে পারে না, যদিও উভয় টাইপই তিনটি i32
মান দিয়ে তৈরি। অন্যথায়, টাপল স্ট্রাকট ইনস্ট্যান্সগুলো টাপলের মতোই যে আপনি সেগুলোকে তাদের পৃথক অংশে ডিস্ট্রাকচার করতে পারেন, এবং আপনি একটি .
এবং তারপরে ইনডেক্স ব্যবহার করে একটি পৃথক মান অ্যাক্সেস করতে পারেন। টাপলের মতো নয়, টাপল স্ট্রাকটগুলোর ক্ষেত্রে আপনাকে struct
-এর টাইপের নাম দিতে হবে যখন আপনি সেগুলোকে ডিস্ট্রাকচার করবেন। উদাহরণস্বরূপ, origin
পয়েন্টের মানগুলোকে x
, y
, এবং z
নামের ভ্যারিয়েবলে ডিস্ট্রাকচার করতে আমরা let Point(x, y, z) = origin;
লিখব।
কোনো ফিল্ড ছাড়া ইউনিট-লাইক স্ট্রাকট
আপনি এমন struct
ও ডিফাইন করতে পারেন যার কোনো ফিল্ড নেই! এগুলোকে ইউনিট-লাইক স্ট্রাকট (unit-like structs) বলা হয় কারণ তারা ()
-এর মতো আচরণ করে, যা আমরা "টাপল টাইপ" বিভাগে উল্লেখ করেছি। ইউনিট-লাইক স্ট্রাকটগুলো দরকারী হতে পারে যখন আপনার কোনো টাইপের উপর একটি ট্রেইট ইমপ্লিমেন্ট করতে হবে কিন্তু আপনার কাছে এমন কোনো ডেটা নেই যা আপনি টাইপের মধ্যে সংরক্ষণ করতে চান। আমরা অধ্যায় ১০-এ ট্রেইট নিয়ে আলোচনা করব। এখানে AlwaysEqual
নামে একটি ইউনিট স্ট্রাকট ঘোষণা এবং ইনস্ট্যানশিয়েট করার একটি উদাহরণ দেওয়া হলো:
struct AlwaysEqual; fn main() { let subject = AlwaysEqual; }
AlwaysEqual
ডিফাইন করতে, আমরা struct
কীওয়ার্ড, যে নামটি আমরা চাই, এবং তারপর একটি সেমিকোলন ব্যবহার করি। কার্লি ব্র্যাকেট বা প্যারেন্থেসিসের প্রয়োজন নেই! তারপর আমরা subject
ভ্যারিয়েবলে AlwaysEqual
-এর একটি ইনস্ট্যান্স পেতে পারি একই ভাবে: আমরা যে নামটি ডিফাইন করেছি তা ব্যবহার করে, কোনো কার্লি ব্র্যাকেট বা প্যারেন্থেসিস ছাড়াই। কল্পনা করুন যে পরে আমরা এই টাইপের জন্য এমন আচরণ ইমপ্লিমেন্ট করব যাতে AlwaysEqual
-এর প্রতিটি ইনস্ট্যান্স সবসময় অন্য যেকোনো টাইপের প্রতিটি ইনস্ট্যান্সের সমান হয়, সম্ভবত পরীক্ষার উদ্দেশ্যে একটি পরিচিত ফলাফল পাওয়ার জন্য। সেই আচরণ ইমপ্লিমেন্ট করার জন্য আমাদের কোনো ডেটার প্রয়োজন হবে না! আপনি অধ্যায় ১০-এ দেখবেন কীভাবে ট্রেইট ডিফাইন করতে হয় এবং সেগুলোকে যেকোনো টাইপের উপর ইমপ্লিমেন্ট করতে হয়, যার মধ্যে ইউনিট-লাইক স্ট্রাকটও রয়েছে।
স্ট্রাকট ডেটার মালিকানা (Ownership of Struct Data)
তালিকা ৫-১ এর
User
struct
সংজ্ঞায়, আমরা&str
স্ট্রিং স্লাইস টাইপের পরিবর্তে ওনড (owned)String
টাইপ ব্যবহার করেছি। এটি একটি ইচ্ছাকৃত পছন্দ কারণ আমরা চাই এইstruct
-এর প্রতিটি ইনস্ট্যান্স তার সমস্ত ডেটার মালিক হোক এবং সেই ডেটা পুরোstruct
-টি বৈধ থাকা পর্যন্ত বৈধ থাকুক।
struct
-এর পক্ষে অন্য কারো মালিকানাধীন ডেটার রেফারেন্স সংরক্ষণ করাও সম্ভব, কিন্তু তা করার জন্য লাইফটাইম (lifetimes) ব্যবহার করতে হয়, যা রাস্টের একটি বৈশিষ্ট্য এবং আমরা অধ্যায় ১০-এ আলোচনা করব। লাইফটাইম নিশ্চিত করে যে একটিstruct
দ্বারা রেফারেন্স করা ডেটাstruct
-টি বৈধ থাকা পর্যন্ত বৈধ থাকবে। ধরুন আপনি লাইফটাইম নির্দিষ্ট না করে একটিstruct
-এ একটি রেফারেন্স সংরক্ষণ করার চেষ্টা করছেন, যেমনটি নিচে দেখানো হলো; এটি কাজ করবে না:struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }
কম্পাইলার অভিযোগ করবে যে তার লাইফটাইম স্পেসিফায়ার প্রয়োজন:
$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` (bin "structs") due to 2 previous errors
অধ্যায় ১০-এ, আমরা আলোচনা করব কীভাবে এই এররগুলো ঠিক করতে হয় যাতে আপনি
struct
-এ রেফারেন্স সংরক্ষণ করতে পারেন, কিন্তু আপাতত, আমরা এই ধরনের এররগুলো&str
-এর মতো রেফারেন্সের পরিবর্তেString
-এর মতো ওনড টাইপ ব্যবহার করে ঠিক করব।
struct
ব্যবহার করে একটি উদাহরণ প্রোগ্রাম
আমরা কখন struct
ব্যবহার করতে চাই তা বোঝার জন্য, আসুন একটি প্রোগ্রাম লিখি যা একটি আয়তক্ষেত্রের ক্ষেত্রফল গণনা করে। আমরা প্রথমে একক ভ্যারিয়েবল ব্যবহার করে শুরু করব, এবং তারপর প্রোগ্রামটিকে রিফ্যাক্টর (refactor) করে struct
ব্যবহার করব।
আসুন কার্গো (Cargo) দিয়ে rectangles নামে একটি নতুন বাইনারি প্রজেক্ট তৈরি করি যা পিক্সেল এককে একটি আয়তক্ষেত্রের প্রস্থ এবং উচ্চতা নিয়ে আয়তক্ষেত্রটির ক্ষেত্রফল গণনা করবে। তালিকা ৫-৮ আমাদের প্রজেক্টের src/main.rs ফাইলে ঠিক এটি করার একটি সংক্ষিপ্ত প্রোগ্রাম দেখাচ্ছে।
fn main() { let width1 = 30; let height1 = 50; println!( "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
এখন, cargo run
ব্যবহার করে এই প্রোগ্রামটি চালান:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
এই কোডটি প্রতিটি ডাইমেনশন (dimension) দিয়ে area
ফাংশনটিকে কল করে আয়তক্ষেত্রের ক্ষেত্রফল বের করতে সফল হয়েছে, কিন্তু এই কোডটিকে আরও স্পষ্ট এবং পাঠযোগ্য করার জন্য আমরা আরও অনেক কিছু করতে পারি।
এই কোডের সমস্যাটি area
ফাংশনের সিগনেচারে স্পষ্ট:
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
area
ফাংশনটির কাজ একটি আয়তক্ষেত্রের ক্ষেত্রফল গণনা করা, কিন্তু আমরা যে ফাংশনটি লিখেছি তার দুটি প্যারামিটার রয়েছে, এবং আমাদের প্রোগ্রামের কোথাও এটা স্পষ্ট নয় যে এই প্যারামিটারগুলো সম্পর্কিত। প্রস্থ এবং উচ্চতাকে একসাথে গ্রুপ করা আরও পাঠযোগ্য এবং পরিচালনাযোগ্য হবে। অধ্যায় ৩ এর "টাপল টাইপ" বিভাগে আমরা এটি করার একটি উপায় নিয়ে ইতিমধ্যে আলোচনা করেছি: টাপল (tuples) ব্যবহার করে।
টাপল দিয়ে রিফ্যাক্টরিং (Refactoring with Tuples)
তালিকা ৫-৯ আমাদের প্রোগ্রামের আরেকটি সংস্করণ দেখাচ্ছে যা টাপল ব্যবহার করে।
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
একদিক থেকে, এই প্রোগ্রামটি ভালো। টাপল আমাদের কিছুটা গঠন যোগ করতে দেয়, এবং আমরা এখন মাত্র একটি আর্গুমেন্ট পাস করছি। কিন্তু অন্যদিক থেকে, এই সংস্করণটি কম স্পষ্ট: টাপল তার উপাদানগুলোর নাম দেয় না, তাই আমাদের টাপলের অংশগুলোতে ইনডেক্স ব্যবহার করতে হয়, যা আমাদের গণনাকে কম স্পষ্ট করে তোলে।
ক্ষেত্রফল গণনার জন্য প্রস্থ এবং উচ্চতা গুলিয়ে ফেললে কোনো সমস্যা হবে না, কিন্তু যদি আমরা স্ক্রিনে আয়তক্ষেত্রটি আঁকতে চাই, তবে এটি গুরুত্বপূর্ণ হবে! আমাদের মনে রাখতে হবে যে width
হলো টাপল ইনডেক্স 0
এবং height
হলো টাপল ইনডেক্স 1
। অন্য কেউ যদি আমাদের কোড ব্যবহার করে, তবে তার জন্য এটি বোঝা এবং মনে রাখা আরও কঠিন হবে। যেহেতু আমরা আমাদের কোডে ডেটার অর্থ প্রকাশ করিনি, তাই এখন ভুল হওয়ার সম্ভাবনা বেড়ে গেছে।
struct
দিয়ে রিফ্যাক্টরিং: আরও অর্থ যোগ করা
আমরা ডেটাকে লেবেল দিয়ে অর্থ যোগ করার জন্য struct
ব্যবহার করি। আমরা যে টাপলটি ব্যবহার করছি সেটিকে একটি struct
-এ রূপান্তর করতে পারি, যেখানে পুরোটার জন্য একটি নাম এবং অংশগুলোর জন্যও নাম থাকবে, যেমনটি তালিকা ৫-১০-এ দেখানো হয়েছে।
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
এখানে, আমরা একটি struct
ডিফাইন করেছি এবং এর নাম দিয়েছি Rectangle
। কার্লি ব্র্যাকেটের ভিতরে, আমরা ফিল্ডগুলোকে width
এবং height
হিসাবে ডিফাইন করেছি, যার উভয়েরই টাইপ u32
। তারপর, main
ফাংশনে, আমরা Rectangle
-এর একটি নির্দিষ্ট ইনস্ট্যান্স তৈরি করেছি যার প্রস্থ 30
এবং উচ্চতা 50
।
আমাদের area
ফাংশনটি এখন একটি প্যারামিটার দিয়ে ডিফাইন করা হয়েছে, যার নাম আমরা দিয়েছি rectangle
, এবং এর টাইপ হলো একটি Rectangle
struct
ইনস্ট্যান্সের একটি অপরিবর্তনীয় ধার (immutable borrow)। অধ্যায় ৪-এ যেমন উল্লেখ করা হয়েছে, আমরা struct
-টির মালিকানা নেওয়ার পরিবর্তে এটি ধার করতে চাই। এইভাবে, main
তার মালিকানা ধরে রাখে এবং rect1
ব্যবহার করা চালিয়ে যেতে পারে, যে কারণে আমরা ফাংশন সিগনেচারে এবং যেখানে ফাংশনটি কল করি সেখানে &
ব্যবহার করি।
area
ফাংশনটি Rectangle
ইনস্ট্যান্সের width
এবং height
ফিল্ডগুলো অ্যাক্সেস করে (উল্লেখ্য যে একটি ধার করা struct
ইনস্ট্যান্সের ফিল্ড অ্যাক্সেস করলে ফিল্ডের মানগুলো মুভ (move) হয় না, যে কারণে আপনি প্রায়শই struct
-এর ধার দেখতে পাবেন)। আমাদের area
-এর ফাংশন সিগনেচার এখন ঠিক তাই বলছে যা আমরা বলতে চাই: Rectangle
-এর ক্ষেত্রফল গণনা করুন, এর width
এবং height
ফিল্ড ব্যবহার করে। এটি প্রকাশ করে যে প্রস্থ এবং উচ্চতা একে অপরের সাথে সম্পর্কিত, এবং এটি টাপল ইনডেক্স মান 0
এবং 1
ব্যবহার করার পরিবর্তে মানগুলোকে বর্ণনামূলক নাম দেয়। এটি স্পষ্টতার দিক থেকে একটি বড় সুবিধা।
ডিরাইভড ট্রেইট দিয়ে দরকারী কার্যকারিতা যোগ করা (Adding Useful Functionality with Derived Traits)
আমাদের প্রোগ্রাম ডিবাগ করার সময় Rectangle
-এর একটি ইনস্ট্যান্স প্রিন্ট করতে পারা এবং এর সমস্ত ফিল্ডের মান দেখতে পারা দরকারী হবে। তালিকা ৫-১১ পূর্ববর্তী অধ্যায়গুলোতে আমরা যেভাবে println!
ম্যাক্রো ব্যবহার করেছি, সেভাবে ব্যবহার করার চেষ্টা করে। তবে এটি কাজ করবে না।
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
যখন আমরা এই কোডটি কম্পাইল করি, আমরা এই মূল বার্তা সহ একটি এরর পাই:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println!
ম্যাক্রো অনেক ধরণের ফরম্যাটিং করতে পারে, এবং ডিফল্টভাবে, কার্লি ব্র্যাকেটগুলো println!
-কে Display
নামে পরিচিত একটি ফরম্যাটিং ব্যবহার করতে বলে: যা সরাসরি এন্ড-ইউজারের ব্যবহারের জন্য আউটপুট। আমরা এখন পর্যন্ত যে প্রিমিটিভ টাইপগুলো দেখেছি সেগুলো ডিফল্টভাবে Display
ইমপ্লিমেন্ট করে কারণ একজন ব্যবহারকারীকে 1
বা অন্য কোনো প্রিমিটিভ টাইপ দেখানোর একটিই উপায় আছে। কিন্তু struct
-এর ক্ষেত্রে, println!
আউটপুট কীভাবে ফরম্যাট করবে তা কম স্পষ্ট কারণ আরও অনেক প্রদর্শনের সম্ভাবনা রয়েছে: আপনি কি কমা চান কি না? আপনি কি কার্লি ব্র্যাকেট প্রিন্ট করতে চান? সমস্ত ফিল্ড কি দেখানো উচিত? এই অস্পষ্টতার কারণে, রাস্ট অনুমান করার চেষ্টা করে না আমরা কী চাই, এবং struct
-এর println!
এবং {}
প্লেসহোল্ডারের সাথে ব্যবহারের জন্য Display
-এর কোনো সরবরাহ করা ইমপ্লিমেন্টেশন নেই।
যদি আমরা এররগুলো পড়া চালিয়ে যাই, আমরা এই সহায়ক নোটটি খুঁজে পাব:
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead```
চলুন চেষ্টা করি! `println!` ম্যাক্রো কলটি এখন `println!("rect1 is {rect1:?}");`-এর মতো দেখাবে। কার্লি ব্র্যাকেটের ভিতরে স্পেসিফায়ার `:?` রাখলে `println!`-কে বলা হয় আমরা `Debug` নামে একটি আউটপুট ফরম্যাট ব্যবহার করতে চাই। `Debug` ট্রেইটটি আমাদের `struct`-কে এমনভাবে প্রিন্ট করতে সক্ষম করে যা ডেভেলপারদের জন্য দরকারী যাতে আমরা আমাদের কোড ডিবাগ করার সময় এর মান দেখতে পারি।
এই পরিবর্তন সহ কোডটি কম্পাইল করুন। ধুর! আমরা এখনও একটি এরর পাচ্ছি:
```text
error[E0277]: `Rectangle` doesn't implement `Debug````
কিন্তু আবারও, কম্পাইলার আমাদের একটি সহায়ক নোট দেয়:
```text
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
রাস্ট ডিবাগিং তথ্য প্রিন্ট করার জন্য কার্যকারিতা অন্তর্ভুক্ত করে, কিন্তু আমাদের struct
-এর জন্য সেই কার্যকারিতা উপলব্ধ করতে স্পষ্টভাবে অপ্ট-ইন করতে হবে। এটি করার জন্য, আমরা struct
সংজ্ঞার ঠিক আগে #[derive(Debug)]
অ্যাট্রিবিউটটি যোগ করি, যেমনটি তালিকা ৫-১২-এ দেখানো হয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
এখন যখন আমরা প্রোগ্রামটি চালাই, আমরা কোনো এরর পাব না, এবং আমরা নিম্নলিখিত আউটপুটটি দেখব:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
সুন্দর! এটি সবচেয়ে সুন্দর আউটপুট নয়, তবে এটি এই ইনস্ট্যান্সের জন্য সমস্ত ফিল্ডের মান দেখায়, যা ডিবাগিংয়ের সময় অবশ্যই সাহায্য করবে। যখন আমাদের বড় struct
থাকে, তখন এমন আউটপুট থাকা দরকারী যা পড়া একটু সহজ; সেই ক্ষেত্রে, আমরা println!
স্ট্রিং-এ {:?}
এর পরিবর্তে {:#?}
ব্যবহার করতে পারি। এই উদাহরণে, {:#?}
স্টাইল ব্যবহার করলে নিম্নলিখিত আউটপুটটি আসবে:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug
ফরম্যাট ব্যবহার করে একটি মান প্রিন্ট করার আরেকটি উপায় হলো dbg!
ম্যাক্রো ব্যবহার করা, যা একটি এক্সপ্রেশনের মালিকানা নেয় (println!
-এর বিপরীতে, যা একটি রেফারেন্স নেয়), আপনার কোডে সেই dbg!
ম্যাক্রো কলটি কোথায় ঘটেছে তার ফাইল এবং লাইন নম্বর প্রিন্ট করে এবং সেই এক্সপ্রেশনের ফলস্বরূপ মান সহ, এবং মানের মালিকানা ফেরত দেয়।
দ্রষ্টব্য:
dbg!
ম্যাক্রো কল করা স্ট্যান্ডার্ড এরর কনসোল স্ট্রিমে (stderr
) প্রিন্ট করে,println!
-এর বিপরীতে, যা স্ট্যান্ডার্ড আউটপুট কনসোল স্ট্রিমে (stdout
) প্রিন্ট করে। আমরা অধ্যায় ১২-এর "স্ট্যান্ডার্ড আউটপুটের পরিবর্তে স্ট্যান্ডার্ড এররে এরর বার্তা লেখা" বিভাগেstderr
এবংstdout
সম্পর্কে আরও কথা বলব।
এখানে একটি উদাহরণ যেখানে আমরা width
ফিল্ডে নির্ধারিত মান এবং rect1
-এর পুরো struct
-এর মানে আগ্রহী:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
আমরা 30 * scale
এক্সপ্রেশনের চারপাশে dbg!
রাখতে পারি এবং, যেহেতু dbg!
এক্সপ্রেশনের মানের মালিকানা ফেরত দেয়, width
ফিল্ডটি একই মান পাবে যেন আমাদের সেখানে dbg!
কল ছিল না। আমরা চাই না dbg!
rect1
-এর মালিকানা নিক, তাই আমরা পরবর্তী কলে rect1
-এর একটি রেফারেন্স ব্যবহার করি। এই উদাহরণের আউটপুটটি দেখতে এইরকম:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
আমরা দেখতে পাচ্ছি প্রথম আউটপুটটি src/main.rs লাইন ১০ থেকে এসেছে যেখানে আমরা 30 * scale
এক্সপ্রেশনটি ডিবাগ করছি, এবং এর ফলস্বরূপ মান 60
(পূর্ণসংখ্যার জন্য ইমপ্লিমেন্ট করা Debug
ফরম্যাটিং হলো শুধুমাত্র তাদের মান প্রিন্ট করা)। src/main.rs এর ১৪ নং লাইনের dbg!
কলটি &rect1
-এর মান আউটপুট করে, যা Rectangle
struct
। এই আউটপুটটি Rectangle
টাইপের সুন্দর Debug
ফরম্যাটিং ব্যবহার করে। dbg!
ম্যাক্রোটি খুব সহায়ক হতে পারে যখন আপনি আপনার কোড কী করছে তা বোঝার চেষ্টা করছেন!
Debug
ট্রেইট ছাড়াও, রাস্ট আমাদের derive
অ্যাট্রিবিউট দিয়ে ব্যবহারের জন্য বেশ কয়েকটি ট্রেইট সরবরাহ করেছে যা আমাদের কাস্টম টাইপগুলোতে দরকারী আচরণ যোগ করতে পারে। সেই ট্রেইট এবং তাদের আচরণগুলো পরিশিষ্ট C-তে তালিকাভুক্ত করা হয়েছে। আমরা কাস্টম আচরণ সহ এই ট্রেইটগুলো কীভাবে ইমপ্লিমেন্ট করতে হয় এবং কীভাবে আপনার নিজস্ব ট্রেইট তৈরি করতে হয় তা অধ্যায় ১০-এ আলোচনা করব। derive
ছাড়াও আরও অনেক অ্যাট্রিবিউট রয়েছে; আরও তথ্যের জন্য, রাস্ট রেফারেন্সের "অ্যাট্রিবিউটস" বিভাগটি দেখুন।
আমাদের area
ফাংশনটি খুব নির্দিষ্ট: এটি শুধুমাত্র আয়তক্ষেত্রের ক্ষেত্রফল গণনা করে। এই আচরণটিকে আমাদের Rectangle
struct
-এর সাথে আরও ঘনিষ্ঠভাবে যুক্ত করা সহায়ক হবে কারণ এটি অন্য কোনো টাইপের সাথে কাজ করবে না। আসুন দেখি কীভাবে আমরা area
ফাংশনটিকে আমাদের Rectangle
টাইপে ডিফাইন করা একটি area
মেথডে পরিণত করে এই কোডটিকে রিফ্যাক্টর করা চালিয়ে যেতে পারি।
মেথড সিনট্যাক্স (Method Syntax)
মেথড (Methods) অনেকটা ফাংশনের মতোই: আমরা fn
কীওয়ার্ড এবং একটি নাম দিয়ে এগুলো ডিক্লেয়ার করি, এগুলোর প্যারামিটার এবং একটি রিটার্ন ভ্যালু থাকতে পারে, এবং এগুলোর মধ্যে কিছু কোড থাকে যা অন্য কোথাও থেকে মেথডটি কল করা হলে রান হয়। ফাংশনের সাথে এর পার্থক্য হলো, মেথডগুলো একটি struct
(অথবা একটি enum
বা একটি trait
অবজেক্ট, যা আমরা যথাক্রমে অধ্যায় ৬ এবং অধ্যায় ১৮-এ আলোচনা করব) এর কনটেক্সটে ডিফাইন করা হয়, এবং তাদের প্রথম প্যারামিটার সবসময় self
হয়, যা struct
-এর সেই ইনস্ট্যান্সটিকে প্রতিনিধিত্ব করে যার উপর মেথডটি কল করা হচ্ছে।
মেথড ডিফাইন করা (Defining Methods)
আসুন area
ফাংশনটিকে পরিবর্তন করি, যেটি একটি Rectangle
ইনস্ট্যান্সকে প্যারামিটার হিসেবে নেয়, এবং এর পরিবর্তে Rectangle
struct
-এর উপর ডিফাইন করা একটি area
মেথড তৈরি করি, যেমনটি তালিকা ৫-১৩-এ দেখানো হয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
Rectangle
-এর কনটেক্সটে ফাংশনটি ডিফাইন করার জন্য, আমরা Rectangle
-এর জন্য একটি impl
(implementation) ব্লক শুরু করি। এই impl
ব্লকের মধ্যে থাকা সবকিছু Rectangle
টাইপের সাথে যুক্ত থাকবে। তারপর আমরা area
ফাংশনটিকে impl
কার্লি ব্র্যাকেটের মধ্যে নিয়ে যাই এবং সিগনেচারে এবং বডির সর্বত্র প্রথম (এবং এই ক্ষেত্রে, একমাত্র) প্যারামিটারটিকে self
এ পরিবর্তন করি। main
ফাংশনে, যেখানে আমরা area
ফাংশন কল করেছিলাম এবং rect1
কে আর্গুমেন্ট হিসাবে পাস করেছিলাম, তার পরিবর্তে আমরা আমাদের Rectangle
ইনস্ট্যান্সের উপর area
মেথড কল করার জন্য মেথড সিনট্যাক্স (method syntax) ব্যবহার করতে পারি। মেথড সিনট্যাক্স একটি ইনস্ট্যান্সের পরে বসে: আমরা একটি ডট এবং তারপরে মেথডের নাম, প্যারেন্থেসিস এবং যেকোনো আর্গুমেন্ট যোগ করি।
area
-এর সিগনেচারে, আমরা rectangle: &Rectangle
এর পরিবর্তে &self
ব্যবহার করি। &self
আসলে self: &Self
-এর সংক্ষিপ্ত রূপ। একটি impl
ব্লকের মধ্যে, Self
টাইপটি সেই টাইপের একটি অ্যালিয়াস (alias) যার জন্য impl
ব্লকটি তৈরি করা হয়েছে। মেথডগুলোর প্রথম প্যারামিটার হিসাবে self
নামের একটি Self
টাইপের প্যারামিটার থাকতে হয়, তাই রাস্ট আপনাকে প্রথম প্যারামিটারের স্থানে শুধুমাত্র self
নামটি দিয়ে এটিকে সংক্ষিপ্ত করার অনুমতি দেয়। মনে রাখবেন যে আমাদের এখনও self
শর্টহ্যান্ডের আগে &
ব্যবহার করতে হবে এটি বোঝাতে যে এই মেথডটি Self
ইনস্ট্যান্সটিকে ধার (borrow) করে, ঠিক যেমনটি আমরা rectangle: &Rectangle
-এ করেছিলাম। মেথডগুলো self
-এর মালিকানা নিতে পারে, self
-কে অপরিবর্তনীয়ভাবে ধার করতে পারে, যেমনটি আমরা এখানে করেছি, অথবা self
-কে পরিবর্তনীয়ভাবে ধার করতে পারে, ঠিক যেমনটি তারা অন্য যেকোনো প্যারামিটারের ক্ষেত্রে পারে।
আমরা এখানে &self
বেছে নিয়েছি একই কারণে যে কারণে আমরা ফাংশন সংস্করণে &Rectangle
ব্যবহার করেছিলাম: আমরা মালিকানা নিতে চাই না, এবং আমরা কেবল struct
-এর ডেটা পড়তে চাই, এতে লিখতে চাই না। যদি আমরা যে ইনস্ট্যান্সের উপর মেথডটি কল করেছি সেটিকে মেথডের কাজের অংশ হিসাবে পরিবর্তন করতে চাইতাম, তাহলে আমরা প্রথম প্যারামিটার হিসাবে &mut self
ব্যবহার করতাম। self
-কে প্রথম প্যারামিটার হিসাবে ব্যবহার করে ইনস্ট্যান্সের মালিকানা নেওয়া একটি মেথড বিরল; এই কৌশলটি সাধারণত ব্যবহৃত হয় যখন মেথডটি self
-কে অন্য কিছুতে রূপান্তরিত করে এবং আপনি চান যে রূপান্তরের পরে কলার মূল ইনস্ট্যান্সটি ব্যবহার করা থেকে বিরত থাকুক।
মেথড সিনট্যাক্স সরবরাহ করা এবং প্রতিটি মেথডের সিগনেচারে self
-এর টাইপ পুনরাবৃত্তি না করার পাশাপাশি, ফাংশনের পরিবর্তে মেথড ব্যবহারের মূল কারণ হলো অর্গানাইজেশন বা সংগঠন। আমরা একটি টাইপের ইনস্ট্যান্সের সাথে যা যা করা যায় তার সবকিছু একটি impl
ব্লকের মধ্যে রেখেছি, যাতে আমাদের কোডের ভবিষ্যতের ব্যবহারকারীদেরকে আমাদের সরবরাহ করা লাইব্রেরির বিভিন্ন জায়গায় Rectangle
-এর ক্ষমতা খুঁজতে না হয়।
মনে রাখবেন যে আমরা একটি মেথডকে struct
-এর একটি ফিল্ডের সমান নাম দিতে পারি। উদাহরণস্বরূপ, আমরা Rectangle
-এর উপর width
নামে একটি মেথড ডিফাইন করতে পারি:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
এখানে, আমরা width
মেথডটিকে true
রিটার্ন করার জন্য তৈরি করছি যদি ইনস্ট্যান্সের width
ফিল্ডের মান 0
-এর চেয়ে বড় হয় এবং false
যদি মান 0
হয়: আমরা একই নামের একটি মেথডের মধ্যে একটি ফিল্ড যেকোনো উদ্দেশ্যে ব্যবহার করতে পারি। main
-এ, যখন আমরা rect1.width
-এর পরে প্যারেন্থেসিস ব্যবহার করি, রাস্ট জানে যে আমরা width
মেথডটির কথা বলছি। যখন আমরা প্যারেন্থেসিস ব্যবহার করি না, রাস্ট জানে যে আমরা width
ফিল্ডটির কথা বলছি।
প্রায়শই, কিন্তু সবসময় নয়, যখন আমরা একটি মেথডকে একটি ফিল্ডের সমান নাম দিই তখন আমরা চাই এটি শুধুমাত্র ফিল্ডের মান রিটার্ন করুক এবং অন্য কিছু না করুক। এই ধরনের মেথডকে গেটার (getters) বলা হয়, এবং রাস্ট অন্য কিছু ভাষার মতো struct
ফিল্ডের জন্য এগুলো স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করে না। গেটারগুলো দরকারী কারণ আপনি ফিল্ডটিকে প্রাইভেট কিন্তু মেথডটিকে পাবলিক করতে পারেন, এবং এইভাবে টাইপের পাবলিক API-এর অংশ হিসাবে সেই ফিল্ডে শুধুমাত্র-পড়ার (read-only) অ্যাক্সেস সক্ষম করতে পারেন। আমরা পাবলিক এবং প্রাইভেট কী এবং কীভাবে একটি ফিল্ড বা মেথডকে পাবলিক বা প্রাইভেট হিসাবে চিহ্নিত করতে হয় তা অধ্যায় ৭-এ আলোচনা করব।
->
অপারেটরটি কোথায়?C এবং C++ এ, মেথড কল করার জন্য দুটি ভিন্ন অপারেটর ব্যবহৃত হয়: আপনি
.
ব্যবহার করেন যদি আপনি সরাসরি অবজেক্টের উপর একটি মেথড কল করেন এবং->
ব্যবহার করেন যদি আপনি অবজেক্টের একটি পয়েন্টারের উপর মেথড কল করেন এবং প্রথমে পয়েন্টারটিকে ডি-রেফারেন্স করতে হয়। অন্য কথায়, যদিobject
একটি পয়েন্টার হয়,object->something()
অনেকটা(*object).something()
-এর মতো।রাস্টের
->
অপারেটরের সমতুল্য কিছু নেই; এর পরিবর্তে, রাস্টের একটি বৈশিষ্ট্য রয়েছে যার নাম স্বয়ংক্রিয় রেফারেন্সিং এবং ডি-রেফারেন্সিং (automatic referencing and dereferencing)। মেথড কল করা রাস্টের কয়েকটি জায়গার মধ্যে একটি যেখানে এই আচরণ রয়েছে।এটি যেভাবে কাজ করে: যখন আপনি
object.something()
দিয়ে একটি মেথড কল করেন, রাস্ট স্বয়ংক্রিয়ভাবে&
,&mut
, বা*
যোগ করে দেয় যাতেobject
মেথডের সিগনেচারের সাথে মেলে। অন্য কথায়, নিম্নলিখিতগুলো একই:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }
প্রথমটি দেখতে অনেক পরিষ্কার। এই স্বয়ংক্রিয় রেফারেন্সিং আচরণটি কাজ করে কারণ মেথডগুলোর একটি স্পষ্ট রিসিভার (receiver) আছে—
self
-এর টাইপ। রিসিভার এবং একটি মেথডের নাম দেওয়া হলে, রাস্ট নির্দিষ্টভাবে বের করতে পারে যে মেথডটি পড়ছে (&self
), পরিবর্তন করছে (&mut self
), বা ব্যবহার করে ফেলছে (self
)। রাস্ট যে মেথড রিসিভারের জন্য ধার করাকে উহ্য (implicit) করে তোলে তা বাস্তবে মালিকানাকে অর্গোনমিক (ergonomic) করার একটি বড় অংশ।
একাধিক প্যারামিটার সহ মেথড (Methods with More Parameters)
আসুন Rectangle
struct
-এর উপর একটি দ্বিতীয় মেথড ইমপ্লিমেন্ট করে মেথড ব্যবহার করার অনুশীলন করি। এবার আমরা চাই Rectangle
-এর একটি ইনস্ট্যান্স Rectangle
-এর আরেকটি ইনস্ট্যান্স নিক এবং true
রিটার্ন করুক যদি দ্বিতীয় Rectangle
-টি self
-এর (প্রথম Rectangle
) মধ্যে সম্পূর্ণরূপে ফিট করতে পারে; অন্যথায়, এটি false
রিটার্ন করবে। অর্থাৎ, একবার আমরা can_hold
মেথডটি ডিফাইন করে ফেলার পর, আমরা তালিকা ৫-১৪-এ দেখানো প্রোগ্রামটি লিখতে চাই।
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
প্রত্যাশিত আউটপুট নিম্নলিখিতটির মতো হবে কারণ rect2
-এর উভয় ডাইমেনশন rect1
-এর ডাইমেনশনের চেয়ে ছোট, কিন্তু rect3
rect1
-এর চেয়ে চওড়া:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
আমরা জানি আমরা একটি মেথড ডিফাইন করতে চাই, তাই এটি impl Rectangle
ব্লকের মধ্যে থাকবে। মেথডের নাম হবে can_hold
, এবং এটি অন্য একটি Rectangle
-এর একটি অপরিবর্তনীয় ধার (immutable borrow) প্যারামিটার হিসাবে নেবে। আমরা প্যারামিটারের টাইপ কী হবে তা মেথড কল করা কোডটি দেখে বলতে পারি: rect1.can_hold(&rect2)
&rect2
পাস করে, যা rect2
, একটি Rectangle
ইনস্ট্যান্সের একটি অপরিবর্তনীয় ধার। এটি যৌক্তিক কারণ আমাদের কেবল rect2
পড়তে হবে (লেখার পরিবর্তে, যার জন্য আমাদের একটি পরিবর্তনযোগ্য ধার প্রয়োজন হতো), এবং আমরা চাই main
rect2
-এর মালিকানা ধরে রাখুক যাতে আমরা can_hold
মেথড কল করার পরেও এটি আবার ব্যবহার করতে পারি। can_hold
-এর রিটার্ন ভ্যালু একটি বুলিয়ান হবে, এবং ইমপ্লিমেন্টেশনটি পরীক্ষা করবে যে self
-এর প্রস্থ এবং উচ্চতা অন্য Rectangle
-এর প্রস্থ এবং উচ্চতার চেয়ে বড় কিনা। আসুন নতুন can_hold
মেথডটি তালিকা ৫-১৩ থেকে impl
ব্লকে যোগ করি, যা তালিকা ৫-১৫-এ দেখানো হয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
যখন আমরা এই কোডটি তালিকা ৫-১৪-এর main
ফাংশন দিয়ে চালাই, আমরা আমাদের কাঙ্ক্ষিত আউটপুট পাব। মেথডগুলো self
প্যারামিটারের পরে একাধিক প্যারামিটার নিতে পারে যা আমরা সিগনেচারে যোগ করি, এবং সেই প্যারামিটারগুলো ফাংশনের প্যারামিটারের মতোই কাজ করে।
অ্যাসোসিয়েটেড ফাংশন (Associated Functions)
একটি impl
ব্লকের মধ্যে ডিফাইন করা সমস্ত ফাংশনকে অ্যাসোসিয়েটেড ফাংশন (associated functions) বলা হয় কারণ সেগুলো impl
-এর পরে নাম দেওয়া টাইপের সাথে যুক্ত। আমরা এমন অ্যাসোসিয়েটেড ফাংশন ডিফাইন করতে পারি যেগুলোর প্রথম প্যারামিটার self
নয় (এবং এইভাবে সেগুলো মেথড নয়) কারণ সেগুলোর সাথে কাজ করার জন্য টাইপের একটি ইনস্ট্যান্সের প্রয়োজন হয় না। আমরা ইতিমধ্যে এই ধরনের একটি ফাংশন ব্যবহার করেছি: String::from
ফাংশন যা String
টাইপের উপর ডিফাইন করা আছে।
যেসব অ্যাসোসিয়েটেড ফাংশন মেথড নয় সেগুলো প্রায়শই কনস্ট্রাক্টর (constructors) হিসাবে ব্যবহৃত হয় যা struct
-এর একটি নতুন ইনস্ট্যান্স রিটার্ন করবে। এগুলোকে প্রায়শই new
বলা হয়, কিন্তু new
কোনো বিশেষ নাম নয় এবং এটি ভাষায় বিল্ট-ইন নয়। উদাহরণস্বরূপ, আমরা square
নামে একটি অ্যাসোসিয়েটেড ফাংশন সরবরাহ করতে পারি যা একটি ডাইমেনশন প্যারামিটার নেবে এবং সেটিকে প্রস্থ এবং উচ্চতা উভয় হিসাবে ব্যবহার করবে, এইভাবে একটি বর্গক্ষেত্র Rectangle
তৈরি করা সহজ করে তুলবে, একই মান দুবার নির্দিষ্ট করার পরিবর্তে:
Filename: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }
রিটার্ন টাইপ এবং ফাংশনের বডিতে Self
কীওয়ার্ডগুলো সেই টাইপের অ্যালিয়াস যা impl
কীওয়ার্ডের পরে আসে, যা এই ক্ষেত্রে Rectangle
।
এই অ্যাসোসিয়েটেড ফাংশনটি কল করার জন্য, আমরা struct
-এর নামের সাথে ::
সিনট্যাক্স ব্যবহার করি; let sq = Rectangle::square(3);
একটি উদাহরণ। এই ফাংশনটি struct
দ্বারা নেমস্পেসড (namespaced) হয়: ::
সিনট্যাক্সটি অ্যাসোসিয়েটেড ফাংশন এবং মডিউল দ্বারা তৈরি নেমস্পেস উভয়ের জন্যই ব্যবহৃত হয়। আমরা অধ্যায় ৭-এ মডিউল নিয়ে আলোচনা করব।
একাধিক impl
ব্লক (Multiple impl
Blocks)
প্রতিটি struct
-এর একাধিক impl
ব্লক থাকতে পারে। উদাহরণস্বরূপ, তালিকা ৫-১৫ তালিকা ৫-১৬-এ দেখানো কোডের সমতুল্য, যেখানে প্রতিটি মেথড তার নিজস্ব impl
ব্লকে রয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
এখানে এই মেথডগুলোকে একাধিক impl
ব্লকে বিভক্ত করার কোনো কারণ নেই, তবে এটি একটি বৈধ সিনট্যাক্স। আমরা অধ্যায় ১০-এ একটি কেস দেখব যেখানে একাধিক impl
ব্লক দরকারী, যেখানে আমরা জেনেরিক টাইপ এবং ট্রেইট নিয়ে আলোচনা করব।
সারসংক্ষেপ (Summary)
struct
আপনাকে আপনার ডোমেইনের জন্য অর্থপূর্ণ কাস্টম টাইপ তৈরি করতে দেয়। struct
ব্যবহার করে, আপনি সম্পর্কিত ডেটার অংশগুলোকে একে অপরের সাথে সংযুক্ত রাখতে পারেন এবং আপনার কোডকে স্পষ্ট করার জন্য প্রতিটি অংশের নাম দিতে পারেন। impl
ব্লকে, আপনি আপনার টাইপের সাথে যুক্ত ফাংশন ডিফাইন করতে পারেন, এবং মেথডগুলো এক ধরনের অ্যাসোসিয়েটেড ফাংশন যা আপনাকে আপনার struct
-এর ইনস্ট্যান্সগুলোর আচরণ নির্দিষ্ট করতে দেয়।
কিন্তু struct
আপনার কাস্টম টাইপ তৈরি করার একমাত্র উপায় নয়: আসুন রাস্টের enum
বৈশিষ্ট্যটির দিকে নজর দিই এবং আপনার টুলবক্সে আরেকটি টুল যোগ করি।
Enum এবং Pattern Matching
এই চ্যাপ্টারে আমরা enumerations, যা enums নামেও পরিচিত, তা নিয়ে আলোচনা করব। Enum ব্যবহার করে আপনি একটি type-কে তার সম্ভাব্য variants গণনা করার মাধ্যমে ডিফাইন করতে পারেন। প্রথমে, আমরা একটি enum ডিফাইন এবং ব্যবহার করে দেখব কিভাবে এটি data-র সাথে অর্থও এনকোড করতে পারে। এরপর আমরা Option
নামে একটি বিশেষ প্রয়োজনীয় enum দেখব, যা প্রকাশ করে যে একটি value হয় কিছু একটা হতে পারে অথবা কিছুই না। তারপর আমরা দেখব match
এক্সপ্রেশনের মাধ্যমে pattern matching ব্যবহার করে কিভাবে একটি enum-এর বিভিন্ন value-র জন্য আলাদা আলাদা কোড চালানো সহজ হয়। সবশেষে, আমরা if let
কনস্ট্রাক্টটি নিয়ে আলোচনা করব, যা আপনার কোডে enum পরিচালনা করার জন্য একটি সুবিধাজনক এবং সংক্ষিপ্ত উপায়।
একটি Enum ডিফাইন করা
যেখানে struct
আপনাকে সম্পর্কিত field এবং data-কে একসাথে গ্রুপ করার একটি উপায় দেয়, যেমন একটি Rectangle
এর width
এবং height
, সেখানে enum
আপনাকে বলার একটি উপায় দেয় যে একটি value একটি সম্ভাব্য সেটের মধ্যে একটি। উদাহরণস্বরূপ, আমরা বলতে পারি যে Rectangle
একটি সম্ভাব্য shape-এর সেট-এর মধ্যে একটি, যার মধ্যে Circle
এবং Triangle
-ও রয়েছে। এটি করার জন্য, Rust আমাদের এই সম্ভাবনাগুলোকে একটি enum হিসাবে এনকোড করার অনুমতি দেয়।
আসুন আমরা এমন একটি পরিস্থিতি দেখি যা আমরা কোডে প্রকাশ করতে চাই এবং দেখি কেন এই ক্ষেত্রে struct-এর চেয়ে enum বেশি উপযোগী এবং উপযুক্ত। ধরুন আমাদের IP address নিয়ে কাজ করতে হবে। বর্তমানে, IP address-এর জন্য দুটি প্রধান standard ব্যবহৃত হয়: ভার্সন ফোর এবং ভার্সন সিক্স। যেহেতু আমাদের প্রোগ্রামে একটি IP address-এর জন্য এই দুটিই একমাত্র সম্ভাবনা, তাই আমরা সমস্ত সম্ভাব্য variant-গুলোকে enumerate বা গণনা করতে পারি, যেখান থেকে enumeration নামটি এসেছে।
যেকোনো IP address হয় ভার্সন ফোর বা ভার্সন সিক্স হতে পারে, কিন্তু একই সাথে উভয়ই হতে পারে না। IP address-এর এই বৈশিষ্ট্যটি enum ডেটা স্ট্রাকচারকে উপযুক্ত করে তোলে কারণ একটি enum value তার variant-গুলোর মধ্যে শুধুমাত্র একটি হতে পারে। ভার্সন ফোর এবং ভার্সন সিক্স উভয় address-ই এখনও মৌলিকভাবে IP address, তাই যখন কোডটি যেকোনো ধরনের IP address-এর জন্য প্রযোজ্য পরিস্থিতি পরিচালনা করে, তখন তাদের একই type হিসাবে গণ্য করা উচিত।
আমরা এই ধারণাটি কোডে প্রকাশ করতে পারি একটি IpAddrKind
enum ডিফাইন করে এবং একটি IP address-এর সম্ভাব্য প্রকার V4
এবং V6
-কে তালিকাভুক্ত করে। এগুলি হলো enum-এর variant:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
IpAddrKind
এখন একটি কাস্টম ডেটা type যা আমরা আমাদের কোডের অন্য কোথাও ব্যবহার করতে পারি।
Enum ভ্যালু
আমরা IpAddrKind
-এর দুটি variant-এর প্রত্যেকটির instance এভাবে তৈরি করতে পারি:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
লক্ষ্য করুন যে enum-এর variant-গুলো তার আইডেন্টিফায়ারের অধীনে namespaced থাকে এবং আমরা দুটিকে আলাদা করতে একটি ডাবল কোলন (::) ব্যবহার করি। এটি দরকারী কারণ এখন IpAddrKind::V4
এবং IpAddrKind::V6
উভয়ই একই type-এর: IpAddrKind
। এর ফলে আমরা, উদাহরণস্বরূপ, একটি function ডিফাইন করতে পারি যা যেকোনো IpAddrKind
গ্রহণ করে:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
এবং আমরা এই function-টিকে যেকোনো variant দিয়ে কল করতে পারি:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
enum ব্যবহার করার আরও সুবিধা আছে। আমাদের IP address type সম্পর্কে আরও ভাবলে দেখা যায়, এই মুহূর্তে আমাদের কাছে আসল IP address data সংরক্ষণ করার কোনো উপায় নেই; আমরা কেবল জানি এটি কোন kind বা প্রকারের। চ্যাপ্টার ৫-এ আপনি যেহেতু struct সম্পর্কে শিখেছেন, আপনি হয়তো এই সমস্যাটি struct দিয়ে সমাধান করার চেষ্টা করতে পারেন, যেমনটি লিস্টিং ৬-১ এ দেখানো হয়েছে।
fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; }
এখানে, আমরা একটি IpAddr
struct ডিফাইন করেছি যার দুটি field আছে: একটি kind
field যা IpAddrKind
type-এর (আমাদের আগে ডিফাইন করা enum) এবং একটি address
field যা String
type-এর। আমাদের এই struct-এর দুটি instance আছে। প্রথমটি হলো home
, এবং এটির kind
হিসেবে IpAddrKind::V4
এবং এর সাথে সম্পর্কিত address data হিসেবে 127.0.0.1
আছে। দ্বিতীয় instance হলো loopback
। এটির kind
হিসেবে IpAddrKind
-এর অন্য variant, V6
আছে এবং এর সাথে ::1
address সম্পর্কিত আছে। আমরা kind
এবং address
value-গুলোকে একসাথে বান্ডিল করতে একটি struct ব্যবহার করেছি, তাই এখন variant-টি value-এর সাথে সম্পর্কিত।
যাইহোক, শুধু একটি enum ব্যবহার করে একই ধারণা প্রকাশ করা আরও সংক্ষিপ্ত: একটি struct-এর ভিতরে enum না রেখে, আমরা সরাসরি প্রতিটি enum variant-এর মধ্যে data রাখতে পারি। IpAddr
enum-এর এই নতুন সংজ্ঞাটি বলছে যে V4
এবং V6
উভয় variant-এর সাথেই String
value যুক্ত থাকবে:
fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); }
আমরা সরাসরি enum-এর প্রতিটি variant-এর সাথে data সংযুক্ত করি, তাই অতিরিক্ত struct-এর কোনো প্রয়োজন নেই। এখানে, enum কিভাবে কাজ করে তার আরেকটি বিস্তারিত দিক দেখাও সহজ: আমরা যে প্রতিটি enum variant ডিফাইন করি তার নামও একটি function হয়ে যায় যা enum-এর একটি instance তৈরি করে। অর্থাৎ, IpAddr::V4()
একটি function কল যা একটি String
আর্গুমেন্ট নেয় এবং IpAddr
type-এর একটি instance রিটার্ন করে। enum ডিফাইন করার ফলে আমরা স্বয়ংক্রিয়ভাবে এই constructor function-টি পেয়ে যাই।
struct-এর চেয়ে enum ব্যবহার করার আরেকটি সুবিধা হলো: প্রতিটি variant-এর সাথে বিভিন্ন type এবং পরিমাণের data যুক্ত থাকতে পারে। ভার্সন ফোর IP address-এ সবসময় চারটি সাংখ্যিক কম্পোনেন্ট থাকবে যার মান ০ থেকে ২৫৫ এর মধ্যে হবে। যদি আমরা V4
address-কে চারটি u8
মান হিসেবে সংরক্ষণ করতে চাই কিন্তু V6
address-কে একটি String
মান হিসাবেই প্রকাশ করতে চাই, তবে আমরা একটি struct দিয়ে তা করতে পারতাম না। enum এই কাজটি সহজেই করতে পারে:
fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
আমরা ভার্সন ফোর এবং ভার্সন সিক্স IP address সংরক্ষণ করার জন্য ডেটা স্ট্রাকচার ডিফাইন করার বিভিন্ন উপায় দেখিয়েছি। তবে, দেখা যাচ্ছে যে IP address সংরক্ষণ করা এবং সেগুলি কোন প্রকারের তা এনকোড করার প্রয়োজনীয়তা এতটাই সাধারণ যে স্ট্যান্ডার্ড লাইব্রেরিতে আমাদের ব্যবহারের জন্য একটি সংজ্ঞা রয়েছে! আসুন দেখি স্ট্যান্ডার্ড লাইব্রেরি কিভাবে IpAddr
ডিফাইন করে: এটিতে আমাদের ডিফাইন করা এবং ব্যবহৃত enum এবং variant-গুলোই আছে, তবে এটি variant-গুলোর ভিতরে address data দুটি ভিন্ন struct-এর আকারে এম্বেড করে, যা প্রতিটি variant-এর জন্য ভিন্নভাবে ডিফাইন করা হয়েছে:
#![allow(unused)] fn main() { struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
এই কোডটি দেখায় যে আপনি একটি enum variant-এর ভিতরে যেকোনো ধরনের ডেটা রাখতে পারেন: উদাহরণস্বরূপ, স্ট্রিং, নিউমেরিক টাইপ, বা struct। এমনকি আপনি অন্য একটি enum-ও অন্তর্ভুক্ত করতে পারেন! এছাড়াও, স্ট্যান্ডার্ড লাইব্রেরির type-গুলো প্রায়শই আপনার নিজের ভাবনার চেয়ে খুব বেশি জটিল হয় না।
লক্ষ্য করুন যে যদিও স্ট্যান্ডার্ড লাইব্রেরিতে IpAddr
-এর একটি সংজ্ঞা রয়েছে, আমরা এখনও কোনো conflict ছাড়াই আমাদের নিজস্ব সংজ্ঞা তৈরি এবং ব্যবহার করতে পারি কারণ আমরা স্ট্যান্ডার্ড লাইব্রেরির সংজ্ঞাটি আমাদের স্কোপে নিয়ে আসিনি। আমরা চ্যাপ্টার ৭-এ স্কোপে type নিয়ে আসার বিষয়ে আরও আলোচনা করব।
আসুন লিস্টিং ৬-২-এ enum-এর আরেকটি উদাহরণ দেখি: এটিতে এর variant-গুলোর মধ্যে বিভিন্ন ধরণের type এম্বেড করা আছে।
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
এই enum-টির চারটি variant আছে এবং তাদের সাথে বিভিন্ন ধরনের ডেটা যুক্ত আছে:
Quit
: এর সাথে কোনো ডেটা যুক্ত নেই।Move
: এর মধ্যে struct-এর মতো named field আছে।Write
: একটিString
অন্তর্ভুক্ত করে।ChangeColor
: তিনটিi32
মান অন্তর্ভুক্ত করে।
লিস্টিং ৬-২-এর মতো variant সহ একটি enum ডিফাইন করা বিভিন্ন ধরণের struct ডিফাইন করার মতোই, তবে enum struct
কীওয়ার্ড ব্যবহার করে না এবং সমস্ত variant Message
type-এর অধীনে একসাথে গ্রুপ করা হয়। নিম্নলিখিত struct-গুলো পূর্ববর্তী enum variant-গুলোর মতো একই ডেটা ধারণ করতে পারত:
struct QuitMessage; // unit struct struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct fn main() {}
কিন্তু যদি আমরা বিভিন্ন struct ব্যবহার করতাম, যার প্রত্যেকটির নিজস্ব type আছে, আমরা তত সহজে একটি function ডিফাইন করতে পারতাম না যা এই ধরনের যেকোনো message নিতে পারে, যেমনটা আমরা লিস্টিং ৬-২-এ ডিফাইন করা Message
enum দিয়ে করতে পারি, যা একটি একক type।
enum এবং struct-এর মধ্যে আরও একটি মিল আছে: ঠিক যেমন আমরা impl
ব্যবহার করে struct-এর উপর মেথড ডিফাইন করতে পারি, তেমনি আমরা enum-এর উপরও মেথড ডিফাইন করতে পারি। এখানে call
নামে একটি মেথড রয়েছে যা আমরা আমাদের Message
enum-এ ডিফাইন করতে পারি:
fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call(); }
মেথডটির বডি self
ব্যবহার করে সেই মানটি পাবে যার উপর আমরা মেথডটি কল করেছি। এই উদাহরণে, আমরা একটি ভেরিয়েবল m
তৈরি করেছি যার মান Message::Write(String::from("hello"))
, এবং যখন m.call()
চালানো হবে, তখন call
মেথডের বডিতে self
এটিই হবে।
আসুন স্ট্যান্ডার্ড লাইব্রেরির আরেকটি enum দেখি যা খুব সাধারণ এবং দরকারী: Option
।
Option
Enum এবং Null ভ্যালুর উপর এর সুবিধা
এই বিভাগটি Option
-এর একটি কেস স্টাডি নিয়ে আলোচনা করে, যা স্ট্যান্ডার্ড লাইব্রেরি দ্বারা ডিফাইন করা আরেকটি enum। Option
type-টি একটি খুব সাধারণ পরিস্থিতি এনকোড করে যেখানে একটি মান কিছু একটা হতে পারে বা কিছুই নাও হতে পারে।
উদাহরণস্বরূপ, যদি আপনি একটি খালি নয় এমন লিস্ট থেকে প্রথম আইটেমটি অনুরোধ করেন, আপনি একটি মান পাবেন। যদি আপনি একটি খালি লিস্ট থেকে প্রথম আইটেমটি অনুরোধ করেন, আপনি কিছুই পাবেন না। এই ধারণাটিকে type সিস্টেমের ভাষায় প্রকাশ করার অর্থ হলো compiler পরীক্ষা করতে পারে যে আপনি সমস্ত প্রয়োজনীয় কেসগুলি হ্যান্ডেল করেছেন কিনা; এই কার্যকারিতাটি অন্যান্য প্রোগ্রামিং ভাষায় অত্যন্ত সাধারণ বাগ প্রতিরোধ করতে পারে।
প্রোগ্রামিং ভাষার ডিজাইন প্রায়শই কোন বৈশিষ্ট্যগুলি অন্তর্ভুক্ত করা হয় তার উপর ভিত্তি করে ভাবা হয়, তবে কোন বৈশিষ্ট্যগুলি বাদ দেওয়া হয় তাও গুরুত্বপূর্ণ। Rust-এ null
বৈশিষ্ট্যটি নেই যা অনেক অন্যান্য ভাষায় রয়েছে। Null হলো এমন একটি মান যার অর্থ সেখানে কোনো মান নেই। যেসব ভাষায় null
আছে, সেখানে ভেরিয়েবল সবসময় দুটি অবস্থার একটিতে থাকতে পারে: null
অথবা not-null
।
তার ২০০৯ সালের উপস্থাপনা "Null References: The Billion Dollar Mistake"-এ, null
-এর উদ্ভাবক টনি হোর একথা বলেছিলেন:
আমি এটাকে আমার বিলিয়ন-ডলারের ভুল বলি। সেই সময়ে, আমি একটি অবজেক্ট-ওরিয়েন্টেড ভাষার জন্য রেফারেন্সের প্রথম ব্যাপক টাইপ সিস্টেম ডিজাইন করছিলাম। আমার লক্ষ্য ছিল নিশ্চিত করা যে রেফারেন্সের সমস্ত ব্যবহার একেবারে নিরাপদ হবে, এবং compiler দ্বারা স্বয়ংক্রিয়ভাবে পরীক্ষা করা হবে। কিন্তু আমি একটি null রেফারেন্স রাখার লোভ সামলাতে পারিনি, কারণ এটি বাস্তবায়ন করা খুব সহজ ছিল। এটি অগণিত ভুল, দুর্বলতা এবং সিস্টেম ক্র্যাশের কারণ হয়েছে, যা সম্ভবত গত চল্লিশ বছরে এক বিলিয়ন ডলারের কষ্ট ও ক্ষতির কারণ হয়েছে।
null
মানের সমস্যা হলো যে যদি আপনি একটি null
মানকে not-null
মান হিসাবে ব্যবহার করার চেষ্টা করেন, তবে আপনি কোনো না কোনো ধরনের একটি error পাবেন। যেহেতু এই null
বা not-null
বৈশিষ্ট্যটি সর্বব্যাপী, তাই এই ধরনের ভুল করা অত্যন্ত সহজ।
তবে, null
যে ধারণাটি প্রকাশ করার চেষ্টা করে তা এখনও একটি দরকারী ধারণা: একটি null
হলো এমন একটি মান যা বর্তমানে কোনো কারণে অবৈধ বা অনুপস্থিত।
সমস্যাটি আসলে ধারণার সাথে নয়, বরং নির্দিষ্ট বাস্তবায়নের সাথে। তাই, Rust-এ null
নেই, তবে এটিতে একটি enum রয়েছে যা একটি মানের উপস্থিতি বা অনুপস্থিতির ধারণাটি এনকোড করতে পারে। এই enum-টি হলো Option<T>
, এবং এটি স্ট্যান্ডার্ড লাইব্রেরি দ্বারা ডিফাইন করা হয়েছে এভাবে:
#![allow(unused)] fn main() { enum Option<T> { None, Some(T), } }
Option<T>
enum-টি এতটাই দরকারী যে এটি প্রিলিউডেও অন্তর্ভুক্ত করা হয়েছে; আপনাকে এটিকে স্পষ্টভাবে স্কোপে আনতে হবে না। এর variant-গুলোও প্রিলিউডে অন্তর্ভুক্ত: আপনি Option::
উপসর্গ ছাড়াই সরাসরি Some
এবং None
ব্যবহার করতে পারেন। Option<T>
enum-টি এখনও একটি সাধারণ enum, এবং Some(T)
ও None
এখনও Option<T>
type-এর variant।
<T>
সিনট্যাক্সটি Rust-এর একটি বৈশিষ্ট্য যা নিয়ে আমরা এখনও কথা বলিনি। এটি একটি জেনেরিক টাইপ প্যারামিটার, এবং আমরা চ্যাপ্টার ১০-এ জেনেরিক সম্পর্কে আরও বিস্তারিত আলোচনা করব। আপাতত, আপনাকে শুধু জানতে হবে যে <T>
মানে Option
enum-এর Some
variant যেকোনো type-এর ডেটার একটি অংশ ধারণ করতে পারে, এবং T
-এর পরিবর্তে ব্যবহৃত প্রতিটি সুনির্দিষ্ট type সামগ্রিক Option<T>
type-কে একটি ভিন্ন type-এ পরিণত করে। এখানে সংখ্যা এবং ক্যারেক্টার type ধারণ করার জন্য Option
মান ব্যবহার করার কিছু উদাহরণ দেওয়া হলো:
fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; }
some_number
-এর type হলো Option<i32>
। some_char
-এর type হলো Option<char>
, যা একটি ভিন্ন type। Rust এই type-গুলো অনুমান করতে পারে কারণ আমরা Some
variant-এর ভিতরে একটি মান নির্দিষ্ট করেছি। absent_number
-এর জন্য, Rust আমাদের সামগ্রিক Option
type-টি annotate করতে বলে: compiler শুধুমাত্র একটি None
মান দেখে সংশ্লিষ্ট Some
variant কোন type ধারণ করবে তা অনুমান করতে পারে না। এখানে, আমরা Rust-কে বলি যে আমরা absent_number
-কে Option<i32>
type-এর হিসাবে বোঝাতে চাই।
যখন আমাদের কাছে একটি Some
মান থাকে, আমরা জানি যে একটি মান উপস্থিত আছে এবং মানটি Some
-এর ভিতরে রয়েছে। যখন আমাদের কাছে একটি None
মান থাকে, তখন এটি某种 অর্থে null
-এর মতোই: আমাদের কাছে একটি বৈধ মান নেই। তাহলে Option<T>
থাকাটা null
থাকার চেয়ে ভালো কেন?
সংক্ষেপে, কারণ Option<T>
এবং T
(যেখানে T
যেকোনো type হতে পারে) ভিন্ন type, তাই compiler আমাদের একটি Option<T>
মানকে এমনভাবে ব্যবহার করতে দেবে না যেন এটি অবশ্যই একটি বৈধ মান। উদাহরণস্বরূপ, এই কোডটি কম্পাইল হবে না, কারণ এটি একটি i8
-কে একটি Option<i8>
-এর সাথে যোগ করার চেষ্টা করছে:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
যদি আমরা এই কোডটি চালাই, আমরা এই ধরনের একটি error বার্তা পাই:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&i8` implements `Add<i8>`
`&i8` implements `Add`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
ব্যাপক! কার্যতঃ, এই error বার্তাটির অর্থ হলো Rust বুঝতে পারছে না কিভাবে একটি i8
এবং একটি Option<i8>
যোগ করতে হয়, কারণ তারা ভিন্ন type। যখন আমাদের কাছে Rust-এ i8
-এর মতো কোনো type-এর মান থাকে, তখন compiler নিশ্চিত করবে যে আমাদের কাছে সবসময় একটি বৈধ মান আছে। আমরা সেই মানটি ব্যবহার করার আগে null
-এর জন্য পরীক্ষা না করেই আত্মবিশ্বাসের সাথে এগিয়ে যেতে পারি। শুধুমাত্র যখন আমাদের কাছে একটি Option<i8>
থাকে (বা আমরা যে ধরনের মান নিয়ে কাজ করছি), তখনই আমাদের একটি মান না থাকার সম্ভাবনা নিয়ে চিন্তা করতে হয়, এবং compiler নিশ্চিত করবে যে আমরা মানটি ব্যবহার করার আগে সেই পরিস্থিতিটি হ্যান্ডেল করি।
অন্য কথায়, T
অপারেশন সম্পাদন করার আগে আপনাকে একটি Option<T>
-কে T
-তে রূপান্তর করতে হবে। সাধারণত, এটি null
-এর সবচেয়ে সাধারণ সমস্যাগুলির মধ্যে একটি ধরতে সাহায্য করে: কোনো কিছুকে not-null
ধরে নেওয়া যখন এটি আসলে null
।
একটি not-null
মান ভুলভাবে ধরে নেওয়ার ঝুঁকি দূর করা আপনাকে আপনার কোডের প্রতি আরও আত্মবিশ্বাসী হতে সাহায্য করে। এমন একটি মান পেতে যা সম্ভবত null
হতে পারে, আপনাকে স্পষ্টভাবে সেই মানের type-কে Option<T>
করে অপ্ট-ইন করতে হবে। তারপরে, যখন আপনি সেই মানটি ব্যবহার করেন, তখন মানটি null
হলে সেই কেসটি স্পষ্টভাবে হ্যান্ডেল করতে হবে। যেখানেই একটি মানের type Option<T>
নয়, আপনি নিরাপদে ধরে নিতে পারেন যে মানটি null
নয়। এটি Rust-এর একটি ইচ্ছাকৃত ডিজাইন সিদ্ধান্ত ছিল null
-এর ব্যাপকতা সীমিত করতে এবং Rust কোডের নিরাপত্তা বৃদ্ধি করতে।
তাহলে আপনি কিভাবে Some
variant থেকে T
মানটি বের করবেন যখন আপনার কাছে Option<T>
type-এর একটি মান থাকে, যাতে আপনি সেই মানটি ব্যবহার করতে পারেন? Option<T>
enum-এর অনেক মেথড রয়েছে যা বিভিন্ন পরিস্থিতিতে দরকারী; আপনি এর ডকুমেন্টেশনে সেগুলি দেখতে পারেন। Option<T>
-এর মেথডগুলোর সাথে পরিচিত হওয়া Rust-এর সাথে আপনার যাত্রায় অত্যন্ত দরকারী হবে।
সাধারণভাবে, একটি Option<T>
মান ব্যবহার করার জন্য, আপনার এমন কোড থাকা দরকার যা প্রতিটি variant হ্যান্ডেল করবে। আপনি চান কিছু কোড শুধুমাত্র তখনই চলুক যখন আপনার কাছে একটি Some(T)
মান থাকে, এবং এই কোডটি ভিতরের T
ব্যবহার করার অনুমতি পায়। আপনি চান অন্য কিছু কোড শুধুমাত্র তখনই চলুক যদি আপনার কাছে একটি None
মান থাকে, এবং সেই কোডের কাছে একটি T
মান উপলব্ধ থাকে না। match
এক্সপ্রেশন একটি কন্ট্রোল ফ্লো কনস্ট্রাক্ট যা enum-এর সাথে ব্যবহার করা হলে ঠিক এই কাজটিই করে: এটি enum-এর কোন variant-টি পেয়েছে তার উপর নির্ভর করে ভিন্ন কোড চালাবে, এবং সেই কোডটি ম্যাচিং মানের ভিতরের ডেটা ব্যবহার করতে পারে।
match
কন্ট্রোল ফ্লো কনস্ট্রাক্ট
Rust-এ match
নামে একটি অত্যন্ত শক্তিশালী কন্ট্রোল ফ্লো কনস্ট্রাক্ট আছে যা আপনাকে একটি মানকে একাধিক প্যাটার্নের সাথে তুলনা করতে এবং কোন প্যাটার্নটি মেলে তার উপর ভিত্তি করে কোড এক্সিকিউট করতে দেয়। প্যাটার্নগুলো লিটারেল ভ্যালু, ভেরিয়েবলের নাম, ওয়াইল্ডকার্ড এবং আরও অনেক কিছু দিয়ে তৈরি হতে পারে; Chapter 19-এ বিভিন্ন ধরণের প্যাটার্ন এবং তাদের কাজ সম্পর্কে আলোচনা করা হয়েছে। match
-এর আসল শক্তি হলো এর প্যাটার্নগুলোর প্রকাশক্ষমতা (expressiveness) এবং compiler এটা নিশ্চিত করে যে সমস্ত সম্ভাব্য কেস হ্যান্ডেল করা হয়েছে।
match
এক্সপ্রেশনকে একটি কয়েন বাছাই করার মেশিনের মতো ভাবুন: কয়েনগুলো বিভিন্ন আকারের ছিদ্রযুক্ত একটি ট্র্যাকের উপর দিয়ে স্লাইড করে, এবং প্রতিটি কয়েন প্রথম যে ছিদ্রের সাথে ফিট করে সেটির মধ্যে পড়ে যায়। একইভাবে, match
-এর প্রতিটি প্যাটার্নের মধ্য দিয়ে মানগুলো যায়, এবং প্রথম যে প্যাটার্নের সাথে মানটি "ফিট" করে, সেই মানটি সংশ্লিষ্ট কোড ব্লকে পড়ে যায় এবং এক্সিকিউশনের সময় ব্যবহৃত হয়।
কয়েনের কথা যেহেতু উঠলই, চলুন match
ব্যবহার করে কয়েনকে উদাহরণ হিসাবে ব্যবহার করি! আমরা এমন একটি ফাংশন লিখতে পারি যা একটি অজানা US কয়েন নেয় এবং কাউন্টিং মেশিনের মতো নির্ধারণ করে যে এটি কোন কয়েন এবং সেন্টে এর মান ফেরত দেয়, যেমনটি লিস্টিং ৬-৩ এ দেখানো হয়েছে।
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
চলুন value_in_cents
ফাংশনের match
অংশটি ভেঙে দেখি। প্রথমে আমরা match
কীওয়ার্ডটি লিখি, যার পরে একটি এক্সপ্রেশন থাকে, এক্ষেত্রে coin
ভ্যালুটি। এটি if
-এর সাথে ব্যবহৃত কন্ডিশনাল এক্সপ্রেশনের মতোই মনে হতে পারে, কিন্তু একটি বড় পার্থক্য আছে: if
-এর ক্ষেত্রে কন্ডিশনটিকে একটি বুলিয়ান (boolean) ভ্যালু হতে হয়, কিন্তু এখানে এটি যেকোনো type-এর হতে পারে। এই উদাহরণে coin
-এর type হলো Coin
enum, যা আমরা প্রথম লাইনে ডিফাইন করেছি।
এরপর আসে match
arm। একটি arm-এর দুটি অংশ থাকে: একটি প্যাটার্ন এবং কিছু কোড। এখানের প্রথম arm-টির প্যাটার্ন হলো Coin::Penny
এবং তারপরে =>
অপারেটর যা প্যাটার্ন এবং চালানোর জন্য কোডকে আলাদা করে। এই ক্ষেত্রে কোডটি শুধু 1
ভ্যালুটি। প্রতিটি arm কমা দ্বারা পরবর্তী arm থেকে পৃথক করা হয়।
যখন match
এক্সপ্রেশনটি এক্সিকিউট হয়, এটি তার ফলাফলের মানকে প্রতিটি arm-এর প্যাটার্নের সাথে ক্রমানুসারে তুলনা করে। যদি একটি প্যাটার্ন মানের সাথে মিলে যায়, তবে সেই প্যাটার্নের সাথে যুক্ত কোডটি এক্সিকিউট হয়। যদি সেই প্যাটার্নটি মানের সাথে না মেলে, এক্সিকিউশন পরবর্তী arm-এ চলে যায়, অনেকটা কয়েন বাছাই করার মেশিনের মতো। আমাদের যতগুলো প্রয়োজন ততগুলো arm থাকতে পারে: লিস্টিং ৬-৩-এ, আমাদের match
-এর চারটি arm আছে।
প্রতিটি arm-এর সাথে যুক্ত কোড একটি এক্সপ্রেশন, এবং ম্যাচিং arm-এর এক্সপ্রেশনের ফলাফলই পুরো match
এক্সপ্রেশনের রিটার্ন ভ্যালু হিসাবে ফেরত আসে।
যখন ম্যাচ arm-এর কোড ছোট হয়, যেমন লিস্টিং ৬-৩-এ যেখানে প্রতিটি arm শুধু একটি ভ্যালু রিটার্ন করে, তখন আমরা সাধারণত কার্লি ব্র্যাকেট ব্যবহার করি না। যদি আপনি একটি ম্যাচ arm-এ একাধিক লাইন কোড চালাতে চান, তবে আপনাকে অবশ্যই কার্লি ব্র্যাকেট ব্যবহার করতে হবে, এবং সেক্ষেত্রে arm-এর পরে কমা দেওয়াটা ঐচ্ছিক। উদাহরণস্বরূপ, নিচের কোডটি যখনই একটি Coin::Penny
দিয়ে মেথডটি কল করা হয়, তখন "Lucky penny!" প্রিন্ট করে, কিন্তু ব্লকের শেষ মান, 1
, রিটার্ন করে:
enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } fn main() {}
ভ্যালুর সাথে বাইন্ড হওয়া প্যাটার্ন
match
arm-এর আরেকটি দরকারী বৈশিষ্ট্য হলো যে তারা প্যাটার্নের সাথে মিলে যাওয়া মানগুলোর অংশে বাইন্ড হতে পারে। এভাবেই আমরা enum ভ্যারিয়েন্ট থেকে মান বের করতে পারি।
উদাহরণস্বরূপ, চলুন আমাদের enum ভ্যারিয়েন্টগুলোর একটিকে পরিবর্তন করে তার ভিতরে ডেটা রাখি। ১৯৯৯ থেকে ২০০৮ সাল পর্যন্ত, মার্কিন যুক্তরাষ্ট্র ৫০টি রাজ্যের জন্য একপাশে বিভিন্ন ডিজাইন সহ কোয়ার্টার তৈরি করেছিল। অন্য কোনো কয়েনে রাজ্যের ডিজাইন ছিল না, তাই শুধুমাত্র কোয়ার্টারেই এই অতিরিক্ত মানটি রয়েছে। আমরা আমাদের enum
-এ এই তথ্য যোগ করতে পারি Quarter
ভ্যারিয়েন্টটিকে পরিবর্তন করে এর ভিতরে একটি UsState
ভ্যালু অন্তর্ভুক্ত করে, যা আমরা লিস্টিং ৬-৪-এ করেছি।
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() {}
আসুন কল্পনা করি যে একজন বন্ধু ৫০টি রাজ্যের সমস্ত কোয়ার্টার সংগ্রহ করার চেষ্টা করছে। আমরা যখন আমাদের খুচরা পয়সাগুলো কয়েনের ধরন অনুযায়ী সাজাব, তখন আমরা প্রতিটি কোয়ার্টারের সাথে যুক্ত রাজ্যের নামও ঘোষণা করব যাতে যদি আমাদের বন্ধুর সংগ্রহে সেটি না থাকে, তবে সে তার সংগ্রহে এটি যোগ করতে পারে।
এই কোডের match
এক্সপ্রেশনে, আমরা Coin::Quarter
ভ্যারিয়েন্টের মানের সাথে মিলে যাওয়া প্যাটার্নে state
নামে একটি ভেরিয়েবল যোগ করি। যখন একটি Coin::Quarter
মেলে, state
ভেরিয়েবলটি সেই কোয়ার্টারের রাজ্যের মানের সাথে বাইন্ড হবে। তারপর আমরা সেই state
ভেরিয়েবলটি সেই arm-এর কোডে ব্যবহার করতে পারি, এভাবে:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {state:?}!"); 25 } } } fn main() { value_in_cents(Coin::Quarter(UsState::Alaska)); }
যদি আমরা value_in_cents(Coin::Quarter(UsState::Alaska))
কল করি, coin
-এর মান হবে Coin::Quarter(UsState::Alaska)
। যখন আমরা সেই মানটি প্রতিটি match
arm-এর সাথে তুলনা করি, Coin::Quarter(state)
-এ না পৌঁছানো পর্যন্ত কোনোটিই মেলে না। সেই সময়ে, state
-এর জন্য বাইন্ডিং হবে UsState::Alaska
মানটি। আমরা তখন সেই বাইন্ডিংটি println!
এক্সপ্রেশনে ব্যবহার করতে পারি, যার ফলে Coin
enum-এর Quarter
ভ্যারিয়েন্ট থেকে ভিতরের রাজ্যের মানটি বের করে আনা যায়।
Option<T>
এর সাথে ম্যাচিং
আগের বিভাগে, আমরা Option<T>
ব্যবহার করার সময় Some
কেস থেকে ভিতরের T
মানটি বের করতে চেয়েছিলাম; আমরা Option<T>
-কেও match
ব্যবহার করে হ্যান্ডেল করতে পারি, যেমনটি আমরা Coin
enum-এর সাথে করেছিলাম! কয়েন তুলনা করার পরিবর্তে, আমরা Option<T>
-এর ভ্যারিয়েন্টগুলো তুলনা করব, কিন্তু match
এক্সপ্রেশনের কাজ করার পদ্ধতি একই থাকে।
ধরুন আমরা একটি ফাংশন লিখতে চাই যা একটি Option<i32>
নেয় এবং যদি ভিতরে একটি মান থাকে, তবে সেই মানের সাথে ১ যোগ করে। যদি ভিতরে কোনো মান না থাকে, ফাংশনটি None
মান ফেরত দেবে এবং কোনো অপারেশন করার চেষ্টা করবে না।
match
-এর সৌজন্যে এই ফাংশনটি লেখা খুব সহজ, এবং এটি লিস্টিং ৬-৫-এর মতো দেখাবে।
fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); }
আসুন plus_one
-এর প্রথম এক্সিকিউশনটি আরও বিস্তারিতভাবে পরীক্ষা করি। যখন আমরা plus_one(five)
কল করি, plus_one
-এর বডিতে x
ভেরিয়েবলের মান হবে Some(5)
। তারপর আমরা সেটিকে প্রতিটি match
arm-এর সাথে তুলনা করি:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5)
মানটি None
প্যাটার্নের সাথে মেলে না, তাই আমরা পরবর্তী arm-এ যাই:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5)
কি Some(i)
-এর সাথে মেলে? হ্যাঁ, মেলে! আমাদের একই ভ্যারিয়েন্ট আছে। i
ভেরিয়েবলটি Some
-এর ভিতরের মানের সাথে বাইন্ড হয়, তাই i
-এর মান 5
হয়। ম্যাচ arm-এর কোডটি তখন এক্সিকিউট হয়, তাই আমরা i
-এর মানের সাথে ১ যোগ করি এবং আমাদের মোট 6
-কে ভিতরে নিয়ে একটি নতুন Some
মান তৈরি করি।
এবার লিস্টিং ৬-৫-এ plus_one
-এর দ্বিতীয় কলটি বিবেচনা করা যাক, যেখানে x
হলো None
। আমরা match
-এ প্রবেশ করি এবং প্রথম arm-এর সাথে তুলনা করি:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
এটা মিলে গেছে! যোগ করার জন্য কোনো মান নেই, তাই প্রোগ্রামটি থেমে যায় এবং =>
-এর ডান পাশের None
মানটি রিটার্ন করে। যেহেতু প্রথম arm-টি মিলে গেছে, অন্য কোনো arm-এর সাথে আর তুলনা করা হয় না।
match
এবং enum একত্রিত করা অনেক পরিস্থিতিতে দরকারী। আপনি Rust কোডে এই প্যাটার্নটি প্রায়শই দেখবেন: একটি enum-এর উপর match
করা, ভিতরের ডেটার সাথে একটি ভেরিয়েবল বাইন্ড করা, এবং তারপর তার উপর ভিত্তি করে কোড চালানো। প্রথমে এটি কিছুটা জটিল মনে হতে পারে, কিন্তু একবার আপনি এতে অভ্যস্ত হয়ে গেলে, আপনার মনে হবে যদি সব ভাষাতেই এটি থাকত। এটি ব্যবহারকারীদের মধ্যে ধারাবাহিকভাবে একটি প্রিয় ফিচার।
ম্যাচগুলো সম্পূর্ণ (Exhaustive) হতে হয়
match
-এর আরও একটি দিক নিয়ে আমাদের আলোচনা করতে হবে: arm-এর প্যাটার্নগুলোকে অবশ্যই সমস্ত সম্ভাবনাকে কভার করতে হবে। আমাদের plus_one
ফাংশনের এই সংস্করণটি বিবেচনা করুন, যাতে একটি বাগ আছে এবং এটি কম্পাইল হবে না:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
আমরা None
কেসটি হ্যান্ডেল করিনি, তাই এই কোডটি একটি বাগ তৈরি করবে। সৌভাগ্যবশত, এটি এমন একটি বাগ যা Rust ধরতে জানে। যদি আমরা এই কোডটি কম্পাইল করার চেষ্টা করি, আমরা এই error পাব:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:572:1
::: /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/core/src/option.rs:576:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust জানে যে আমরা প্রতিটি সম্ভাব্য কেস কভার করিনি, এবং এমনকি কোন প্যাটার্নটি আমরা ভুলে গেছি সেটাও জানে! Rust-এর ম্যাচগুলো exhaustive বা সম্পূর্ণ: কোডটি বৈধ হওয়ার জন্য আমাদের অবশ্যই প্রতিটি সম্ভাবনাকে শেষ করতে হবে। বিশেষ করে Option<T>
-এর ক্ষেত্রে, যখন Rust আমাদের None
কেসটি স্পষ্টভাবে হ্যান্ডেল করতে ভুলে যাওয়া থেকে বিরত রাখে, তখন এটি আমাদের এমন একটি মান আছে বলে ধরে নেওয়া থেকে রক্ষা করে যখন আমাদের কাছে null
থাকতে পারে, যার ফলে আগে আলোচিত বিলিয়ন-ডলারের ভুলটি অসম্ভব হয়ে যায়।
ক্যাচ-অল প্যাটার্ন এবং _
প্লেসহোল্ডার
enum ব্যবহার করে, আমরা কয়েকটি নির্দিষ্ট মানের জন্য বিশেষ পদক্ষেপ নিতে পারি, কিন্তু অন্য সব মানের জন্য একটি ডিফল্ট পদক্ষেপ নিতে পারি। কল্পনা করুন আমরা একটি গেম তৈরি করছি যেখানে, যদি আপনি একটি ডাইস রোলে ৩ পান, আপনার প্লেয়ার নড়াচড়া করে না, বরং একটি নতুন সুন্দর টুপি পায়। যদি আপনি ৭ পান, আপনার প্লেয়ার একটি সুন্দর টুপি হারায়। অন্য সব মানের জন্য, আপনার প্লেয়ার গেম বোর্ডে সেই সংখ্যক ঘর সরে যায়। এখানে একটি match
রয়েছে যা সেই যুক্তিটি বাস্তবায়ন করে, যেখানে ডাইস রোলের ফলাফল একটি র্যান্ডম মানের পরিবর্তে হার্ডকোড করা হয়েছে, এবং অন্য সমস্ত যুক্তি বডি ছাড়া ফাংশন দ্বারা প্রতিনিধিত্ব করা হয়েছে কারণ সেগুলির বাস্তবায়ন এই উদাহরণের আওতার বাইরে:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {} }
প্রথম দুটি arm-এর জন্য, প্যাটার্নগুলি হলো লিটারেল মান 3
এবং 7
। শেষ arm-টির জন্য যা অন্য সব সম্ভাব্য মানকে কভার করে, প্যাটার্নটি হলো other
নামে একটি ভেরিয়েবল। other
arm-এর জন্য যে কোডটি চলে তা move_player
ফাংশনে ভেরিয়েবলটি পাস করে ব্যবহার করে।
এই কোডটি কম্পাইল হয়, যদিও আমরা u8
-এর সমস্ত সম্ভাব্য মান তালিকাভুক্ত করিনি, কারণ শেষ প্যাটার্নটি বিশেষভাবে তালিকাভুক্ত নয় এমন সমস্ত মানকে ম্যাচ করবে। এই ক্যাচ-অল প্যাটার্নটি match
-এর সম্পূর্ণ (exhaustive) হওয়ার প্রয়োজনীয়তা পূরণ করে। মনে রাখবেন যে আমাদের ক্যাচ-অল arm-টি শেষে রাখতে হবে কারণ প্যাটার্নগুলো ক্রমানুসারে মূল্যায়ন করা হয়। যদি আমরা ক্যাচ-অল arm-টি আগে রাখি, অন্য arm-গুলো কখনওই চলবে না, তাই Rust আমাদের সতর্ক করবে যদি আমরা একটি ক্যাচ-অল-এর পরে arm যোগ করি!
Rust-এর আরও একটি প্যাটার্ন আছে যা আমরা ব্যবহার করতে পারি যখন আমরা একটি ক্যাচ-অল চাই কিন্তু ক্যাচ-অল প্যাটার্নের মানটি ব্যবহার করতে চাই না: _
একটি বিশেষ প্যাটার্ন যা যেকোনো মানকে ম্যাচ করে এবং সেই মানের সাথে বাইন্ড হয় না। এটি Rust-কে বলে যে আমরা মানটি ব্যবহার করতে যাচ্ছি না, তাই Rust আমাদের একটি অব্যবহৃত ভেরিয়েবল সম্পর্কে সতর্ক করবে না।
আসুন গেমের নিয়ম পরিবর্তন করি: এখন, যদি আপনি ৩ বা ৭ ছাড়া অন্য কিছু রোল করেন, আপনাকে আবার রোল করতে হবে। আমাদের আর ক্যাচ-অল মানটি ব্যবহার করার প্রয়োজন নেই, তাই আমরা আমাদের কোডটি পরিবর্তন করে other
ভেরিয়েবলের পরিবর্তে _
ব্যবহার করতে পারি:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {} }
এই উদাহরণটিও সম্পূর্ণতার (exhaustiveness) প্রয়োজনীয়তা পূরণ করে কারণ আমরা শেষ arm-এ অন্য সব মানকে স্পষ্টভাবে উপেক্ষা করছি; আমরা কিছুই ভুলে যাইনি।
অবশেষে, আমরা গেমের নিয়ম আরও একবার পরিবর্তন করব যাতে আপনি যদি ৩ বা ৭ ছাড়া অন্য কিছু রোল করেন তবে আপনার টার্নে আর কিছুই হবে না। আমরা _
arm-এর সাথে যুক্ত কোড হিসাবে ইউনিট ভ্যালু (খালি টাপল টাইপ যা আমরা "The Tuple Type" বিভাগে উল্লেখ করেছি) ব্যবহার করে এটি প্রকাশ করতে পারি:
fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {} }
এখানে, আমরা Rust-কে স্পষ্টভাবে বলছি যে আমরা আগের arm-গুলোর কোনো প্যাটার্নের সাথে মেলে না এমন অন্য কোনো মান ব্যবহার করতে যাচ্ছি না, এবং আমরা এই ক্ষেত্রে কোনো কোড চালাতে চাই না।
প্যাটার্ন এবং ম্যাচিং সম্পর্কে আরও অনেক কিছু আছে যা আমরা Chapter 19-এ আলোচনা করব। আপাতত, আমরা if let
সিনট্যাক্সে চলে যাচ্ছি, যা এমন পরিস্থিতিতে কার্যকর হতে পারে যেখানে match
এক্সপ্রেশনটি কিছুটা শব্দবহুল হয়।
if let
এবং let else
দিয়ে সংক্ষিপ্ত কন্ট্রোল ফ্লো
if let
সিনট্যাক্সটি আপনাকে if
এবং let
-কে একত্রিত করে একটি কম শব্দবহুল উপায়ে এমন ভ্যালুগুলো হ্যান্ডেল করতে দেয় যা একটি প্যাটার্নের সাথে মেলে, এবং বাকিগুলোকে উপেক্ষা করে। লিস্টিং ৬-৬ এর প্রোগ্রামটি বিবেচনা করুন যা config_max
ভেরিয়েবলের একটি Option<u8>
মানের উপর ম্যাচ করে কিন্তু শুধুমাত্র তখনই কোড চালাতে চায় যখন মানটি Some
ভ্যারিয়েন্ট হয়।
fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {max}"), _ => (), } }
যদি মানটি Some
হয়, আমরা প্যাটার্নে max
ভেরিয়েবলের সাথে মানটি বাইন্ড করে Some
ভ্যারিয়েন্টের ভিতরের মানটি প্রিন্ট করি। আমরা None
ভ্যালু নিয়ে কিছু করতে চাই না। match
এক্সপ্রেশনকে সন্তুষ্ট করার জন্য, আমাদের শুধুমাত্র একটি ভ্যারিয়েন্ট প্রসেস করার পরে _ => ()
যোগ করতে হয়, যা একটি বিরক্তিকর বাড়তি কোড।
এর পরিবর্তে, আমরা if let
ব্যবহার করে এটি আরও সংক্ষিপ্তভাবে লিখতে পারি। নিচের কোডটি লিস্টিং ৬-৬ এর match
-এর মতোই আচরণ করে:
fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!("The maximum is configured to be {max}"); } }``` `if let` সিনট্যাক্সটি একটি প্যাটার্ন এবং একটি এক্সপ্রেশন নেয় যা একটি সমান চিহ্ন দ্বারা পৃথক করা থাকে। এটি `match`-এর মতোই কাজ করে, যেখানে এক্সপ্রেশনটি `match`-কে দেওয়া হয় এবং প্যাটার্নটি তার প্রথম arm হয়। এই ক্ষেত্রে, প্যাটার্নটি হলো `Some(max)`, এবং `max` ভেরিয়েবলটি `Some`-এর ভিতরের মানের সাথে বাইন্ড হয়। আমরা তখন `if let` ব্লকের বডিতে `max` ব্যবহার করতে পারি, ঠিক যেমন আমরা সংশ্লিষ্ট `match` arm-এ `max` ব্যবহার করেছিলাম। `if let` ব্লকের কোডটি কেবল তখনই চলে যদি মানটি প্যাটার্নের সাথে মেলে। `if let` ব্যবহার করার অর্থ হলো কম টাইপিং, কম ইনডেন্টেশন এবং কম বাড়তি কোড। তবে, আপনি `match`-এর সেই সম্পূর্ণ চেকিং (exhaustive checking) হারান যা নিশ্চিত করে যে আপনি কোনো কেস হ্যান্ডেল করতে ভুলে যাচ্ছেন না। `match` এবং `if let`-এর মধ্যে কোনটি বেছে নেবেন তা নির্ভর করে আপনার নির্দিষ্ট পরিস্থিতিতে আপনি কী করছেন এবং সংক্ষিপ্ততা পাওয়ার জন্য সম্পূর্ণ চেকিং হারানোর ট্রেড-অফটি উপযুক্ত কিনা তার উপর। অন্য কথায়, আপনি `if let`-কে একটি `match`-এর জন্য সিনট্যাক্স সুগার (syntax sugar) হিসাবে ভাবতে পারেন যা মান একটি প্যাটার্নের সাথে মিললে কোড চালায় এবং তারপর অন্য সব মানকে উপেক্ষা করে। আমরা `if let`-এর সাথে একটি `else` অন্তর্ভুক্ত করতে পারি। `else`-এর সাথে থাকা কোড ব্লকটি সেই কোড ব্লকের মতোই যা `_` কেসের সাথে যেত, যা `if let` এবং `else`-এর সমতুল্য `match` এক্সপ্রেশনে থাকতো। লিস্টিং ৬-৪-এর `Coin` enum সংজ্ঞাটি মনে করুন, যেখানে `Quarter` ভ্যারিয়েন্টটিতে একটি `UsState` মানও ছিল। যদি আমরা কোয়ার্টারগুলোর রাজ্যের নাম ঘোষণা করার পাশাপাশি দেখা সমস্ত নন-কোয়ার্টার কয়েন গণনা করতে চাই, আমরা এটি একটি `match` এক্সপ্রেশন দিয়ে করতে পারতাম, এভাবে: ```rust #[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let coin = Coin::Penny; let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {state:?}!"), _ => count += 1, } }
অথবা আমরা একটি if let
এবং else
এক্সপ্রেশন ব্যবহার করতে পারতাম, এভাবে:
#[derive(Debug)] enum UsState { Alabama, Alaska, // --snip-- } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn main() { let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {state:?}!"); } else { count += 1; } }
let...else
ব্যবহার করে “হ্যাপি পাথে” থাকা
একটি সাধারণ প্যাটার্ন হলো যখন একটি মান উপস্থিত থাকে তখন কিছু গণনা করা এবং অন্যথায় একটি ডিফল্ট মান ফেরত দেওয়া। আমাদের UsState
মানসহ কয়েনের উদাহরণটি চালিয়ে গেলে, যদি আমরা কোয়ার্টারের উপর থাকা রাজ্যের বয়সের উপর নির্ভর করে মজার কিছু বলতে চাই, আমরা একটি রাজ্যের বয়স পরীক্ষা করার জন্য UsState
-এ একটি মেথড যুক্ত করতে পারি, এভাবে:
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { if let Coin::Quarter(state) = coin { if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } else { None } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
তারপর আমরা কয়েনের ধরনের উপর ম্যাচ করার জন্য if let
ব্যবহার করতে পারি, কন্ডিশনের বডির মধ্যে একটি state
ভেরিয়েবল চালু করে, যেমনটি লিস্টিং ৬-৭-এ দেখানো হয়েছে।
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { if let Coin::Quarter(state) = coin { if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } else { None } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
এটি কাজটি সম্পন্ন করে, কিন্তু এটি কাজটি if let
স্টেটমেন্টের বডির মধ্যে ঠেলে দিয়েছে, এবং যদি কাজটি আরও জটিল হয়, তবে টপ-লেভেল ব্রাঞ্চগুলো কীভাবে সম্পর্কিত তা বোঝা কঠিন হতে পারে। আমরা if let
থেকে state
তৈরি করতে বা আগেভাগেই রিটার্ন করতে এক্সপ্রেশনগুলো একটি মান তৈরি করে এই সত্যের সুবিধাও নিতে পারি, যেমনটি লিস্টিং ৬-৮-এ দেখানো হয়েছে। (আপনি match
দিয়েও একই রকম করতে পারেন।)
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { let state = if let Coin::Quarter(state) = coin { state } else { return None; }; if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
তবে এটিও তার নিজস্ব উপায়ে অনুসরণ করা কিছুটা বিরক্তিকর! if let
-এর একটি ব্রাঞ্চ একটি মান তৈরি করে, এবং অন্যটি ফাংশন থেকে পুরোপুরি রিটার্ন করে।
এই সাধারণ প্যাটার্নটিকে আরও সুন্দরভাবে প্রকাশ করার জন্য, Rust-এ let...else
রয়েছে। let...else
সিনট্যাক্সটি বাম দিকে একটি প্যাটার্ন এবং ডানদিকে একটি এক্সপ্রেশন নেয়, যা if let
-এর মতোই, তবে এটির কোনো if
ব্রাঞ্চ নেই, কেবল একটি else
ব্রাঞ্চ আছে। যদি প্যাটার্নটি মেলে, এটি বাইরের স্কোপে প্যাটার্ন থেকে মানটি বাইন্ড করবে। যদি প্যাটার্নটি না মেলে, প্রোগ্রামটি else
arm-এ চলে যাবে, যা অবশ্যই ফাংশন থেকে রিটার্ন করতে হবে।
লিস্টিং ৬-৯-এ, আপনি দেখতে পারেন লিস্টিং ৬-৮ if let
-এর পরিবর্তে let...else
ব্যবহার করলে কেমন দেখায়।
#[derive(Debug)] // so we can inspect the state in a minute enum UsState { Alabama, Alaska, // --snip-- } impl UsState { fn existed_in(&self, year: u16) -> bool { match self { UsState::Alabama => year >= 1819, UsState::Alaska => year >= 1959, // -- snip -- } } } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn describe_state_quarter(coin: Coin) -> Option<String> { let Coin::Quarter(state) = coin else { return None; }; if state.existed_in(1900) { Some(format!("{state:?} is pretty old, for America!")) } else { Some(format!("{state:?} is relatively new.")) } } fn main() { if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) { println!("{desc}"); } }
লক্ষ্য করুন যে এটি ফাংশনের প্রধান বডিতে "হ্যাপি পাথে" (on the happy path) থাকে, if let
-এর মতো দুটি ব্রাঞ্চের জন্য উল্লেখযোগ্যভাবে ভিন্ন কন্ট্রোল ফ্লো ছাড়াই।
যদি আপনার এমন পরিস্থিতি থাকে যেখানে আপনার প্রোগ্রামের যুক্তি একটি match
ব্যবহার করে প্রকাশ করার জন্য খুব শব্দবহুল হয়, মনে রাখবেন যে if let
এবং let...else
আপনার Rust টুলবক্সেও রয়েছে।
সারাংশ
আমরা এখন কাস্টম type তৈরি করতে enum কীভাবে ব্যবহার করতে হয় তা কভার করেছি যা একটি গণনাকৃত মানের সেটের মধ্যে একটি হতে পারে। আমরা দেখিয়েছি কীভাবে স্ট্যান্ডার্ড লাইব্রেরির Option<T>
type আপনাকে type সিস্টেম ব্যবহার করে error প্রতিরোধ করতে সহায়তা করে। যখন enum মানগুলোর ভিতরে ডেটা থাকে, আপনি match
বা if let
ব্যবহার করে সেই মানগুলো এক্সট্র্যাক্ট এবং ব্যবহার করতে পারেন, এটি নির্ভর করে আপনার কতগুলো কেস হ্যান্ডেল করতে হবে তার উপর।
আপনার Rust প্রোগ্রামগুলো এখন আপনার ডোমেনের ধারণাগুলো structs এবং enums ব্যবহার করে প্রকাশ করতে পারে। আপনার API-তে ব্যবহারের জন্য কাস্টম type তৈরি করা type safety নিশ্চিত করে: compiler নিশ্চিত করবে যে আপনার ফাংশনগুলো কেবল সেই type-এর মান পায় যা প্রতিটি ফাংশন আশা করে।
আপনার ব্যবহারকারীদের জন্য একটি সুসংগঠিত API সরবরাহ করার জন্য যা ব্যবহার করা সহজ এবং শুধুমাত্র ব্যবহারকারীদের যা প্রয়োজন তা প্রকাশ করে, আসুন এখন Rust-এর মডিউলগুলোর দিকে নজর দেওয়া যাক।
প্যাকেজ, ক্রেট এবং মডিউলের মাধ্যমে ক্রমবর্ধমান প্রজেক্ট ম্যানেজ করা
আপনি যখন বড় প্রোগ্রাম লিখবেন, আপনার কোড অর্গানাইজ করা ক্রমশ গুরুত্বপূর্ণ হয়ে উঠবে। সম্পর্কিত ফাংশনালিটি গ্রুপ করে এবং ভিন্ন ভিন্ন ফিচারসহ কোডকে আলাদা করে, আপনি স্পষ্ট করতে পারবেন যে একটি নির্দিষ্ট ফিচার ইমপ্লিমেন্ট করা কোড কোথায় খুঁজে পাওয়া যাবে এবং সেই ফিচারের কার্যকারিতা পরিবর্তন করতে কোথায় যেতে হবে।
এখন পর্যন্ত আমরা যে প্রোগ্রামগুলো লিখেছি তা একটি ফাইলের একটি মডিউলের মধ্যে ছিল। একটি প্রজেক্ট যখন বড় হয়, তখন কোডকে একাধিক মডিউল এবং তারপর একাধিক ফাইলে বিভক্ত করে অর্গানাইজ করা উচিত। একটি প্যাকেজে একাধিক বাইনারি ক্রেট এবং ঐচ্ছিকভাবে একটি লাইব্রেরি ক্রেট থাকতে পারে। প্যাকেজ বড় হওয়ার সাথে সাথে, আপনি এর কিছু অংশ আলাদা ক্রেটে এক্সট্র্যাক্ট করতে পারেন যা এক্সটার্নাল ডিপেন্ডেন্সি হয়ে ওঠে। এই অধ্যায়ে এই সমস্ত কৌশল নিয়ে আলোচনা করা হয়েছে। খুব বড় প্রজেক্টের জন্য, যেখানে একগুচ্ছ আন্তঃসম্পর্কিত প্যাকেজ একসাথে বিকশিত হয়, কার্গো (Cargo) workspaces সরবরাহ করে, যা আমরা অধ্যায় ১৪-এর “কার্গো ওয়ার্কস্পেস” বিভাগে আলোচনা করব।
আমরা ইমপ্লিমেন্টেশন ডিটেইলস এনক্যাপসুলেট করা নিয়েও আলোচনা করব, যা আপনাকে উচ্চ স্তরে কোড পুনঃব্যবহার (reuse) করতে দেয়: একবার আপনি একটি অপারেশন ইমপ্লিমেন্ট করার পর, অন্য কোড আপনার কোডের পাবলিক ইন্টারফেসের মাধ্যমে তাকে কল করতে পারে, এর ইমপ্লিমেন্টেশন কীভাবে কাজ করে তা না জেনেই। আপনি যেভাবে কোড লিখেন তা নির্ধারণ করে কোন অংশগুলো অন্য কোডের ব্যবহারের জন্য পাবলিক থাকবে এবং কোন অংশগুলো প্রাইভেট ইমপ্লিমেন্টেশন ডিটেইলস যা আপনি পরিবর্তন করার অধিকার রাখেন। এটি আপনার মাথায় রাখার মতো বিবরণের পরিমাণ সীমিত করার আরেকটি উপায়।
এর সাথে সম্পর্কিত একটি ধারণা হলো স্কোপ (scope): যে নেস্টেড কনটেক্সটে কোড লেখা হয়, সেখানে একটি নামের সেট থাকে যা “ইন স্কোপ” (in scope) হিসেবে সংজ্ঞায়িত। কোড পড়া, লেখা এবং কম্পাইল করার সময়, প্রোগ্রামার এবং কম্পাইলারদের জানতে হবে যে একটি নির্দিষ্ট স্থানের একটি নির্দিষ্ট নাম কোনো variable
, function
, struct
, enum
, module
, constant
বা অন্য কোনো আইটেমকে নির্দেশ করে কিনা এবং সেই আইটেমটির অর্থ কী। আপনি স্কোপ তৈরি করতে পারেন এবং কোন নামগুলো স্কোপের ভিতরে বা বাইরে থাকবে তা পরিবর্তন করতে পারেন। আপনি একই স্কোপে একই নামের দুটি আইটেম রাখতে পারবেন না; নাম সংক্রান্ত বিরোধ (name conflicts) মেটানোর জন্য টুলস উপলব্ধ আছে।
রাস্টের বেশ কিছু ফিচার আছে যা আপনাকে আপনার কোডের অর্গানাইজেশন পরিচালনা করতে সাহায্য করে, যার মধ্যে রয়েছে কোন ডিটেইলস এক্সপোজ করা হবে, কোন ডিটেইলস প্রাইভেট থাকবে, এবং আপনার প্রোগ্রামের প্রতিটি স্কোপে কোন নামগুলো থাকবে। এই ফিচারগুলোকে কখনও কখনও সম্মিলিতভাবে মডিউল সিস্টেম (module system) বলা হয়, যার মধ্যে রয়েছে:
- প্যাকেজ (Packages): কার্গোর একটি ফিচার যা আপনাকে ক্রেট বিল্ড, টেস্ট এবং শেয়ার করতে দেয়।
- ক্রেট (Crates): মডিউলের একটি ট্রি যা একটি লাইব্রেরি বা এক্সিকিউটেবল তৈরি করে।
- মডিউল এবং use: আপনাকে পাথের অর্গানাইজেশন, স্কোপ এবং প্রাইভেসি নিয়ন্ত্রণ করতে দেয়।
- পাথ (Paths): একটি আইটেম, যেমন কোনো
struct
,function
, বাmodule
-এর নামকরণ করার একটি উপায়।
এই অধ্যায়ে, আমরা এই সমস্ত ফিচারগুলো নিয়ে আলোচনা করব, দেখব কিভাবে তারা একে অপরের সাথে ইন্টারঅ্যাক্ট করে, এবং স্কোপ ম্যানেজ করার জন্য এগুলো কীভাবে ব্যবহার করতে হয় তা ব্যাখ্যা করব। এই অধ্যায়ের শেষে, মডিউল সিস্টেম সম্পর্কে আপনার একটি শক্ত ধারণা তৈরি হবে এবং আপনি একজন প্রো-এর মতো স্কোপ নিয়ে কাজ করতে সক্ষম হবেন!
প্যাকেজ এবং ক্রেট
মডিউল সিস্টেমের প্রথম যে অংশগুলো নিয়ে আমরা আলোচনা করব তা হলো প্যাকেজ এবং ক্রেট।
একটি ক্রেট (crate) হলো কোডের সবচেয়ে ছোট অংশ যা রাস্ট কম্পাইলার একবারে বিবেচনা করে। এমনকি যদি আপনি cargo
-র পরিবর্তে rustc
রান করেন এবং একটি মাত্র সোর্স কোড ফাইল পাস করেন (যেমনটা আমরা প্রথম অধ্যায়ের “একটি রাস্ট প্রোগ্রাম লেখা এবং রান করা” অংশে করেছিলাম), কম্পাইলার সেই ফাইলটিকে একটি ক্রেট হিসেবে বিবেচনা করে। ক্রেট-এর মধ্যে মডিউল থাকতে পারে, এবং সেই মডিউলগুলো অন্য ফাইলে ডিফাইন করা থাকতে পারে যা ক্রেটের সাথে কম্পাইল হয়, যা আমরা পরবর্তী বিভাগগুলিতে দেখব।
একটি ক্রেট দুই ধরনের হতে পারে: একটি বাইনারি ক্রেট অথবা একটি লাইব্রেরি ক্রেট। বাইনারি ক্রেট (Binary crates) হলো এমন প্রোগ্রাম যা আপনি একটি এক্সিকিউটেবল (executable) হিসেবে কম্পাইল করতে পারেন, যেমন একটি কমান্ড লাইন প্রোগ্রাম বা একটি সার্ভার। প্রতিটিতে অবশ্যই main
নামের একটি ফাংশন থাকতে হবে যা এক্সিকিউটেবল রান হলে কী ঘটবে তা নির্ধারণ করে। আমরা এখন পর্যন্ত যতগুলো ক্রেট তৈরি করেছি তার সবই বাইনারি ক্রেট।
লাইব্রেরি ক্রেট (Library crates)-এর কোনো main
ফাংশন নেই এবং এগুলো এক্সিকিউটেবল হিসেবে কম্পাইল হয় না। এর পরিবর্তে, তারা এমন ফাংশনালিটি ডিফাইন করে যা একাধিক প্রজেক্টের সাথে শেয়ার করার জন্য তৈরি। উদাহরণস্বরূপ, আমরা দ্বিতীয় অধ্যায়ে যে rand
ক্রেট ব্যবহার করেছি, তা র্যান্ডম নম্বর জেনারেট করার ফাংশনালিটি প্রদান করে। বেশিরভাগ সময় যখন Rustacean-রা “ক্রেট” বলেন, তখন তারা লাইব্রেরি ক্রেটকেই বোঝান, এবং তারা “ক্রেট” শব্দটিকে সাধারণ প্রোগ্রামিং ধারণা “লাইব্রেরি”-র সমার্থক হিসেবে ব্যবহার করেন।
ক্রেট রুট (crate root) হলো একটি সোর্স ফাইল যেখান থেকে রাস্ট কম্পাইলার কাজ শুরু করে এবং যা আপনার ক্রেটের রুট মডিউল তৈরি করে (আমরা মডিউল সম্পর্কে বিস্তারিতভাবে “স্কোপ এবং প্রাইভেসি নিয়ন্ত্রণের জন্য মডিউল ডিফাইন করা” বিভাগে ব্যাখ্যা করব)।
একটি প্যাকেজ (package) হলো এক বা একাধিক ক্রেটের একটি বান্ডিল যা একটি নির্দিষ্ট সেট অফ ফাংশনালিটি প্রদান করে। একটি প্যাকেজে একটি Cargo.toml ফাইল থাকে যা বর্ণনা করে কিভাবে সেই ক্রেটগুলো বিল্ড করতে হবে। কার্গো (Cargo) নিজেও আসলে একটি প্যাকেজ যা আপনার কোড বিল্ড করার জন্য ব্যবহৃত কমান্ড লাইন টুলের বাইনারি ক্রেট ধারণ করে। কার্গো প্যাকেজে একটি লাইব্রেরি ক্রেটও রয়েছে যার উপর বাইনারি ক্রেটটি নির্ভর করে। অন্যান্য প্রজেক্টগুলো কার্গো কমান্ড লাইন টুলের মতো একই লজিক ব্যবহার করার জন্য কার্গো লাইব্রেরি ক্রেটের উপর নির্ভর করতে পারে।
একটি প্যাকেজে আপনার ইচ্ছামত যত খুশি বাইনারি ক্রেট থাকতে পারে, কিন্তু সর্বোচ্চ একটি লাইব্রেরি ক্রেট থাকতে পারে। একটি প্যাকেজে অবশ্যই কমপক্ষে একটি ক্রেট থাকতে হবে, তা লাইব্রেরি হোক বা বাইনারি।
চলুন দেখি একটি প্যাকেজ তৈরি করলে কী ঘটে। প্রথমে আমরা cargo new my-project
কমান্ডটি প্রবেশ করাই:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
cargo new my-project
চালানোর পর, কার্গো কী তৈরি করেছে তা দেখার জন্য আমরা ls
ব্যবহার করি। প্রজেক্ট ডিরেক্টরিতে, একটি Cargo.toml ফাইল রয়েছে, যা আমাদের একটি প্যাকেজ দেয়। এছাড়াও একটি src ডিরেক্টরি রয়েছে যার মধ্যে main.rs ফাইলটি আছে। আপনার টেক্সট এডিটরে Cargo.toml খুলুন, এবং লক্ষ্য করুন যে সেখানে src/main.rs সম্পর্কে কোনো উল্লেখ নেই। কার্গো এই কনভেনশন অনুসরণ করে যে, src/main.rs হলো প্যাকেজের নামের সাথে মিলিয়ে একটি বাইনারি ক্রেটের ক্রেট রুট। একইভাবে, কার্গো জানে যে যদি প্যাকেজ ডিরেক্টরিতে src/lib.rs থাকে, তাহলে প্যাকেজটিতে প্যাকেজের নামের সাথে মিলিয়ে একটি লাইব্রেরি ক্রেট রয়েছে, এবং src/lib.rs হলো তার ক্রেট রুট। কার্গো লাইব্রেরি বা বাইনারি বিল্ড করার জন্য ক্রেট রুট ফাইলগুলো rustc
-কে পাস করে।
এখানে, আমাদের একটি প্যাকেজ আছে যাতে শুধুমাত্র src/main.rs রয়েছে, যার অর্থ হলো এটিতে শুধুমাত্র my-project
নামের একটি বাইনারি ক্রেট আছে। যদি একটি প্যাকেজে src/main.rs এবং src/lib.rs উভয়ই থাকে, তবে এটির দুটি ক্রেট রয়েছে: একটি বাইনারি এবং একটি লাইব্রেরি, এবং উভয়ের নামই প্যাকেজের নামের সমান হয়। একটি প্যাকেজে একাধিক বাইনারি ক্রেট থাকতে পারে src/bin ডিরেক্টরিতে ফাইল রাখার মাধ্যমে: প্রতিটি ফাইল একটি পৃথক বাইনারি ক্রেট হবে।
স্কোপ এবং প্রাইভেসি নিয়ন্ত্রণের জন্য মডিউল ডিফাইন করা
এই বিভাগে, আমরা মডিউল এবং মডিউল সিস্টেমের অন্যান্য অংশ নিয়ে কথা বলব, যেমন পাথ (paths), যা আপনাকে বিভিন্ন আইটেমের নামকরণ করতে দেয়; use
কীওয়ার্ড, যা একটি পাথকে স্কোপের মধ্যে নিয়ে আসে; এবং pub
কীওয়ার্ড, যা আইটেমগুলোকে পাবলিক করে। আমরা as
কীওয়ার্ড, এক্সটার্নাল প্যাকেজ এবং গ্লোব অপারেটর নিয়েও আলোচনা করব।
মডিউল চিট শিট
আমরা মডিউল এবং পাথের বিস্তারিত বিবরণে যাওয়ার আগে, এখানে মডিউল, পাথ, use
কীওয়ার্ড, এবং pub
কীওয়ার্ড কম্পাইলারে কীভাবে কাজ করে এবং বেশিরভাগ ডেভেলপাররা কীভাবে তাদের কোড অর্গানাইজ করেন তার একটি দ্রুত রেফারেন্স দেওয়া হলো। আমরা এই অধ্যায় জুড়ে এই নিয়মগুলির প্রতিটি উদাহরণ নিয়ে আলোচনা করব, কিন্তু মডিউল কীভাবে কাজ করে তা মনে করিয়ে দেওয়ার জন্য এটি একটি চমৎকার রেফারেন্স।
- ক্রেট রুট থেকে শুরু করুন: একটি ক্রেট কম্পাইল করার সময়, কম্পাইলার প্রথমে ক্রেট রুট ফাইলে (সাধারণত একটি লাইব্রেরি ক্রেটের জন্য src/lib.rs বা একটি বাইনারি ক্রেটের জন্য src/main.rs) কম্পাইল করার জন্য কোড খোঁজে।
- মডিউল ডিক্লেয়ার করা: ক্রেট রুট ফাইলে, আপনি নতুন মডিউল ডিক্লেয়ার করতে পারেন; ধরুন আপনি
mod garden;
দিয়ে একটি “garden” মডিউল ডিক্লেয়ার করলেন। কম্পাইলার মডিউলের কোডটি এই জায়গাগুলিতে খুঁজবে:- ইনলাইন,
mod garden
-এর পরের সেমিকোলনটির পরিবর্তে কোঁকড়া বন্ধনীর (curly brackets) ভিতরে। - src/garden.rs ফাইলে।
- src/garden/mod.rs ফাইলে।
- ইনলাইন,
- সাবমডিউল ডিক্লেয়ার করা: ক্রেট রুট ছাড়া অন্য যেকোনো ফাইলে আপনি সাবমডিউল ডিক্লেয়ার করতে পারেন। উদাহরণস্বরূপ, আপনি হয়তো src/garden.rs ফাইলে
mod vegetables;
ডিক্লেয়ার করতে পারেন। কম্পাইলার সাবমডিউলের কোডটি প্যারেন্ট মডিউলের নামের ডিরেক্টরিতে এই জায়গাগুলিতে খুঁজবে:- ইনলাইন, সরাসরি
mod vegetables
-এর পরে, সেমিকোলনের পরিবর্তে কোঁকড়া বন্ধনীর ভিতরে। - src/garden/vegetables.rs ফাইলে।
- src/garden/vegetables/mod.rs ফাইলে।
- ইনলাইন, সরাসরি
- মডিউলের কোডের পাথ: একবার একটি মডিউল আপনার ক্রেটের অংশ হয়ে গেলে, আপনি সেই ক্রেটের অন্য যেকোনো জায়গা থেকে সেই মডিউলের কোড রেফার করতে পারবেন, যতক্ষণ প্রাইভেসি নিয়ম অনুমতি দেয়, কোডের পাথ ব্যবহার করে। উদাহরণস্বরূপ, garden vegetables মডিউলের একটি
Asparagus
টাইপcrate::garden::vegetables::Asparagus
পাথে পাওয়া যাবে। - প্রাইভেট বনাম পাবলিক: একটি মডিউলের ভিতরের কোড ডিফল্টরূপে তার প্যারেন্ট মডিউল থেকে প্রাইভেট থাকে। একটি মডিউলকে পাবলিক করতে,
mod
-এর পরিবর্তেpub mod
দিয়ে ডিক্লেয়ার করুন। একটি পাবলিক মডিউলের ভেতরের আইটেমগুলোকেও পাবলিক করতে, তাদের ডিক্লেয়ারেশনের আগেpub
ব্যবহার করুন। use
কীওয়ার্ড: একটি স্কোপের মধ্যে,use
কীওয়ার্ড লম্বা পাথের পুনরাবৃত্তি কমাতে আইটেমগুলোর জন্য শর্টকাট তৈরি করে। যেকোনো স্কোপে যেখানেcrate::garden::vegetables::Asparagus
রেফার করা যায়, আপনিuse crate::garden::vegetables::Asparagus;
ব্যবহার করে একটি শর্টকাট তৈরি করতে পারেন এবং তারপর থেকে সেই স্কোপে ঐ টাইপটি ব্যবহার করার জন্য আপনাকে কেবলAsparagus
লিখতে হবে।
এখানে, আমরা backyard
নামে একটি বাইনারি ক্রেট তৈরি করছি যা এই নিয়মগুলি তুলে ধরে। ক্রেটের ডিরেক্টরি, যার নামও backyard
, তাতে এই ফাইল এবং ডিরেক্টরিগুলো রয়েছে:
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
এই ক্ষেত্রে ক্রেট রুট ফাইলটি হলো src/main.rs, এবং এতে রয়েছে:
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
pub mod garden;
লাইনটি কম্পাইলারকে src/garden.rs-এ পাওয়া কোড অন্তর্ভুক্ত করতে বলে, যা হলো:
pub mod vegetables;
এখানে, pub mod vegetables;
মানে src/garden/vegetables.rs-এর কোডও অন্তর্ভুক্ত করা হয়েছে। সেই কোডটি হলো:
#[derive(Debug)]
pub struct Asparagus {}
এখন আসুন এই নিয়মগুলির বিস্তারিত বিবরণে যাই এবং সেগুলি বাস্তবে প্রদর্শন করি!
সম্পর্কিত কোডকে মডিউলে গ্রুপিং করা
মডিউল (Modules) আমাদের একটি ক্রেটের মধ্যে পঠনযোগ্যতা (readability) এবং সহজ পুনঃব্যবহারের জন্য কোড অর্গানাইজ করতে দেয়। মডিউল আমাদের আইটেমগুলির প্রাইভেসি (privacy) নিয়ন্ত্রণ করার অনুমতিও দেয় কারণ একটি মডিউলের ভেতরের কোড ডিফল্টরূপে প্রাইভেট থাকে। প্রাইভেট আইটেমগুলো হলো অভ্যন্তরীণ ইমপ্লিমেন্টেশন ডিটেইলস যা বাইরের ব্যবহারের জন্য উপলব্ধ নয়। আমরা মডিউল এবং তার ভেতরের আইটেমগুলোকে পাবলিক করার সিদ্ধান্ত নিতে পারি, যা সেগুলোকে এক্সপোজ করে এবং বাইরের কোডকে সেগুলো ব্যবহার ও তার উপর নির্ভর করার অনুমতি দেয়।
একটি উদাহরণ হিসাবে, চলুন একটি লাইব্রেরি ক্রেট লিখি যা একটি রেস্তোরাঁর কার্যকারিতা প্রদান করে। আমরা ফাংশনের সিগনেচারগুলো ডিফাইন করব কিন্তু তাদের বডি খালি রাখব, যাতে একটি রেস্তোরাঁর ইমপ্লিমেন্টেশনের পরিবর্তে কোডের অর্গানাইজেশনের উপর মনোযোগ দিতে পারি।
রেস্তোরাঁ শিল্পে, একটি রেস্তোরাঁর কিছু অংশকে ফ্রন্ট অফ হাউস (front of house) এবং অন্যগুলোকে ব্যাক অফ হাউস (back of house) বলা হয়। ফ্রন্ট অফ হাউস হলো যেখানে গ্রাহকরা থাকেন; এর মধ্যে রয়েছে যেখানে হোস্টরা গ্রাহকদের বসান, সার্ভাররা অর্ডার এবং পেমেন্ট নেন, এবং বারটেন্ডাররা ড্রিঙ্ক তৈরি করেন। ব্যাক অফ হাউস হলো যেখানে শেফ এবং বাবুর্চিরা রান্নাঘরে কাজ করেন, ডিশওয়াশাররা থালাবাসন পরিষ্কার করেন, এবং ম্যানেজাররা প্রশাসনিক কাজ করেন।
আমাদের ক্রেটকে এইভাবে গঠন করার জন্য, আমরা এর ফাংশনগুলোকে নেস্টেড মডিউলে অর্গানাইজ করতে পারি। cargo new restaurant --lib
চালিয়ে restaurant
নামে একটি নতুন লাইব্রেরি তৈরি করুন। তারপর কিছু মডিউল এবং ফাংশন সিগনেচার ডিফাইন করতে Listing 7-1-এর কোডটি src/lib.rs-এ লিখুন; এই কোডটি হলো ফ্রন্ট অফ হাউস সেকশন।
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
আমরা mod
কীওয়ার্ড এবং তারপরে মডিউলের নাম (এই ক্ষেত্রে, front_of_house
) দিয়ে একটি মডিউল ডিফাইন করি। মডিউলের বডি তখন কোঁকড়া বন্ধনীর ভিতরে যায়। মডিউলের ভিতরে, আমরা অন্য মডিউল রাখতে পারি, যেমন এই ক্ষেত্রে hosting
এবং serving
মডিউল। মডিউল অন্যান্য আইটেম, যেমন struct, enum, constant, trait এবং Listing 7-1-এর মতো ফাংশনের ডেফিনিশনও রাখতে পারে।
মডিউল ব্যবহার করে, আমরা সম্পর্কিত ডেফিনিশনগুলোকে একসাথে গ্রুপ করতে পারি এবং কেন তারা সম্পর্কিত তার নামকরণ করতে পারি। এই কোড ব্যবহারকারী প্রোগ্রামাররা সমস্ত ডেফিনিশন পড়ার পরিবর্তে গ্রুপগুলোর উপর ভিত্তি করে কোড নেভিগেট করতে পারবেন, যা তাদের জন্য প্রাসঙ্গিক ডেফিনিশন খুঁজে পাওয়া সহজ করে তোলে। যে প্রোগ্রামাররা এই কোডে নতুন ফাংশনালিটি যোগ করবেন, তারা জানবেন প্রোগ্রামকে অর্গানাইজড রাখতে কোডটি কোথায় রাখতে হবে।
পূর্বে, আমরা উল্লেখ করেছি যে src/main.rs এবং src/lib.rs-কে ক্রেট রুট বলা হয়। তাদের এই নামের কারণ হলো এই দুটি ফাইলের বিষয়বস্তু ক্রেটের মডিউল স্ট্রাকচারের রুটে crate
নামে একটি মডিউল তৈরি করে, যা মডিউল ট্রি (module tree) নামে পরিচিত।
Listing 7-2, Listing 7-1-এর স্ট্রাকচারের জন্য মডিউল ট্রি দেখায়।
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
এই ট্রি দেখায় কিভাবে কিছু মডিউল অন্য মডিউলের ভিতরে নেস্ট করা আছে; উদাহরণস্বরূপ, hosting
front_of_house
-এর ভিতরে নেস্ট করা। ট্রি এটিও দেখায় যে কিছু মডিউল সিবলিং (siblings), যার মানে তারা একই মডিউলে ডিফাইন করা হয়েছে; hosting
এবং serving
হলো front_of_house
-এর মধ্যে ডিফাইন করা সিবলিং। যদি মডিউল A মডিউল B-এর ভিতরে থাকে, আমরা বলি যে মডিউল A হলো মডিউল B-এর চাইল্ড (child) এবং মডিউল B হলো মডিউল A-এর প্যারেন্ট (parent)। লক্ষ্য করুন যে পুরো মডিউল ট্রি-টি crate
নামের একটি অন্তর্নিহিত (implicit) মডিউলের অধীনে রুট করা হয়েছে।
মডিউল ট্রি আপনাকে আপনার কম্পিউটারের ফাইলসিস্টেমের ডিরেক্টরি ট্রি-এর কথা মনে করিয়ে দিতে পারে; এটি একটি খুব উপযুক্ত তুলনা! ফাইলসিস্টেমের ডিরেক্টরির মতো, আপনি আপনার কোড অর্গানাইজ করতে মডিউল ব্যবহার করেন। এবং একটি ডিরেক্টরিতে ফাইলের মতো, আমাদের মডিউলগুলো খুঁজে বের করার একটি উপায় প্রয়োজন।
মডিউল ট্রি-তে কোনো আইটেম রেফার করার জন্য পাথ
মডিউল ট্রি-তে কোনো আইটেম রাস্টকে কোথায় খুঁজে পাবে তা দেখানোর জন্য, আমরা একটি পাথ (path) ব্যবহার করি, ঠিক যেমন আমরা ফাইলসিস্টেম নেভিগেট করার সময় পাথ ব্যবহার করি। একটি ফাংশন কল করার জন্য, আমাদের তার পাথ জানতে হবে।
একটি পাথ দুই ধরনের হতে পারে:
- একটি অ্যাবসোলিউট পাথ (absolute path) হলো ক্রেট রুট থেকে শুরু হওয়া সম্পূর্ণ পাথ; এক্সটার্নাল ক্রেট থেকে আসা কোডের জন্য অ্যাবসোলিউট পাথ ক্রেটের নাম দিয়ে শুরু হয়, এবং বর্তমান ক্রেটের কোডের জন্য এটি
crate
লিটারেল দিয়ে শুরু হয়। - একটি রিলেটিভ পাথ (relative path) বর্তমান মডিউল থেকে শুরু হয় এবং
self
,super
বা বর্তমান মডিউলের কোনো আইডেন্টিফায়ার ব্যবহার করে।
অ্যাবসোলিউট এবং রিলেটিভ উভয় পাথই এক বা একাধিক আইডেন্টিফায়ার দ্বারা অনুসরণ করা হয় যা ডাবল কোলন (::
) দ্বারা বিভক্ত থাকে।
Listing 7-1-এ ফিরে আসা যাক, ধরুন আমরা add_to_waitlist
ফাংশনটি কল করতে চাই। এটি জিজ্ঞেস করার মতোই: add_to_waitlist
ফাংশনের পাথ কী? Listing 7-3-এ Listing 7-1-এর কিছু মডিউল এবং ফাংশন বাদ দিয়ে দেখানো হয়েছে।
আমরা ক্রেট রুটে ডিফাইন করা একটি নতুন ফাংশন eat_at_restaurant
থেকে add_to_waitlist
ফাংশনটি কল করার দুটি উপায় দেখাব। এই পাথগুলো সঠিক, কিন্তু আরেকটি সমস্যা রয়েছে যা এই উদাহরণটিকে কম্পাইল হতে বাধা দেবে। আমরা একটু পরেই ব্যাখ্যা করব কেন।
eat_at_restaurant
ফাংশনটি আমাদের লাইব্রেরি ক্রেটের পাবলিক API-এর অংশ, তাই আমরা এটিকে pub
কীওয়ার্ড দিয়ে চিহ্নিত করেছি। "pub
কীওয়ার্ড দিয়ে পাথ এক্সপোজ করা” বিভাগে, আমরা pub
সম্পর্কে আরও বিস্তারিত আলোচনা করব।
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
eat_at_restaurant
-এ প্রথমবার যখন আমরা add_to_waitlist
ফাংশনটি কল করি, আমরা একটি অ্যাবসোলিউট পাথ ব্যবহার করি। add_to_waitlist
ফাংশনটি eat_at_restaurant
-এর একই ক্রেটে ডিফাইন করা হয়েছে, যার মানে আমরা একটি অ্যাবসোলিউট পাথ শুরু করতে crate
কীওয়ার্ডটি ব্যবহার করতে পারি। এরপর আমরা add_to_waitlist
পর্যন্ত পৌঁছানোর জন্য প্রতিটি মডিউলকে ক্রমানুসারে অন্তর্ভুক্ত করি। আপনি একই কাঠামোর একটি ফাইলসিস্টেম কল্পনা করতে পারেন: আমরা add_to_waitlist
প্রোগ্রামটি চালানোর জন্য /front_of_house/hosting/add_to_waitlist
পাথটি নির্দিষ্ট করতাম; ক্রেট রুট থেকে শুরু করার জন্য crate
নামটি ব্যবহার করা আপনার শেলের ফাইলসিস্টেম রুট থেকে শুরু করার জন্য /
ব্যবহার করার মতো।
eat_at_restaurant
-এ দ্বিতীয়বার যখন আমরা add_to_waitlist
কল করি, আমরা একটি রিলেটিভ পাথ ব্যবহার করি। পাথটি front_of_house
দিয়ে শুরু হয়, যা eat_at_restaurant
-এর মতো মডিউল ট্রি-এর একই লেভেলে ডিফাইন করা মডিউলের নাম। এখানে ফাইলসিস্টেমের সমতুল্য হবে front_of_house/hosting/add_to_waitlist
পাথটি ব্যবহার করা। একটি মডিউলের নাম দিয়ে শুরু করার মানে হলো পাথটি রিলেটিভ।
রিলেটিভ বা অ্যাবসোলিউট পাথ ব্যবহার করবেন কিনা, সেই সিদ্ধান্তটি আপনি আপনার প্রজেক্টের উপর ভিত্তি করে নেবেন এবং এটি নির্ভর করে আপনি আইটেম ডেফিনিশন কোডটি আইটেম ব্যবহারকারী কোডের সাথে একসাথে নাকি আলাদাভাবে সরানোর সম্ভাবনা বেশি তার উপর। উদাহরণস্বরূপ, যদি আমরা front_of_house
মডিউল এবং eat_at_restaurant
ফাংশনটিকে customer_experience
নামের একটি মডিউলে স্থানান্তর করি, তাহলে আমাদের add_to_waitlist
-এর অ্যাবসোলিউট পাথ আপডেট করতে হবে, কিন্তু রিলেটিভ পাথটি তখনও বৈধ থাকবে। তবে, যদি আমরা eat_at_restaurant
ফাংশনটিকে আলাদাভাবে dining
নামের একটি মডিউলে স্থানান্তর করি, তাহলে add_to_waitlist
কলের অ্যাবসোলিউট পাথ একই থাকবে, কিন্তু রিলেটিভ পাথ আপডেট করতে হবে। আমাদের সাধারণ পছন্দ হলো অ্যাবসোলিউট পাথ নির্দিষ্ট করা, কারণ কোড ডেফিনিশন এবং আইটেম কল একে অপরের থেকে স্বাধীনভাবে সরানোর প্রয়োজন হওয়ার সম্ভাবনা বেশি।
চলুন Listing 7-3 কম্পাইল করার চেষ্টা করি এবং খুঁজে বের করি কেন এটি এখনও কম্পাইল হবে না! আমরা যে এররগুলো পাই তা Listing 7-4-এ দেখানো হয়েছে।
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
এরর মেসেজগুলো বলছে যে hosting
মডিউলটি প্রাইভেট। অন্য কথায়, hosting
মডিউল এবং add_to_waitlist
ফাংশনের জন্য আমাদের কাছে সঠিক পাথ রয়েছে, কিন্তু রাস্ট আমাদের সেগুলি ব্যবহার করতে দেবে না কারণ এটি প্রাইভেট সেকশন অ্যাক্সেস করতে পারে না। রাস্ট-এ, সমস্ত আইটেম (ফাংশন, মেথড, struct, enum, মডিউল, এবং ধ্রুবক) ডিফল্টরূপে প্যারেন্ট মডিউলের কাছে প্রাইভেট থাকে। আপনি যদি কোনো ফাংশন বা struct-এর মতো আইটেমকে প্রাইভেট করতে চান তবে সেটিকে একটি মডিউলের মধ্যে রাখুন।
প্যারেন্ট মডিউলের আইটেমগুলো চাইল্ড মডিউলের ভিতরের প্রাইভেট আইটেম ব্যবহার করতে পারে না, কিন্তু চাইল্ড মডিউলের আইটেমগুলো তাদের পূর্বপুরুষ (ancestor) মডিউলের আইটেম ব্যবহার করতে পারে। এর কারণ হলো চাইল্ড মডিউলগুলো তাদের ইমপ্লিমেন্টেশন ডিটেইলসকে র্যাপ করে এবং লুকিয়ে রাখে, কিন্তু চাইল্ড মডিউলগুলো যে কনটেক্সটে ডিফাইন করা হয়েছে তা দেখতে পারে। আমাদের রূপকটি চালিয়ে যেতে, প্রাইভেসি নিয়মগুলোকে একটি রেস্তোরাঁর ব্যাক অফিসের মতো ভাবুন: সেখানে যা ঘটে তা রেস্তোরাঁর গ্রাহকদের কাছে প্রাইভেট, কিন্তু অফিস ম্যানেজাররা তাদের পরিচালিত রেস্তোরাঁর সবকিছু দেখতে এবং করতে পারেন।
রাস্ট মডিউল সিস্টেমটিকে এমনভাবে কাজ করার জন্য বেছে নিয়েছে যাতে অভ্যন্তরীণ ইমপ্লিমেন্টেশন ডিটেইলস লুকানো ডিফল্ট হয়। এইভাবে, আপনি জানেন যে ভিতরের কোডের কোন অংশগুলো বাইরের কোডকে ব্রেক না করে পরিবর্তন করা যেতে পারে। তবে, রাস্ট আপনাকে pub
কীওয়ার্ড ব্যবহার করে একটি আইটেমকে পাবলিক করার মাধ্যমে চাইল্ড মডিউলের কোডের অভ্যন্তরীণ অংশগুলোকে বাইরের পূর্বপুরুষ মডিউলের কাছে এক্সপোজ করার অপশন দেয়।
pub
কীওয়ার্ড দিয়ে পাথ এক্সপোজ করা
আসুন Listing 7-4-এর এররটিতে ফিরে যাই যা আমাদের বলেছিল যে hosting
মডিউলটি প্রাইভেট। আমরা চাই প্যারেন্ট মডিউলের eat_at_restaurant
ফাংশনটি চাইল্ড মডিউলের add_to_waitlist
ফাংশনটি অ্যাক্সেস করুক, তাই আমরা hosting
মডিউলটিকে pub
কীওয়ার্ড দিয়ে চিহ্নিত করি, যেমনটি Listing 7-5-এ দেখানো হয়েছে।
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
দুর্ভাগ্যবশত, Listing 7-5-এর কোডটি এখনও কম্পাইলার এরর দেয়, যেমনটি Listing 7-6-এ দেখানো হয়েছে।
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:10:37
|
10 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:13:30
|
13 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
কী হলো? mod hosting
-এর সামনে pub
কীওয়ার্ড যুক্ত করা মডিউলটিকে পাবলিক করে। এই পরিবর্তনের ফলে, যদি আমরা front_of_house
অ্যাক্সেস করতে পারি, আমরা hosting
অ্যাক্সেস করতে পারব। কিন্তু hosting
-এর ভেতরের বিষয়বস্তু এখনও প্রাইভেট; মডিউলকে পাবলিক করা তার ভেতরের বিষয়বস্তুকে পাবলিক করে না। একটি মডিউলের উপর pub
কীওয়ার্ড শুধুমাত্র তার পূর্বপুরুষ মডিউলের কোডকে এটি রেফার করার অনুমতি দেয়, এর ভেতরের কোড অ্যাক্সেস করার অনুমতি দেয় না। যেহেতু মডিউল হলো কন্টেইনার, শুধুমাত্র মডিউলটিকে পাবলিক করে আমরা খুব বেশি কিছু করতে পারি না; আমাদের আরও এগিয়ে যেতে হবে এবং মডিউলের ভেতরের এক বা একাধিক আইটেমকেও পাবলিক করতে হবে।
Listing 7-6-এর এররগুলো বলছে যে add_to_waitlist
ফাংশনটি প্রাইভেট। প্রাইভেসি নিয়ম struct, enum, ফাংশন, এবং মেথডের পাশাপাশি মডিউলের ক্ষেত্রেও প্রযোজ্য।
আসুন add_to_waitlist
ফাংশনটিকেও পাবলিক করি তার ডেফিনিশনের আগে pub
কীওয়ার্ড যোগ করে, যেমন Listing 7-7-এ দেখানো হয়েছে।
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
এখন কোডটি কম্পাইল হবে! প্রাইভেসি নিয়মের সাপেক্ষে pub
কীওয়ার্ড যোগ করা কেন আমাদের eat_at_restaurant
-এ এই পাথগুলো ব্যবহার করতে দেয় তা দেখতে, আসুন অ্যাবসোলিউট এবং রিলেটিভ পাথগুলো দেখি।
অ্যাবসোলিউট পাথে, আমরা crate
দিয়ে শুরু করি, যা আমাদের ক্রেটের মডিউল ট্রি-এর রুট। front_of_house
মডিউলটি ক্রেট রুটে ডিফাইন করা হয়েছে। যদিও front_of_house
পাবলিক নয়, কারণ eat_at_restaurant
ফাংশনটি front_of_house
-এর মতো একই মডিউলে ডিফাইন করা হয়েছে (অর্থাৎ, eat_at_restaurant
এবং front_of_house
সিবলিং), আমরা eat_at_restaurant
থেকে front_of_house
রেফার করতে পারি। এরপরে hosting
মডিউলটি pub
দিয়ে চিহ্নিত করা হয়েছে। আমরা hosting
-এর প্যারেন্ট মডিউল অ্যাক্সেস করতে পারি, তাই আমরা hosting
অ্যাক্সেস করতে পারি। অবশেষে, add_to_waitlist
ফাংশনটি pub
দিয়ে চিহ্নিত এবং আমরা তার প্যারেন্ট মডিউল অ্যাক্সেস করতে পারি, তাই এই ফাংশন কলটি কাজ করে!
রিলেটিভ পাথে, প্রথম ধাপ ছাড়া যুক্তিটি অ্যাবসোলিউট পাথের মতোই: ক্রেট রুট থেকে শুরু না করে পাথটি front_of_house
থেকে শুরু হয়। front_of_house
মডিউলটি eat_at_restaurant
-এর মতো একই মডিউলের মধ্যে ডিফাইন করা হয়েছে, তাই eat_at_restaurant
যে মডিউলে ডিফাইন করা হয়েছে সেখান থেকে শুরু হওয়া রিলেটিভ পাথটি কাজ করে। তারপর, যেহেতু hosting
এবং add_to_waitlist
pub
দিয়ে চিহ্নিত করা হয়েছে, বাকি পাথ কাজ করে, এবং এই ফাংশন কলটি বৈধ!
আপনি যদি আপনার লাইব্রেরি ক্রেট শেয়ার করার পরিকল্পনা করেন যাতে অন্যান্য প্রজেক্ট আপনার কোড ব্যবহার করতে পারে, আপনার পাবলিক API হলো আপনার ক্রেটের ব্যবহারকারীদের সাথে আপনার চুক্তি যা নির্ধারণ করে তারা কীভাবে আপনার কোডের সাথে ইন্টারঅ্যাক্ট করতে পারে। আপনার পাবলিক API-তে পরিবর্তনগুলি পরিচালনা করার বিষয়ে অনেক বিবেচনা রয়েছে যাতে লোকেরা আপনার ক্রেটের উপর নির্ভর করতে পারে। এই বিবেচনাগুলো এই বইয়ের আওতার বাইরে; আপনি যদি এই বিষয়ে আগ্রহী হন, তাহলে The Rust API Guidelines দেখুন।
একটি বাইনারি এবং একটি লাইব্রেরি সহ প্যাকেজের জন্য সেরা অভ্যাস
আমরা উল্লেখ করেছি যে একটি প্যাকেজে একটি src/main.rs বাইনারি ক্রেট রুট এবং একটি src/lib.rs লাইব্রেরি ক্রেট রুট উভয়ই থাকতে পারে, এবং উভয় ক্রেটের নাম ডিফল্টরূপে প্যাকেজের নাম হবে। সাধারণত, এই প্যাটার্নের প্যাকেজগুলোতে, যেখানে একটি লাইব্রেরি এবং একটি বাইনারি উভয় ক্রেটই থাকে, বাইনারি ক্রেটে কেবল লাইব্রেরি ক্রেটে সংজ্ঞায়িত কোড কল করে একটি এক্সিকিউটেবল শুরু করার জন্য যথেষ্ট কোড থাকে। এটি অন্যান্য প্রজেক্টকে প্যাকেজের提供的 কার্যকারিতা থেকে উপকৃত হতে দেয় কারণ লাইব্রেরি ক্রেটের কোড শেয়ার করা যেতে পারে।
মডিউল ট্রি src/lib.rs-এ ডিফাইন করা উচিত। তারপরে, যেকোনো পাবলিক আইটেম বাইনারি ক্রেটে প্যাকেজের নাম দিয়ে পাথ শুরু করে ব্যবহার করা যেতে পারে। বাইনারি ক্রেটটি লাইব্রেরি ক্রেটের একজন ব্যবহারকারী হয়ে ওঠে ঠিক যেমন একটি সম্পূর্ণ এক্সটার্নাল ক্রেট লাইব্রেরি ক্রেট ব্যবহার করবে: এটি কেবল পাবলিক API ব্যবহার করতে পারে। এটি আপনাকে একটি ভাল API ডিজাইন করতে সাহায্য করে; আপনি কেবল লেখকই নন, আপনি একজন ক্লায়েন্টও!
অধ্যায় ১২-এ, আমরা একটি কমান্ড লাইন প্রোগ্রামের সাথে এই সাংগঠনিক অনুশীলনটি প্রদর্শন করব যা একটি বাইনারি ক্রেট এবং একটি লাইব্রেরি ক্রেট উভয়ই ধারণ করবে।
super
দিয়ে রিলেটিভ পাথ শুরু করা
আমরা super
ব্যবহার করে রিলেটিভ পাথ তৈরি করতে পারি যা বর্তমান মডিউল বা ক্রেট রুটের পরিবর্তে প্যারেন্ট মডিউলে শুরু হয়। এটি ফাইলসিস্টেম পাথ ..
সিনট্যাক্স দিয়ে শুরু করার মতো, যার মানে হলো প্যারেন্ট ডিরেক্টরিতে যাওয়া। super
ব্যবহার করে আমরা এমন একটি আইটেম রেফারেন্স করতে পারি যা আমরা জানি প্যারেন্ট মডিউলে আছে, যা মডিউল ট্রি পুনর্বিন্যাস করা সহজ করে তুলতে পারে যখন মডিউলটি প্যারেন্টের সাথে ঘনিষ্ঠভাবে সম্পর্কিত কিন্তু প্যারেন্টকে ভবিষ্যতে মডিউল ট্রি-এর অন্য কোথাও সরানো হতে পারে।
Listing 7-8-এর কোডটি বিবেচনা করুন যা এমন একটি পরিস্থিতি মডেল করে যেখানে একজন শেফ একটি ভুল অর্ডার ঠিক করে এবং ব্যক্তিগতভাবে গ্রাহকের কাছে নিয়ে আসে। back_of_house
মডিউলে ডিফাইন করা fix_incorrect_order
ফাংশনটি super
দিয়ে শুরু হওয়া deliver_order
-এর পাথ নির্দিষ্ট করে প্যারেন্ট মডিউলে ডিফাইন করা deliver_order
ফাংশনটিকে কল করে।
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
fix_incorrect_order
ফাংশনটি back_of_house
মডিউলে রয়েছে, তাই আমরা super
ব্যবহার করে back_of_house
-এর প্যারেন্ট মডিউলে যেতে পারি, যা এক্ষেত্রে crate
, অর্থাৎ রুট। সেখান থেকে আমরা deliver_order
খুঁজি এবং এটি খুঁজে পাই। সফল! আমরা মনে করি back_of_house
মডিউল এবং deliver_order
ফাংশনটি একে অপরের সাথে একই সম্পর্কে থাকার সম্ভাবনা রয়েছে এবং যদি আমরা ক্রেটের মডিউল ট্রি পুনর্বিন্যাস করার সিদ্ধান্ত নিই তবে একসাথে সরানো হবে। তাই, আমরা super
ব্যবহার করেছি যাতে ভবিষ্যতে এই কোডটি অন্য মডিউলে সরানো হলে আমাদের কম জায়গায় কোড আপডেট করতে হয়।
Struct এবং Enum-কে পাবলিক করা
আমরা pub
ব্যবহার করে struct এবং enum-কে পাবলিক হিসাবে মনোনীত করতে পারি, কিন্তু struct এবং enum-এর সাথে pub
-এর ব্যবহারে কিছু অতিরিক্ত বিবরণ রয়েছে। যদি আমরা একটি struct ডেফিনিশনের আগে pub
ব্যবহার করি, আমরা struct-টিকে পাবলিক করি, কিন্তু struct-এর ফিল্ডগুলো তখনও প্রাইভেট থাকবে। আমরা প্রতিটি ফিল্ডকে কেস-বাই-কেস ভিত্তিতে পাবলিক বা নট পাবলিক করতে পারি। Listing 7-9-এ, আমরা একটি পাবলিক toast
ফিল্ড এবং একটি প্রাইভেট seasonal_fruit
ফিল্ড সহ একটি পাবলিক back_of_house::Breakfast
struct ডিফাইন করেছি। এটি একটি রেস্তোরাঁর পরিস্থিতি মডেল করে যেখানে গ্রাহক খাবারের সাথে আসা রুটির ধরন বেছে নিতে পারেন, কিন্তু শেফ সিদ্ধান্ত নেন কোন ফলটি খাবারের সাথে পরিবেশন করা হবে, যা ঋতু এবং স্টকের উপর ভিত্তি করে নির্ধারিত হয়। উপলব্ধ ফল দ্রুত পরিবর্তিত হয়, তাই গ্রাহকরা ফল বেছে নিতে বা এমনকি তারা কোন ফল পাবেন তা দেখতেও পারেন না।
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast.
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like.
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal.
// meal.seasonal_fruit = String::from("blueberries");
}
যেহেতু back_of_house::Breakfast
struct-এর toast
ফিল্ডটি পাবলিক, তাই eat_at_restaurant
-এ আমরা ডট নোটেশন ব্যবহার করে toast
ফিল্ডে লিখতে এবং পড়তে পারি। লক্ষ্য করুন যে আমরা eat_at_restaurant
-এ seasonal_fruit
ফিল্ডটি ব্যবহার করতে পারি না, কারণ seasonal_fruit
প্রাইভেট। seasonal_fruit
ফিল্ডের মান পরিবর্তনকারী লাইনটি আনকমেন্ট করে চেষ্টা করুন কী এরর পান তা দেখতে!
এছাড়াও, লক্ষ্য করুন যে যেহেতু back_of_house::Breakfast
-এর একটি প্রাইভেট ফিল্ড আছে, তাই struct-টিকে একটি পাবলিক অ্যাসোসিয়েটেড ফাংশন সরবরাহ করতে হবে যা Breakfast
-এর একটি ইনস্ট্যান্স তৈরি করে (আমরা এখানে এটির নাম দিয়েছি summer
)। যদি Breakfast
-এর এমন কোনো ফাংশন না থাকত, আমরা eat_at_restaurant
-এ Breakfast
-এর একটি ইনস্ট্যান্স তৈরি করতে পারতাম না কারণ আমরা eat_at_restaurant
-এ প্রাইভেট seasonal_fruit
ফিল্ডের মান সেট করতে পারতাম না।
এর বিপরীতে, যদি আমরা একটি enum-কে পাবলিক করি, তবে এর সমস্ত ভ্যারিয়েন্ট তখন পাবলিক হয়ে যায়। আমাদের কেবল enum
কীওয়ার্ডের আগে pub
প্রয়োজন, যেমনটি Listing 7-10-এ দেখানো হয়েছে।
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
যেহেতু আমরা Appetizer
enum-কে পাবলিক করেছি, তাই আমরা eat_at_restaurant
-এ Soup
এবং Salad
ভ্যারিয়েন্টগুলো ব্যবহার করতে পারি।
Enum-গুলো খুব একটা কাজের নয় যদি না তাদের ভ্যারিয়েন্টগুলো পাবলিক হয়; প্রতি ক্ষেত্রে সমস্ত enum ভ্যারিয়েন্টকে pub
দিয়ে অ্যানোটেট করা বিরক্তিকর হবে, তাই enum ভ্যারিয়েন্টের জন্য ডিফল্ট হলো পাবলিক হওয়া। Struct-গুলো প্রায়শই তাদের ফিল্ড পাবলিক না হয়েও দরকারী, তাই struct ফিল্ডগুলো pub
দিয়ে অ্যানোটেট না করা পর্যন্ত ডিফল্টরূপে সবকিছু প্রাইভেট থাকার সাধারণ নিয়ম অনুসরণ করে।
pub
-কে জড়িত করে আরও একটি পরিস্থিতি রয়েছে যা আমরা এখনও কভার করিনি, এবং সেটি হলো আমাদের শেষ মডিউল সিস্টেম ফিচার: use
কীওয়ার্ড। আমরা প্রথমে use
নিয়ে একা আলোচনা করব, এবং তারপর আমরা দেখাব কিভাবে pub
এবং use
একত্রিত করা যায়।
use
কীওয়ার্ড ব্যবহার করে স্কোপে পাথ নিয়ে আসা
ফাংশন কল করার জন্য বারবার পুরো পাথ লেখাটা বেশ অসুবিধাজনক এবং পুনরাবৃত্তিমূলক মনে হতে পারে। Listing 7-7-এ, আমরা add_to_waitlist
ফাংশনের জন্য অ্যাবসোলিউট বা রিলেটিভ পাথ যাই বেছে নিই না কেন, প্রতিবার add_to_waitlist
কল করার সময় আমাদের front_of_house
এবং hosting
-ও নির্দিষ্ট করতে হয়েছিল। সৌভাগ্যবশত, এই প্রক্রিয়াটি সহজ করার একটি উপায় আছে: আমরা use
কীওয়ার্ড দিয়ে একবার একটি পাথের শর্টকাট তৈরি করে নিতে পারি এবং তারপর স্কোপের অন্য সব জায়গায় ছোট নামটি ব্যবহার করতে পারি।
Listing 7-11-এ, আমরা crate::front_of_house::hosting
মডিউলটিকে eat_at_restaurant
ফাংশনের স্কোপে নিয়ে এসেছি, তাই eat_at_restaurant
-এর মধ্যে add_to_waitlist
ফাংশনটি কল করার জন্য আমাদের কেবল hosting::add_to_waitlist
নির্দিষ্ট করতে হবে।
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
একটি স্কোপে use
এবং একটি পাথ যোগ করা ফাইলসিস্টেমে একটি সিম্বলিক লিঙ্ক তৈরি করার মতো। ক্রেট রুটে use crate::front_of_house::hosting
যোগ করার মাধ্যমে, hosting
এখন সেই স্কোপে একটি বৈধ নাম, ঠিক যেন hosting
মডিউলটি ক্রেট রুটেই ডিফাইন করা হয়েছিল। use
দিয়ে স্কোপে আনা পাথগুলোও অন্য যেকোনো পাথের মতোই প্রাইভেসি পরীক্ষা করে।
মনে রাখবেন যে use
শুধুমাত্র সেই নির্দিষ্ট স্কোপের জন্য শর্টকাট তৈরি করে যেখানে use
ব্যবহার করা হয়েছে। Listing 7-12-এ eat_at_restaurant
ফাংশনটিকে customer
নামের একটি নতুন চাইল্ড মডিউলে সরানো হয়েছে, যা use
স্টেটমেন্টের থেকে ভিন্ন একটি স্কোপ, তাই ফাংশনের বডি কম্পাইল হবে না।
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
কম্পাইলার এরর দেখায় যে শর্টকাটটি আর customer
মডিউলের মধ্যে প্রযোজ্য নয়:
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`
|
help: consider importing this module through its public re-export
|
10 + use crate::hosting;
|
warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:7:5
|
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
লক্ষ্য করুন, একটি ওয়ার্নিংও রয়েছে যে use
তার স্কোপে আর ব্যবহৃত হচ্ছে না! এই সমস্যাটি সমাধান করার জন্য, use
-কে customer
মডিউলের ভিতরেও সরিয়ে নিন, অথবা চাইল্ড customer
মডিউলের মধ্যে প্যারেন্ট মডিউলের শর্টকাটটিকে super::hosting
দিয়ে রেফারেন্স করুন।
প্রচলিত use
পাথ তৈরি করা
Listing 7-11-এ, আপনি হয়তো ভেবেছিলেন কেন আমরা use crate::front_of_house::hosting
নির্দিষ্ট করেছি এবং তারপর eat_at_restaurant
-এ hosting::add_to_waitlist
কল করেছি, যেখানে আমরা use
পাথটিকে add_to_waitlist
ফাংশন পর্যন্ত প্রসারিত করে একই ফলাফল অর্জন করতে পারতাম, যেমনটি Listing 7-13-এ দেখানো হয়েছে।
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
যদিও Listing 7-11 এবং Listing 7-13 উভয়ই একই কাজ সম্পন্ন করে, use
দিয়ে একটি ফাংশনকে স্কোপে আনার প্রচলিত (idiomatic) উপায় হলো Listing 7-11। use
দিয়ে ফাংশনের প্যারেন্ট মডিউলকে স্কোপে আনার মানে হলো ফাংশনটি কল করার সময় আমাদের প্যারেন্ট মডিউলটি নির্দিষ্ট করতে হবে। ফাংশন কল করার সময় প্যারেন্ট মডিউল নির্দিষ্ট করা এটা স্পষ্ট করে যে ফাংশনটি স্থানীয়ভাবে ডিফাইন করা হয়নি, এবং একই সাথে সম্পূর্ণ পাথের পুনরাবৃত্তিও কমায়। Listing 7-13-এর কোডটি অস্পষ্ট কারণ add_to_waitlist
কোথায় ডিফাইন করা হয়েছে তা বোঝা যায় না।
অন্যদিকে, use
দিয়ে struct, enum এবং অন্যান্য আইটেম আনার সময়, সম্পূর্ণ পাথ নির্দিষ্ট করাই প্রচলিত রীতি। Listing 7-14 স্ট্যান্ডার্ড লাইব্রেরির HashMap
struct-কে একটি বাইনারি ক্রেটের স্কোপে আনার প্রচলিত উপায় দেখায়।
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
এই রীতির পিছনে কোনো শক্তিশালী কারণ নেই: এটি কেবল একটি কনভেনশন যা সময়ের সাথে তৈরি হয়েছে, এবং লোকেরা এইভাবেই রাস্ট কোড পড়তে এবং লিখতে অভ্যস্ত হয়ে গেছে।
এই রীতির ব্যতিক্রম হলো যদি আমরা use
স্টেটমেন্ট ব্যবহার করে একই নামের দুটি আইটেমকে স্কোপে নিয়ে আসি, কারণ রাস্ট এটির অনুমতি দেয় না। Listing 7-15 দেখায় কিভাবে একই নামের কিন্তু ভিন্ন প্যারেন্ট মডিউলের দুটি Result
টাইপকে স্কোপে আনা যায় এবং কীভাবে তাদের রেফার করতে হয়।
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
আপনি দেখতে পাচ্ছেন, প্যারেন্ট মডিউল ব্যবহার করে দুটি Result
টাইপকে আলাদা করা হয়েছে। যদি আমরা use std::fmt::Result
এবং use std::io::Result
নির্দিষ্ট করতাম, তাহলে আমাদের একই স্কোপে দুটি Result
টাইপ থাকত, এবং যখন আমরা Result
ব্যবহার করতাম, রাস্ট জানত না আমরা কোনটি বোঝাতে চাইছি।
as
কীওয়ার্ড দিয়ে নতুন নাম প্রদান করা
use
দিয়ে একই নামের দুটি টাইপকে একই স্কোপে আনার সমস্যার আরেকটি সমাধান আছে: পাথের পরে, আমরা as
এবং টাইপের জন্য একটি নতুন স্থানীয় নাম বা alias নির্দিষ্ট করতে পারি। Listing 7-16-এ, Listing 7-15-এর কোডটি লেখার আরেকটি উপায় দেখানো হয়েছে, যেখানে as
ব্যবহার করে দুটি Result
টাইপের মধ্যে একটির নাম পরিবর্তন করা হয়েছে।
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
দ্বিতীয় use
স্টেটমেন্টে, আমরা std::io::Result
টাইপের জন্য নতুন নাম IoResult
বেছে নিয়েছি, যা std::fmt
থেকে আনা Result
-এর সাথে冲突 করবে না, যেটিও আমরা স্কোপে নিয়ে এসেছি। Listing 7-15 এবং Listing 7-16 উভয়ই প্রচলিত হিসাবে বিবেচিত হয়, তাই পছন্দ আপনার!
pub use
দিয়ে নাম পুনরায়-এক্সপোর্ট করা
যখন আমরা use
কীওয়ার্ড দিয়ে একটি নামকে স্কোপে নিয়ে আসি, তখন নামটি সেই স্কোপের জন্য প্রাইভেট থাকে যেখানে আমরা এটি ইম্পোর্ট করেছি। সেই স্কোপের বাইরের কোডকে সেই নামটি এমনভাবে রেফার করতে সক্ষম করার জন্য যেন এটি সেই স্কোপেই ডিফাইন করা হয়েছিল, আমরা pub
এবং use
একত্রিত করতে পারি। এই কৌশলটিকে রি-এক্সপোর্টিং (re-exporting) বলা হয় কারণ আমরা একটি আইটেমকে স্কোপে নিয়ে আসছি এবং একই সাথে সেই আইটেমটিকে অন্যদের তাদের স্কোপে আনার জন্য উপলব্ধ করে দিচ্ছি।
Listing 7-17-এ Listing 7-11-এর কোডটি দেখানো হয়েছে যেখানে রুট মডিউলের use
-কে pub use
-এ পরিবর্তন করা হয়েছে।
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
এই পরিবর্তনের আগে, এক্সটার্নাল কোডকে restaurant::front_of_house::hosting::add_to_waitlist()
পাথ ব্যবহার করে add_to_waitlist
ফাংশনটি কল করতে হতো, যার জন্য front_of_house
মডিউলটিকেও pub
হিসাবে চিহ্নিত করার প্রয়োজন হতো। এখন যেহেতু এই pub use
রুট মডিউল থেকে hosting
মডিউলটিকে রি-এক্সপোর্ট করেছে, এক্সটার্নাল কোড এর পরিবর্তে restaurant::hosting::add_to_waitlist()
পাথ ব্যবহার করতে পারে।
রি-এক্সপোর্টিং তখন উপযোগী যখন আপনার কোডের অভ্যন্তরীণ কাঠামো এবং আপনার কোড কল করা প্রোগ্রামাররা ডোমেইন সম্পর্কে যেভাবে চিন্তা করেন, তা ভিন্ন হয়। উদাহরণস্বরূপ, এই রেস্তোরাঁর রূপকে, রেস্তোরাঁ পরিচালনাকারী ব্যক্তিরা “ফ্রন্ট অফ হাউস” এবং “ব্যাক অফ হাউস” নিয়ে ভাবেন। কিন্তু রেস্তোরাঁয় আসা গ্রাহকরা সম্ভবত রেস্তোরাঁর অংশগুলো সম্পর্কে সেই পরিভাষায় ভাববেন না। pub use
-এর মাধ্যমে, আমরা আমাদের কোড একটি কাঠামোতে লিখতে পারি কিন্তু একটি ভিন্ন কাঠামো এক্সপোজ করতে পারি। এটি করলে আমাদের লাইব্রেরিটি লাইব্রেরিতে কাজ করা প্রোগ্রামারদের জন্য এবং লাইব্রেরি কল করা প্রোগ্রামারদের জন্য সুসংগঠিত হয়। আমরা pub use
-এর আরেকটি উদাহরণ এবং এটি কীভাবে আপনার ক্রেটের ডকুমেন্টেশনকে প্রভাবিত করে তা অধ্যায় ১৪-এর “pub use
দিয়ে একটি সুবিধাজনক পাবলিক API এক্সপোর্ট করা” বিভাগে দেখব।
এক্সটার্নাল প্যাকেজ ব্যবহার করা
দ্বিতীয় অধ্যায়ে, আমরা একটি গেসিং গেম প্রজেক্ট প্রোগ্রাম করেছিলাম যা র্যান্ডম নম্বর পেতে rand
নামের একটি এক্সটার্নাল প্যাকেজ ব্যবহার করেছিল। আমাদের প্রজেক্টে rand
ব্যবহার করার জন্য, আমরা Cargo.toml-এ এই লাইনটি যোগ করেছিলাম:
rand = "0.8.5"
Cargo.toml-এ rand
-কে একটি ডিপেন্ডেন্সি হিসাবে যোগ করা কার্গোকে crates.io থেকে rand
প্যাকেজ এবং এর যেকোনো ডিপেন্ডেন্সি ডাউনলোড করতে এবং rand
আমাদের প্রজেক্টের জন্য উপলব্ধ করতে বলে।
তারপর, rand
ডেফিনিশনগুলোকে আমাদের প্যাকেজের স্কোপে আনতে, আমরা ক্রেটের নাম, rand
দিয়ে শুরু হওয়া একটি use
লাইন যোগ করেছিলাম এবং যে আইটেমগুলো আমরা স্কোপে আনতে চেয়েছিলাম তা তালিকাভুক্ত করেছিলাম। মনে করুন যে দ্বিতীয় অধ্যায়ের “একটি র্যান্ডম নম্বর জেনারেট করা” অংশে, আমরা Rng
ট্রেইটটিকে স্কোপে নিয়ে এসেছিলাম এবং rand::thread_rng
ফাংশনটি কল করেছিলাম:
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}");
}
রাস্ট কমিউনিটির সদস্যরা crates.io-তে অনেক প্যাকেজ উপলব্ধ করেছেন, এবং সেগুলোর যেকোনো একটিকে আপনার প্যাকেজে অন্তর্ভুক্ত করার জন্য এই একই পদক্ষেপগুলো অনুসরণ করতে হয়: সেগুলোকে আপনার প্যাকেজের Cargo.toml ফাইলে তালিকাভুক্ত করা এবং use
ব্যবহার করে তাদের ক্রেট থেকে আইটেমগুলোকে স্কোপে আনা।
মনে রাখবেন যে স্ট্যান্ডার্ড std
লাইব্রেরিও একটি ক্রেট যা আমাদের প্যাকেজের জন্য এক্সটার্নাল। যেহেতু স্ট্যান্ডার্ড লাইব্রেরিটি রাস্ট ভাষার সাথে সরবরাহ করা হয়, তাই std
-কে অন্তর্ভুক্ত করার জন্য আমাদের Cargo.toml পরিবর্তন করার প্রয়োজন নেই। কিন্তু সেখান থেকে আইটেমগুলোকে আমাদের প্যাকেজের স্কোপে আনতে আমাদের use
দিয়ে এটিকে রেফার করতে হবে। উদাহরণস্বরূপ, HashMap
-এর জন্য আমরা এই লাইনটি ব্যবহার করব:
#![allow(unused)] fn main() { use std::collections::HashMap; }
এটি একটি অ্যাবসোলিউট পাথ যা std
দিয়ে শুরু হয়, যা স্ট্যান্ডার্ড লাইব্রেরি ক্রেটের নাম।
বড় use
তালিকা পরিষ্কার করতে নেস্টেড পাথ ব্যবহার করা
যদি আমরা একই ক্রেট বা একই মডিউলে ডিফাইন করা একাধিক আইটেম ব্যবহার করি, তবে প্রতিটি আইটেমকে তার নিজস্ব লাইনে তালিকাভুক্ত করলে আমাদের ফাইলগুলিতে অনেক উল্লম্ব স্থান (vertical space) নষ্ট হতে পারে। উদাহরণস্বরূপ, Listing 2-4-এর গেসিং গেমে আমাদের এই দুটি use
স্টেটমেন্ট std
থেকে আইটেমগুলোকে স্কোপে এনেছিল:
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
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}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
এর পরিবর্তে, আমরা একই আইটেমগুলোকে এক লাইনে স্কোপে আনতে নেস্টেড পাথ ব্যবহার করতে পারি। আমরা এটি করি পাথের সাধারণ অংশ নির্দিষ্ট করে, তারপরে দুটি কোলন, এবং তারপর কোঁকড়া বন্ধনীর (curly brackets) মধ্যে পাথের বিভিন্ন অংশগুলির একটি তালিকা দিয়ে, যেমনটি Listing 7-18-এ দেখানো হয়েছে।
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--
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");
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!"),
}
}```
</Listing>
বড় প্রোগ্রামগুলিতে, নেস্টেড পাথ ব্যবহার করে একই ক্রেট বা মডিউল থেকে অনেক আইটেম স্কোপে আনা পৃথক `use` স্টেটমেন্টের সংখ্যা অনেক কমাতে পারে!
আমরা একটি পাথের যেকোনো স্তরে একটি নেস্টেড পাথ ব্যবহার করতে পারি, যা দুটি `use` স্টেটমেন্টকে একত্রিত করার সময় উপযোগী হয় যেগুলোর একটি সাবপাথ শেয়ার করা থাকে। উদাহরণস্বরূপ, Listing 7-19 দুটি `use` স্টেটমেন্ট দেখায়: একটি যা `std::io`-কে স্কোপে নিয়ে আসে এবং আরেকটি যা `std::io::Write`-কে স্কোপে নিয়ে আসে।
<Listing number="7-19" file-name="src/lib.rs" caption="দুটি `use` স্টেটমেন্ট যেখানে একটি অন্যটির সাবপাথ">
```rust,noplayground
use std::io;
use std::io::Write;
এই দুটি পাথের সাধারণ অংশ হলো std::io
, এবং এটিই প্রথম সম্পূর্ণ পাথ। এই দুটি পাথকে একটি use
স্টেটমেন্টে একত্রিত করতে, আমরা নেস্টেড পাথে self
ব্যবহার করতে পারি, যেমনটি Listing 7-20-এ দেখানো হয়েছে।
use std::io::{self, Write};
এই লাইনটি std::io
এবং std::io::Write
-কে স্কোপে নিয়ে আসে।
গ্লোব অপারেটর
যদি আমরা একটি পাথে ডিফাইন করা সমস্ত পাবলিক আইটেমকে স্কোপে আনতে চাই, আমরা সেই পাথ এবং তারপরে *
গ্লোব অপারেটরটি নির্দিষ্ট করতে পারি:
#![allow(unused)] fn main() { use std::collections::*; }
এই use
স্টেটমেন্টটি std::collections
-এ ডিফাইন করা সমস্ত পাবলিক আইটেমকে বর্তমান স্কোপে নিয়ে আসে। গ্লোব অপারেটর ব্যবহার করার সময় সতর্ক থাকুন! গ্লোব ব্যবহার করলে কোন নামগুলো স্কোপে আছে এবং আপনার প্রোগ্রামে ব্যবহৃত একটি নাম কোথা থেকে ডিফাইন করা হয়েছে তা বোঝা কঠিন হয়ে যেতে পারে। উপরন্তু, যদি ডিপেন্ডেন্সি তার ডেফিনিশন পরিবর্তন করে, তাহলে আপনি যা ইম্পোর্ট করেছেন তাও পরিবর্তিত হবে, যা ডিপেন্ডেন্সি আপগ্রেড করার সময় কম্পাইলার এররের কারণ হতে পারে, যদি ডিপেন্ডেন্সি আপনার একই স্কোপের কোনো ডেফিনিশনের মতো একই নামের একটি ডেফিনিশন যোগ করে, উদাহরণস্বরূপ।
গ্লোব অপারেটরটি প্রায়শই টেস্টিংয়ের সময় tests
মডিউলের মধ্যে পরীক্ষার অধীনে থাকা সমস্ত কিছু আনতে ব্যবহৃত হয়; আমরা এটি সম্পর্কে অধ্যায় ১১-এর “কিভাবে টেস্ট লিখতে হয়” অংশে কথা বলব। গ্লোব অপারেটরটি কখনও কখনও প্রিলিউড প্যাটার্নের অংশ হিসাবেও ব্যবহৃত হয়: সেই প্যাটার্ন সম্পর্কে আরও তথ্যের জন্য স্ট্যান্ডার্ড লাইব্রেরির ডকুমেন্টেশন দেখুন।
মডিউলগুলোকে বিভিন্ন ফাইলে বিভক্ত করা
এখন পর্যন্ত, এই অধ্যায়ের সমস্ত উদাহরণে একটি ফাইলের মধ্যেই একাধিক মডিউল ডিফাইন করা হয়েছে। যখন মডিউলগুলো বড় হয়ে যায়, তখন কোড নেভিগেট করা সহজ করার জন্য আপনি তাদের ডেফিনিশনগুলোকে একটি পৃথক ফাইলে সরাতে চাইতে পারেন।
উদাহরণস্বরূপ, চলুন Listing 7-17-এর কোড থেকে শুরু করি যেখানে একাধিক রেস্তোরাঁ মডিউল ছিল। আমরা সব মডিউল ক্রেট রুট ফাইলে ডিফাইন না করে, মডিউলগুলোকে আলাদা ফাইলে এক্সট্র্যাক্ট করব। এই ক্ষেত্রে, ক্রেট রুট ফাইলটি হলো src/lib.rs, কিন্তু এই পদ্ধতিটি বাইনারি ক্রেটের ক্ষেত্রেও কাজ করে যার ক্রেট রুট ফাইল হলো src/main.rs।
প্রথমে আমরা front_of_house
মডিউলটিকে তার নিজস্ব ফাইলে এক্সট্র্যাক্ট করব। front_of_house
মডিউলের জন্য কোঁকড়া বন্ধনীর (curly brackets) ভিতরের কোডটি সরিয়ে ফেলুন, শুধুমাত্র mod front_of_house;
ডিক্লেয়ারেশনটি রেখে দিন, যাতে src/lib.rs ফাইলে Listing 7-21-এ দেখানো কোডটি থাকে। মনে রাখবেন, যতক্ষণ না আমরা Listing 7-22-এ src/front_of_house.rs ফাইলটি তৈরি করছি, ততক্ষণ এটি কম্পাইল হবে না।
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
এরপর, কোঁকড়া বন্ধনীর মধ্যে যে কোডটি ছিল, সেটি src/front_of_house.rs নামের একটি নতুন ফাইলে রাখুন, যেমনটি Listing 7-22-এ দেখানো হয়েছে। কম্পাইলার জানে যে এই ফাইলটিতে কোড খুঁজতে হবে কারণ এটি ক্রেট রুটে front_of_house
নামের মডিউল ডিক্লেয়ারেশনটি পেয়েছে।
pub mod hosting {
pub fn add_to_waitlist() {}
}```
</Listing>
মনে রাখবেন যে আপনার মডিউল ট্রি-তে একটি `mod` ডিক্লেয়ারেশন ব্যবহার করে একটি ফাইল শুধুমাত্র _একবারই_ লোড করতে হবে। একবার কম্পাইলার জেনে গেলে যে ফাইলটি প্রজেক্টের অংশ (এবং `mod` স্টেটমেন্টটি কোথায় রেখেছেন তার উপর ভিত্তি করে মডিউল ট্রি-তে কোডটি কোথায় অবস্থিত তা জানে), আপনার প্রজেক্টের অন্যান্য ফাইলগুলোকে লোড করা ফাইলের কোডটি তার ডিক্লেয়ার করা পাথ ব্যবহার করে রেফার করা উচিত, যা [“মডিউল ট্রি-তে কোনো আইটেম রেফার করার জন্য পাথ”][paths]<!-- ignore --> বিভাগে আলোচনা করা হয়েছে। অন্য কথায়, `mod` একটি “include” অপারেশন _নয়_ যা আপনি অন্যান্য প্রোগ্রামিং ভাষায় দেখে থাকতে পারেন।
এর পরে, আমরা `hosting` মডিউলটিকে তার নিজস্ব ফাইলে এক্সট্র্যাক্ট করব। প্রক্রিয়াটি কিছুটা ভিন্ন কারণ `hosting` হলো `front_of_house`-এর একটি চাইল্ড মডিউল, রুট মডিউলের নয়। আমরা `hosting`-এর জন্য ফাইলটিকে একটি নতুন ডিরেক্টরিতে রাখব যার নাম হবে মডিউল ট্রি-তে তার পূর্বপুরুষদের নামে, এই ক্ষেত্রে _src/front_of_house_।
`hosting` সরানো শুরু করতে, আমরা _src/front_of_house.rs_ পরিবর্তন করে শুধুমাত্র `hosting` মডিউলের ডিক্লেয়ারেশনটি রাখব:
<Listing file-name="src/front_of_house.rs">
```rust,ignore
pub mod hosting;
তারপর আমরা একটি src/front_of_house ডিরেক্টরি এবং একটি hosting.rs ফাইল তৈরি করব যাতে hosting
মডিউলে করা ডেফিনিশনগুলো থাকবে:
pub fn add_to_waitlist() {}
যদি আমরা এর পরিবর্তে hosting.rs ফাইলটি src ডিরেক্টরিতে রাখতাম, কম্পাইলার আশা করত যে hosting.rs কোডটি ক্রেট রুটে ডিক্লেয়ার করা একটি hosting
মডিউলে থাকবে, এবং front_of_house
মডিউলের চাইল্ড হিসাবে ডিক্লেয়ার করা হবে না। কোন মডিউলের কোডের জন্য কোন ফাইলগুলো পরীক্ষা করতে হবে সে সম্পর্কে কম্পাইলারের নিয়মগুলোর মানে হলো ডিরেক্টরি এবং ফাইলগুলো মডিউল ট্রি-এর সাথে আরও ঘনিষ্ঠভাবে মিলে যায়।
বিকল্প ফাইল পাথ
এখন পর্যন্ত আমরা সবচেয়ে প্রচলিত ফাইল পাথগুলো কভার করেছি যা রাস্ট কম্পাইলার ব্যবহার করে, কিন্তু রাস্ট একটি পুরানো স্টাইলের ফাইল পাথও সাপোর্ট করে। ক্রেট রুটে ডিক্লেয়ার করা
front_of_house
নামের একটি মডিউলের জন্য, কম্পাইলার মডিউলের কোডটি খুঁজবে:
- src/front_of_house.rs (যা আমরা কভার করেছি)
- src/front_of_house/mod.rs (পুরানো স্টাইল, এখনও সমর্থিত পাথ)
front_of_house
-এর একটি সাবমডিউলhosting
-এর জন্য, কম্পাইলার মডিউলের কোড খুঁজবে:
- src/front_of_house/hosting.rs (যা আমরা কভার করেছি)
- src/front_of_house/hosting/mod.rs (পুরানো স্টাইল, এখনও সমর্থিত পাথ)
আপনি যদি একই মডিউলের জন্য উভয় স্টাইল ব্যবহার করেন, আপনি একটি কম্পাইলার এরর পাবেন। একই প্রজেক্টে বিভিন্ন মডিউলের জন্য উভয় স্টাইলের মিশ্রণ ব্যবহার করা অনুমোদিত, কিন্তু এটি আপনার প্রজেক্ট নেভিগেট করা লোকেদের জন্য বিভ্রান্তিকর হতে পারে।
mod.rs নামের ফাইল ব্যবহার করার স্টাইলের প্রধান অসুবিধা হলো আপনার প্রজেক্টে অনেকগুলো mod.rs নামের ফাইল তৈরি হতে পারে, যা আপনার এডিটরে একই সময়ে খোলা থাকলে বিভ্রান্তিকর হতে পারে।
আমরা প্রতিটি মডিউলের কোড একটি পৃথক ফাইলে সরিয়েছি, এবং মডিউল ট্রি একই রয়েছে। eat_at_restaurant
-এর ফাংশন কলগুলো কোনো পরিবর্তন ছাড়াই কাজ করবে, যদিও ডেফিনিশনগুলো বিভিন্ন ফাইলে রয়েছে। এই কৌশলটি আপনাকে মডিউলগুলো আকারে বড় হওয়ার সাথে সাথে নতুন ফাইলে স্থানান্তর করতে দেয়।
মনে রাখবেন যে src/lib.rs-এর pub use crate::front_of_house::hosting
স্টেটমেন্টটিও পরিবর্তিত হয়নি, এবং use
-এর ক্রেটের অংশ হিসাবে কোন ফাইলগুলো কম্পাইল করা হয় তার উপর কোনো প্রভাব নেই। mod
কীওয়ার্ড মডিউল ডিক্লেয়ার করে, এবং রাস্ট সেই মডিউলে থাকা কোডের জন্য মডিউলের নামের সাথে মিলিয়ে একটি ফাইল খোঁজে।
সারসংক্ষেপ
রাস্ট আপনাকে একটি প্যাকেজকে একাধিক ক্রেটে এবং একটি ক্রেটকে একাধিক মডিউলে বিভক্ত করতে দেয় যাতে আপনি এক মডিউলে ডিফাইন করা আইটেম অন্য মডিউল থেকে রেফার করতে পারেন। আপনি অ্যাবসোলিউট বা রিলেটিভ পাথ নির্দিষ্ট করে এটি করতে পারেন। এই পাথগুলো একটি use
স্টেটমেন্টের মাধ্যমে স্কোপে আনা যেতে পারে যাতে আপনি সেই স্কোপে আইটেমটির একাধিক ব্যবহারের জন্য একটি ছোট পাথ ব্যবহার করতে পারেন। মডিউল কোড ডিফল্টরূপে প্রাইভেট থাকে, কিন্তু আপনি pub
কীওয়ার্ড যোগ করে ডেফিনিশনগুলোকে পাবলিক করতে পারেন।
পরবর্তী অধ্যায়ে, আমরা স্ট্যান্ডার্ড লাইব্রেরির কিছু কালেকশন ডেটা স্ট্রাকচার দেখব যা আপনি আপনার সুন্দরভাবে সাজানো কোডে ব্যবহার করতে পারবেন।
সাধারণ Collections
Rust-এর standard library-তে খুবই দরকারি কিছু ডেটা স্ট্রাকচার (data structure) রয়েছে, যেগুলোকে collections বলা হয়। বেশিরভাগ ডেটা টাইপ (data type) একটি নির্দিষ্ট ভ্যালু (value) প্রকাশ করে, কিন্তু collections-এ একাধিক ভ্যালু থাকতে পারে। বিল্ট-ইন (built-in) array এবং tuple টাইপের মতো নয়, এই collections-এর ডেটা heap-এ স্টোর করা হয়। এর মানে হলো, compile time-এ ডেটার পরিমাণ জানার প্রয়োজন হয় না এবং প্রোগ্রাম (program) চলার সময় এটি বাড়তে বা কমতে পারে। প্রতিটি collection-এর নিজস্ব সক্ষমতা এবং সীমাবদ্ধতা রয়েছে। আপনার প্রয়োজন অনুযায়ী সঠিক collection বেছে নেওয়া একটি দক্ষতা যা সময়ের সাথে সাথে আপনি অর্জন করবেন। এই অধ্যায়ে আমরা তিনটি বহুল ব্যবহৃত collection নিয়ে আলোচনা করব:
- একটি vector আপনাকে একাধিক ভ্যালু একে অপরের পাশে রেখে স্টোর করার সুযোগ দেয়।
- একটি string হলো ক্যারেক্টার বা অক্ষরের একটি collection। আমরা এর আগে
String
টাইপ নিয়ে কথা বলেছি, কিন্তু এই অধ্যায়ে আমরা এটি নিয়ে বিস্তারিত আলোচনা করব। - একটি hash map আপনাকে একটি নির্দিষ্ট key-এর সাথে একটি ভ্যালু যুক্ত করার সুযোগ দেয়। এটি map নামক সাধারণ ডেটা স্ট্রাকচারের একটি বিশেষ বাস্তবায়ন।
Standard library-র অন্যান্য collection সম্পর্কে জানতে the documentation দেখুন।
আমরা এই অধ্যায়ে vector, string, এবং hash map কীভাবে তৈরি ও আপডেট করতে হয় এবং এদের প্রত্যেকের বিশেষত্ব কী, তা নিয়ে আলোচনা করব।
Vector ব্যবহার করে ভ্যালুর তালিকা স্টোর করা
আমরা প্রথম যে collection টাইপটি দেখব তা হলো Vec<T>
, যা vector নামেও পরিচিত। Vector আপনাকে একটি ডেটা স্ট্রাকচারের মধ্যে একাধিক ভ্যালু স্টোর করার সুযোগ দেয়, যা মেমরিতে সমস্ত ভ্যালু একে অপরের পাশে রাখে। Vector শুধুমাত্র একই টাইপের ভ্যালু স্টোর করতে পারে। যখন আপনার কাছে আইটেমের একটি তালিকা থাকে, যেমন একটি ফাইলের টেক্সট লাইন বা শপিং কার্টে থাকা আইটেমের দাম, তখন এগুলি খুব দরকারি।
নতুন Vector তৈরি করা
একটি নতুন খালি vector তৈরি করতে, আমরা Vec::new
ফাংশনটি কল করি, যেমনটি লিস্টিং ৮-১ এ দেখানো হয়েছে।
fn main() { let v: Vec<i32> = Vec::new(); }
লক্ষ্য করুন, আমরা এখানে একটি type annotation যোগ করেছি। যেহেতু আমরা এই vector-এ কোনো ভ্যালু রাখছি না, তাই Rust জানে না আমরা কী ধরনের element স্টোর করতে চাই। এটি একটি গুরুত্বপূর্ণ বিষয়। Vector জেনেরিক (generics) ব্যবহার করে তৈরি করা হয়; আমরা অধ্যায় ১০-এ আপনার নিজের টাইপের সাথে জেনেরিক কীভাবে ব্যবহার করতে হয় তা আলোচনা করব। আপাতত জেনে রাখুন যে standard library দ্বারা সরবরাহ করা Vec<T>
টাইপটি যেকোনো টাইপ ধারণ করতে পারে। যখন আমরা একটি নির্দিষ্ট টাইপের ভ্যালু রাখার জন্য একটি vector তৈরি করি, তখন আমরা angle brackets-এর মধ্যে টাইপটি নির্দিষ্ট করতে পারি। লিস্টিং ৮-১ এ, আমরা Rust-কে বলেছি যে v
-তে থাকা Vec<T>
টি i32
টাইপের element ধারণ করবে।
বেশিরভাগ সময়, আপনি প্রাথমিক ভ্যালুসহ একটি Vec<T>
তৈরি করবেন এবং Rust অনুমান করে নেবে আপনি কোন টাইপের ভ্যালু স্টোর করতে চান, তাই আপনাকে খুব কমই এই type annotation করতে হবে। Rust সুবিধাজনকভাবে vec!
macro সরবরাহ করে, যা আপনার দেওয়া ভ্যালুগুলো ধারণ করে একটি নতুন vector তৈরি করবে। লিস্টিং ৮-২ একটি নতুন Vec<i32>
তৈরি করে যা 1
, 2
, এবং 3
ভ্যালুগুলো ধারণ করে। Integer টাইপটি i32
কারণ এটি ডিফল্ট ইন্টিজার টাইপ, যা আমরা অধ্যায় ৩ এর "Data Types" বিভাগে আলোচনা করেছি।
fn main() { let v = vec![1, 2, 3]; }
যেহেতু আমরা প্রাথমিক i32
ভ্যালু দিয়েছি, Rust অনুমান করতে পারে যে v
-এর টাইপ হলো Vec<i32>
, এবং type annotation-এর প্রয়োজন নেই। এরপর, আমরা দেখব কীভাবে একটি vector পরিবর্তন করতে হয়।
Vector আপডেট করা
একটি vector তৈরি করে তাতে element যোগ করার জন্য, আমরা push
মেথড ব্যবহার করতে পারি, যেমনটি লিস্টিং ৮-৩ এ দেখানো হয়েছে।
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
যেকোনো ভ্যারিয়েবলের মতোই, যদি আমরা এর ভ্যালু পরিবর্তন করতে চাই, তবে অধ্যায় ৩-এ আলোচিত mut
কীওয়ার্ড ব্যবহার করে এটিকে mutable করতে হবে। আমরা যে সংখ্যাগুলো এর ভেতরে রেখেছি তা সবই i32
টাইপের, এবং Rust ডেটা থেকে এটি অনুমান করে নেয়, তাই আমাদের Vec<i32>
annotation-এর প্রয়োজন নেই।
Vector-এর Element পড়া
একটি vector-এ স্টোর করা ভ্যালু রেফারেন্স করার দুটি উপায় আছে: indexing ব্যবহার করে অথবা get
মেথড ব্যবহার করে। নীচের উদাহরণগুলিতে, আমরা অতিরিক্ত স্পষ্টতার জন্য এই ফাংশনগুলো থেকে ফেরত আসা ভ্যালুগুলির টাইপ annotate করেছি।
লিস্টিং ৮-৪ একটি vector-এর ভ্যালু অ্যাক্সেস করার উভয় পদ্ধতি দেখায়, indexing সিনট্যাক্স এবং get
মেথডসহ।
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {third}"); let third: Option<&i32> = v.get(2); match third { Some(third) => println!("The third element is {third}"), None => println!("There is no third element."), } }
এখানে কয়েকটি বিষয় লক্ষ্য করুন। আমরা তৃতীয় element পেতে index ভ্যালু 2
ব্যবহার করি কারণ vector শূন্য থেকে সংখ্যা দ্বারা index করা হয়। &
এবং []
ব্যবহার করে আমরা index-এর element-এর একটি রেফারেন্স পাই। যখন আমরা get
মেথডটি আর্গুমেন্ট হিসাবে index পাস করে ব্যবহার করি, তখন আমরা একটি Option<&T>
পাই যা আমরা match
-এর সাথে ব্যবহার করতে পারি।
Rust একটি element রেফারেন্স করার এই দুটি উপায় সরবরাহ করে যাতে আপনি বেছে নিতে পারেন যে প্রোগ্রামটি কীভাবে আচরণ করবে যখন আপনি বিদ্যমান element-এর পরিসরের বাইরের কোনো index ভ্যালু ব্যবহার করার চেষ্টা করবেন। উদাহরণস্বরূপ, ধরা যাক আমাদের পাঁচটি element-এর একটি vector আছে এবং আমরা প্রতিটি কৌশল ব্যবহার করে ১০০তম index-এর একটি element অ্যাক্সেস করার চেষ্টা করি, যেমনটি লিস্টিং ৮-৫ এ দেখানো হয়েছে।
fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }``` </Listing> যখন আমরা এই কোডটি চালাই, প্রথম `[]` মেথডটি প্রোগ্রামটিকে প্যানিক (panic) করাবে কারণ এটি একটি অস্তিত্বহীন element-কে রেফারেন্স করছে। এই পদ্ধতিটি তখনই সবচেয়ে ভালো যখন আপনি চান যে আপনার প্রোগ্রামটি ক্র্যাশ করুক যদি vector-এর শেষের বাইরে কোনো element অ্যাক্সেস করার চেষ্টা করা হয়। যখন `get` মেথডটিকে এমন একটি index পাস করা হয় যা vector-এর বাইরে, তখন এটি প্যানিক না করে `None` রিটার্ন করে। আপনি এই পদ্ধতিটি ব্যবহার করবেন যদি vector-এর পরিসরের বাইরের কোনো element অ্যাক্সেস করা সাধারণ পরিস্থিতিতে মাঝে মাঝে ঘটতে পারে। আপনার কোডে তখন `Some(&element)` বা `None` পরিচালনা করার জন্য লজিক থাকবে, যেমনটি অধ্যায় ৬-এ আলোচনা করা হয়েছে। উদাহরণস্বরূপ, indexটি কোনো ব্যক্তি দ্বারা একটি সংখ্যা ইনপুট করার মাধ্যমে আসতে পারে। যদি তারা ভুলবশত একটি খুব বড় সংখ্যা প্রবেশ করায় এবং প্রোগ্রামটি একটি `None` ভ্যালু পায়, আপনি ব্যবহারকারীকে বলতে পারেন বর্তমান vector-এ কতগুলি আইটেম আছে এবং তাদের একটি বৈধ ভ্যালু প্রবেশ করার আরেকটি সুযোগ দিতে পারেন। এটি একটি টাইপের ভুলের জন্য প্রোগ্রাম ক্র্যাশ করার চেয়ে বেশি ব্যবহারকারী-বান্ধব হবে! যখন প্রোগ্রামের একটি বৈধ রেফারেন্স থাকে, তখন borrow checker মালিকানা এবং ধার করার নিয়মগুলি (অধ্যায় ৪-এ আচ্ছাদিত) প্রয়োগ করে যাতে এই রেফারেন্স এবং vector-এর বিষয়বস্তুর অন্য কোনো রেফারেন্স বৈধ থাকে। সেই নিয়মটি স্মরণ করুন যা বলে যে আপনি একই স্কোপে mutable এবং immutable রেফারেন্স রাখতে পারবেন না। সেই নিয়মটি লিস্টিং ৮-৬-এ প্রযোজ্য, যেখানে আমরা একটি vector-এর প্রথম element-এর একটি immutable রেফারেন্স ধরে রাখি এবং শেষে একটি element যোগ করার চেষ্টা করি। এই প্রোগ্রামটি কাজ করবে না যদি আমরা ফাংশনের পরে সেই element-কে আবার রেফারেন্স করার চেষ্টা করি। <Listing number="8-6" caption="একটি আইটেমের রেফারেন্স ধরে রেখে একটি vector-এ একটি element যোগ করার চেষ্টা"> ```rust,ignore,does_not_compile fn main() { let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); println!("The first element is: {first}"); }
এই কোডটি কম্পাইল করলে এই error-টি আসবে:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ------- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
লিস্টিং ৮-৬ এর কোডটি দেখে মনে হতে পারে যে এটি কাজ করা উচিত: প্রথম element-এর একটি রেফারেন্স vector-এর শেষের পরিবর্তনে কেন পাত্তা দেবে? এই error-টি vector-এর কাজ করার পদ্ধতির কারণে হয়: কারণ vector মেমরিতে ভ্যালুগুলো একে অপরের পাশে রাখে, vector-এর শেষে একটি নতুন element যোগ করার জন্য নতুন মেমরি বরাদ্দ করার এবং পুরানো element-গুলো নতুন জায়গায় অনুলিপি করার প্রয়োজন হতে পারে, যদি vector-টি যেখানে বর্তমানে সংরক্ষিত আছে সেখানে সমস্ত element পাশাপাশি রাখার জন্য পর্যাপ্ত জায়গা না থাকে। সেক্ষেত্রে, প্রথম element-এর রেফারেন্সটি একটি ডিঅ্যালোকেটেড মেমরির দিকে নির্দেশ করবে। ধার করার নিয়মগুলি প্রোগ্রামগুলিকে সেই পরিস্থিতিতে পড়া থেকে বিরত রাখে।
দ্রষ্টব্য:
Vec<T>
টাইপের বাস্তবায়নের বিবরণ সম্পর্কে আরও জানতে, "The Rustonomicon" দেখুন।
Vector-এর ভ্যালুগুলোর উপর Iterate করা
একটি vector-এর প্রতিটি element-কে পর্যায়ক্রমে অ্যাক্সেস করার জন্য, আমরা একবারে একটি অ্যাক্সেস করার জন্য index ব্যবহার না করে সমস্ত element-এর মধ্যে দিয়ে iterate করব। লিস্টিং ৮-৭ দেখায় কীভাবে একটি for
লুপ ব্যবহার করে i32
ভ্যালুর একটি vector-এর প্রতিটি element-এর immutable রেফারেন্স পেতে এবং সেগুলি প্রিন্ট করতে হয়।
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } }
আমরা সমস্ত element-এ পরিবর্তন আনার জন্য একটি mutable vector-এর প্রতিটি element-এর mutable রেফারেন্সের উপরও iterate করতে পারি। লিস্টিং ৮-৮ এর for
লুপ প্রতিটি element-এর সাথে 50
যোগ করবে।
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
mutable রেফারেন্স যে ভ্যালুকে নির্দেশ করে তা পরিবর্তন করতে, আমাদের *
dereference operator ব্যবহার করতে হবে i
-এর ভ্যালুটি পাওয়ার জন্য, তারপর আমরা +=
operator ব্যবহার করতে পারি। আমরা dereference operator সম্পর্কে অধ্যায় ১৫-এর "Following the Reference to the Value" বিভাগে আরও কথা বলব।
একটি vector-এর উপর iterate করা, immutable হোক বা mutable, borrow checker-এর নিয়মের কারণে নিরাপদ। যদি আমরা লিস্টিং ৮-৭ এবং লিস্টিং ৮-৮ এর for
লুপের বডিতে আইটেম ঢোকানো বা সরানোর চেষ্টা করতাম, আমরা লিস্টিং ৮-৬ এর কোডের মতো একটি compiler error পেতাম। for
লুপ যে vector-এর রেফারেন্স ধরে রাখে তা পুরো vector-এর একযোগে পরিবর্তন প্রতিরোধ করে।
একাধিক Type স্টোর করার জন্য Enum ব্যবহার
Vector শুধুমাত্র একই টাইপের ভ্যালু স্টোর করতে পারে। এটি অসুবিধাজনক হতে পারে; বিভিন্ন টাইপের আইটেমের তালিকা স্টোর করার প্রয়োজন অবশ্যই আছে। সৌভাগ্যবশত, একটি enum-এর ভ্যারিয়েন্টগুলি একই enum টাইপের অধীনে সংজ্ঞায়িত করা হয়, তাই যখন আমাদের বিভিন্ন টাইপের element প্রতিনিধিত্ব করার জন্য একটি টাইপের প্রয়োজন হয়, তখন আমরা একটি enum সংজ্ঞায়িত এবং ব্যবহার করতে পারি!
উদাহরণস্বরূপ, ধরুন আমরা একটি স্প্রেডশীটের একটি সারি থেকে ভ্যালু পেতে চাই যেখানে সারির কিছু কলামে ইন্টিজার, কিছু ফ্লোটিং-পয়েন্ট নম্বর এবং কিছু স্ট্রিং রয়েছে। আমরা একটি enum সংজ্ঞায়িত করতে পারি যার ভ্যারিয়েন্টগুলি বিভিন্ন ভ্যালু টাইপ ধারণ করবে, এবং সমস্ত enum ভ্যারিয়েন্ট একই টাইপের বলে বিবেচিত হবে: সেই enum-এর টাইপ। তারপর আমরা সেই enum ধারণ করার জন্য একটি vector তৈরি করতে পারি এবং এইভাবে, অবশেষে, বিভিন্ন টাইপ ধারণ করতে পারি। আমরা এটি লিস্টিং ৮-৯-এ প্রদর্শন করেছি।
fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; }
Rust-কে compile time-এ জানতে হবে vector-এ কোন টাইপগুলো থাকবে যাতে এটি জানতে পারে প্রতিটি element স্টোর করার জন্য heap-এ ঠিক কতটা মেমরি লাগবে। এই vector-এ কোন টাইপগুলো অনুমোদিত সে সম্পর্কেও আমাদের সুস্পষ্ট হতে হবে। যদি Rust একটি vector-কে যেকোনো টাইপ ধারণ করার অনুমতি দিত, তাহলে একটি বা একাধিক টাইপ vector-এর element-গুলোর উপর সঞ্চালিত অপারেশনগুলির সাথে error ঘটাতে পারত। একটি enum এবং একটি match
এক্সপ্রেশন ব্যবহার করার অর্থ হল যে Rust compile time-এ নিশ্চিত করবে যে প্রতিটি সম্ভাব্য কেস পরিচালনা করা হয়েছে, যেমনটি অধ্যায় ৬-এ আলোচনা করা হয়েছে।
যদি আপনি না জানেন যে একটি প্রোগ্রাম রানটাইমে একটি vector-এ স্টোর করার জন্য কোন কোন টাইপ পাবে, তাহলে enum কৌশলটি কাজ করবে না। এর পরিবর্তে, আপনি একটি trait object ব্যবহার করতে পারেন, যা আমরা অধ্যায় ১৮-এ আলোচনা করব।
এখন যেহেতু আমরা vector ব্যবহারের কিছু সবচেয়ে সাধারণ উপায় নিয়ে আলোচনা করেছি, standard library দ্বারা Vec<T>
-তে সংজ্ঞায়িত সমস্ত দরকারি মেথডগুলির জন্য the API documentation পর্যালোচনা করতে ভুলবেন না। উদাহরণস্বরূপ, push
ছাড়াও, একটি pop
মেথড রয়েছে যা শেষ element-টি সরিয়ে দেয় এবং ফেরত দেয়।
Vector Drop হলে তার Element-গুলোও Drop হয়
অন্য যেকোনো struct
-এর মতো, একটি vector যখন স্কোপের বাইরে চলে যায় তখন তা মুক্ত হয়ে যায়, যেমনটি লিস্টিং ৮-১০-এ দেখানো হয়েছে।
fn main() { { let v = vec![1, 2, 3, 4]; // do stuff with v } // <- v goes out of scope and is freed here }
যখন vector-টি ড্রপ করা হয়, তখন তার সমস্ত বিষয়বস্তুও ড্রপ করা হয়, যার মানে হল এটি যে ইন্টিজারগুলি ধারণ করে সেগুলি পরিষ্কার করা হবে। borrow checker নিশ্চিত করে যে একটি vector-এর বিষয়বস্তুর যেকোনো রেফারেন্স শুধুমাত্র তখনই ব্যবহৃত হয় যখন vector নিজেই বৈধ থাকে।
চলুন পরবর্তী collection টাইপ-এ যাওয়া যাক: String
!
স্ট্রিং ব্যবহার করে UTF-8 এনকোডেড টেক্সট স্টোর করা
আমরা অধ্যায় ৪-এ স্ট্রিং নিয়ে আলোচনা করেছি, কিন্তু এখন আমরা এটি আরও গভীরভাবে দেখব। নতুন Rust ব্যবহারকারীরা সাধারণত তিনটি কারণে স্ট্রিং নিয়ে সমস্যায় পড়েন: সম্ভাব্য error তুলে ধরার ক্ষেত্রে Rust-এর প্রবণতা, স্ট্রিং ডেটা স্ট্রাকচারটি যতটা সহজ ভাবা হয় তার চেয়ে বেশি জটিল হওয়া, এবং UTF-8। এই কারণগুলো একত্রিত হয়ে এমন পরিস্থিতি তৈরি করে যা অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজ থেকে আসা ডেভেলপারদের জন্য কঠিন মনে হতে পারে।
আমরা collections-এর প্রেক্ষাপটে স্ট্রিং নিয়ে আলোচনা করছি কারণ স্ট্রিং মূলত বাইটের (bytes) একটি collection হিসাবে প্রয়োগ করা হয়, এবং এর সাথে কিছু অতিরিক্ত মেথড থাকে যা সেই বাইটগুলোকে টেক্সট হিসাবে ব্যাখ্যা করার সময় দরকারি কার্যকারিতা প্রদান করে। এই বিভাগে, আমরা String
-এর সেইসব অপারেশন নিয়ে কথা বলব যা প্রতিটি collection টাইপের মধ্যেই রয়েছে, যেমন তৈরি করা, আপডেট করা এবং পড়া। এছাড়াও আমরা আলোচনা করব String
অন্যান্য collection থেকে কোন কোন ক্ষেত্রে আলাদা, বিশেষ করে মানুষ এবং কম্পিউটার যেভাবে String
ডেটাকে ব্যাখ্যা করে তার পার্থক্যের কারণে String
-এ ইনডেক্সিং (indexing) করাটা বেশ জটিল।
স্ট্রিং কী?
প্রথমে আমরা স্ট্রিং বলতে কী বুঝি তা নির্ধারণ করব। Rust-এর কোর ল্যাঙ্গুয়েজে শুধুমাত্র একটি স্ট্রিং টাইপ আছে, যা হলো স্ট্রিং স্লাইস str
, এবং এটি সাধারণত এর ধার করা (borrowed) রূপ &str
-এ দেখা যায়। অধ্যায় ৪-এ, আমরা স্ট্রিং স্লাইস নিয়ে কথা বলেছিলাম, যা অন্য কোথাও স্টোর করা UTF-8 এনকোডেড স্ট্রিং ডেটার রেফারেন্স। উদাহরণস্বরূপ, স্ট্রিং লিটারেল (string literals) প্রোগ্রামের বাইনারিতে স্টোর করা হয় এবং তাই সেগুলি স্ট্রিং স্লাইস।
String
টাইপটি Rust-এর standard library দ্বারা সরবরাহ করা হয়, এটি কোর ল্যাঙ্গুয়েজে কোড করা নেই। এটি একটি পরিবর্তনশীল (growable), পরিবর্তনযোগ্য (mutable), নিজস্ব (owned), এবং UTF-8 এনকোডেড স্ট্রিং টাইপ। যখন Rust ব্যবহারকারীরা "স্ট্রিং" বলেন, তখন তারা String
বা স্ট্রিং স্লাইস &str
উভয়কেই বোঝাতে পারেন, শুধু একটিকে নয়। যদিও এই বিভাগটি মূলত String
সম্পর্কিত, উভয় টাইপই Rust-এর standard library-তে ব্যাপকভাবে ব্যবহৃত হয় এবং String
ও স্ট্রিং স্লাইস উভয়ই UTF-8 এনকোডেড।
নতুন স্ট্রিং তৈরি করা
Vec<T>
-এর সাথে উপলব্ধ অনেক অপারেশন String
-এর সাথেও উপলব্ধ, কারণ String
আসলে বাইটের একটি vector-এর উপর একটি র্যাপার (wrapper) হিসাবে প্রয়োগ করা হয়েছে, যাতে কিছু অতিরিক্ত গ্যারান্টি, সীমাবদ্ধতা এবং ক্ষমতা রয়েছে। Vec<T>
এবং String
-এর সাথে একইভাবে কাজ করে এমন একটি ফাংশনের উদাহরণ হলো new
ফাংশন, যা একটি ইনস্ট্যান্স তৈরি করতে ব্যবহৃত হয়, যেমনটি লিস্টিং ৮-১১-তে দেখানো হয়েছে।
fn main() { let mut s = String::new(); }
এই লাইনটি s
নামে একটি নতুন, খালি স্ট্রিং তৈরি করে, যেখানে আমরা পরে ডেটা লোড করতে পারব। প্রায়শই, আমাদের কাছে কিছু প্রাথমিক ডেটা থাকে যা দিয়ে আমরা স্ট্রিং শুরু করতে চাই। এর জন্য, আমরা to_string
মেথড ব্যবহার করি, যা Display
trait প্রয়োগকারী যেকোনো টাইপের উপর উপলব্ধ, যেমন স্ট্রিং লিটারেল। লিস্টিং ৮-১২ দুটি উদাহরণ দেখায়।
fn main() { let data = "initial contents"; let s = data.to_string(); // The method also works on a literal directly: let s = "initial contents".to_string(); }
এই কোডটি initial contents
লেখা সহ একটি স্ট্রিং তৈরি করে।
আমরা String::from
ফাংশন ব্যবহার করেও একটি স্ট্রিং লিটারেল থেকে String
তৈরি করতে পারি। লিস্টিং ৮-১৩-এর কোডটি লিস্টিং ৮-১২-এর কোডের সমতুল্য যা to_string
ব্যবহার করে।
fn main() { let s = String::from("initial contents"); }
যেহেতু স্ট্রিং অনেক কিছুর জন্য ব্যবহৃত হয়, আমরা স্ট্রিং-এর জন্য বিভিন্ন জেনেরিক API ব্যবহার করতে পারি, যা আমাদের অনেক বিকল্প সরবরাহ করে। তাদের মধ্যে কিছু অপ্রয়োজনীয় মনে হতে পারে, কিন্তু সবগুলোরই নিজস্ব স্থান আছে! এক্ষেত্রে, String::from
এবং to_string
একই কাজ করে, তাই আপনি কোনটি বেছে নেবেন তা আপনার স্টাইল এবং পঠনযোগ্যতার উপর নির্ভর করে।
মনে রাখবেন যে স্ট্রিংগুলো UTF-8 এনকোডেড, তাই আমরা সেগুলিতে যেকোনো সঠিকভাবে এনকোড করা ডেটা অন্তর্ভুক্ত করতে পারি, যেমনটি লিস্টিং ৮-১৪-তে দেখানো হয়েছে।
fn main() { let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שלום"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
এগুলো সবই বৈধ String
ভ্যালু।
একটি স্ট্রিং আপডেট করা
একটি String
-এর আকার বাড়তে পারে এবং এর বিষয়বস্তু পরিবর্তন হতে পারে, যেমন Vec<T>
-এর বিষয়বস্তু পরিবর্তন করা যায়, যদি আপনি এতে আরও ডেটা পুশ করেন। এছাড়াও, আপনি সুবিধামত +
অপারেটর বা format!
ম্যাক্রো ব্যবহার করে String
ভ্যালু সংযুক্ত (concatenate) করতে পারেন।
push_str
এবং push
দিয়ে একটি স্ট্রিং-এ যোগ করা
আমরা push_str
মেথড ব্যবহার করে একটি স্ট্রিং স্লাইস যোগ করে একটি String
বড় করতে পারি, যেমনটি লিস্টিং ৮-১৫-তে দেখানো হয়েছে।
fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
এই দুটি লাইনের পরে, s
-এ foobar
থাকবে। push_str
মেথডটি একটি স্ট্রিং স্লাইস নেয় কারণ আমরা প্যারামিটারের মালিকানা (ownership) নিতে চাই না। উদাহরণস্বরূপ, লিস্টিং ৮-১৬-এর কোডে, আমরা s1
-এ s2
-এর বিষয়বস্তু যোগ করার পরেও s2
ব্যবহার করতে চাই।
fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {s2}"); }
যদি push_str
মেথডটি s2
-এর মালিকানা নিয়ে নিত, আমরা শেষ লাইনে এর ভ্যালু প্রিন্ট করতে পারতাম না। কিন্তু এই কোডটি আমাদের প্রত্যাশা অনুযায়ী কাজ করে!
push
মেথডটি একটি একক ক্যারেক্টার (character) প্যারামিটার হিসাবে নেয় এবং এটি String
-এ যোগ করে। লিস্টিং ৮-১৭ push
মেথড ব্যবহার করে একটি String
-এ l অক্ষরটি যোগ করে।
fn main() { let mut s = String::from("lo"); s.push('l'); }
এর ফলে, s
-এ lol
থাকবে।
+
অপারেটর বা format!
ম্যাক্রো দিয়ে Concatenation
প্রায়শই, আপনি দুটি বিদ্যমান স্ট্রিং একত্রিত করতে চাইবেন। একটি উপায় হলো +
অপারেটর ব্যবহার করা, যেমনটি লিস্টিং ৮-১৮-তে দেখানো হয়েছে।
fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used }
s3
স্ট্রিংটিতে Hello, world!
থাকবে। s1
যোগ করার পরে আর বৈধ থাকে না এবং আমরা s2
-এর একটি রেফারেন্স ব্যবহার করেছি, এর কারণটি সেই মেথডের সিগনেচারের সাথে সম্পর্কিত যা +
অপারেটর ব্যবহার করার সময় কল করা হয়। +
অপারেটরটি add
মেথড ব্যবহার করে, যার সিগনেচারটি প্রায় এরকম:
fn add(self, s: &str) -> String {
Standard library-তে, আপনি add
মেথডটি জেনেরিক এবং অ্যাসোসিয়েটেড টাইপ ব্যবহার করে সংজ্ঞায়িত দেখতে পাবেন। এখানে, আমরা সুনির্দিষ্ট টাইপ ব্যবহার করেছি, যা ঘটে যখন আমরা String
ভ্যালু দিয়ে এই মেথডটি কল করি। আমরা অধ্যায় ১০-এ জেনেরিক নিয়ে আলোচনা করব। এই সিগনেচারটি আমাদের +
অপারেটরের জটিল অংশগুলো বোঝার জন্য প্রয়োজনীয় সূত্র দেয়।
প্রথমত, s2
-এর একটি &
আছে, যার মানে আমরা প্রথম স্ট্রিং-এর সাথে দ্বিতীয় স্ট্রিং-এর একটি রেফারেন্স যোগ করছি। এটি add
ফাংশনের s
প্যারামিটারের কারণে: আমরা শুধুমাত্র একটি &str
একটি String
-এ যোগ করতে পারি; আমরা দুটি String
ভ্যালু একসাথে যোগ করতে পারি না। কিন্তু অপেক্ষা করুন—&s2
-এর টাইপ হলো &String
, &str
নয়, যেমনটি add
-এর দ্বিতীয় প্যারামিটারে নির্দিষ্ট করা আছে। তাহলে লিস্টিং ৮-১৮ কেন কম্পাইল হয়?
add
কলে &s2
ব্যবহার করতে পারার কারণ হলো কম্পাইলার &String
আর্গুমেন্টটিকে একটি &str
-এ coerce (রূপান্তর) করতে পারে। যখন আমরা add
মেথডটি কল করি, Rust একটি deref coercion ব্যবহার করে, যা এখানে &s2
-কে &s2[..]
-তে পরিণত করে। আমরা অধ্যায় ১৫-এ deref coercion নিয়ে আরও বিস্তারিত আলোচনা করব। যেহেতু add
s
প্যারামিটারের মালিকানা নেয় না, তাই এই অপারেশনের পরেও s2
একটি বৈধ String
থাকবে।
দ্বিতীয়ত, আমরা সিগনেচারে দেখতে পাচ্ছি যে add
self
-এর মালিকানা নেয় কারণ self
-এর আগে &
নেই। এর মানে লিস্টিং ৮-১৮-এর s1
add
কলের মধ্যে মুভ (move) হয়ে যাবে এবং তারপরে আর বৈধ থাকবে না। সুতরাং, যদিও let s3 = s1 + &s2;
দেখে মনে হচ্ছে এটি উভয় স্ট্রিং কপি করে একটি নতুন তৈরি করবে, এই স্টেটমেন্টটি আসলে s1
-এর মালিকানা নেয়, s2
-এর বিষয়বস্তুর একটি কপি যোগ করে এবং তারপর ফলাফলের মালিকানা ফেরত দেয়। অন্য কথায়, এটি দেখতে অনেক কপি করার মতো মনে হলেও, এর বাস্তবায়ন কপি করার চেয়ে অনেক বেশি কার্যকর।
যদি আমাদের একাধিক স্ট্রিং যুক্ত করতে হয়, তাহলে +
অপারেটরের ব্যবহার বেশ громоздким (unwieldy) হয়ে যায়:
fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3; }
এই মুহুর্তে, s
হবে tic-tac-toe
। এতগুলো +
এবং "
অক্ষরের কারণে কী ঘটছে তা বোঝা কঠিন। আরও জটিল উপায়ে স্ট্রিং একত্রিত করার জন্য, আমরা এর পরিবর্তে format!
ম্যাক্রো ব্যবহার করতে পারি:
fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{s1}-{s2}-{s3}"); }
এই কোডটি s
-কে tic-tac-toe
-তে সেট করে। format!
ম্যাক্রো println!
-এর মতো কাজ করে, কিন্তু আউটপুট স্ক্রিনে প্রিন্ট করার পরিবর্তে, এটি বিষয়বস্তুসহ একটি String
ফেরত দেয়। format!
ব্যবহার করা কোডের সংস্করণটি পড়া অনেক সহজ, এবং format!
ম্যাক্রো দ্বারা তৈরি কোড রেফারেন্স ব্যবহার করে যাতে এই কলটি তার কোনো প্যারামিটারের মালিকানা না নেয়।
স্ট্রিং-এ ইনডেক্সিং
অন্যান্য অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজে, ইনডেক্স দ্বারা একটি স্ট্রিং-এর স্বতন্ত্র অক্ষর অ্যাক্সেস করা একটি বৈধ এবং সাধারণ অপারেশন। যাইহোক, আপনি যদি Rust-এ ইনডেক্সিং সিনট্যাক্স ব্যবহার করে একটি String
-এর অংশ অ্যাক্সেস করার চেষ্টা করেন, আপনি একটি এরর পাবেন। লিস্টিং ৮-১৯-এর অবৈধ কোডটি বিবেচনা করুন।
fn main() {
let s1 = String::from("hi");
let h = s1[0];
}
এই কোডটি নিম্নলিখিত এরর দেবে:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:16
|
3 | let h = s1[0];
| ^ string indices are ranges of `usize`
|
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
but trait `SliceIndex<[_]>` is implemented for `usize`
= help: for that trait implementation, expected `[_]`, found `str`
= note: required for `String` to implement `Index<{integer}>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error
এরর এবং নোটটি পুরো গল্পটি বলে দেয়: Rust স্ট্রিং ইনডেক্সিং সমর্থন করে না। কিন্তু কেন নয়? এই প্রশ্নের উত্তর দিতে, আমাদের আলোচনা করতে হবে Rust কীভাবে মেমরিতে স্ট্রিং স্টোর করে।
অভ্যন্তরীণ উপস্থাপনা (Internal Representation)
একটি String
হলো Vec<u8>
-এর উপর একটি র্যাপার। আসুন লিস্টিং ৮-১৪ থেকে আমাদের সঠিকভাবে এনকোড করা UTF-8 উদাহরণ স্ট্রিংগুলির কিছু দেখি। প্রথমত, এটি:
fn main() { let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שלום"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
এই ক্ষেত্রে, len
হবে 4
, যার মানে "Hola"
স্ট্রিংটি স্টোর করা ভেক্টরটি ৪ বাইট দীর্ঘ। UTF-8-এ এনকোড করার সময় এই অক্ষরগুলির প্রতিটি এক বাইট করে জায়গা নেয়। তবে, নিম্নলিখিত লাইনটি আপনাকে অবাক করতে পারে (লক্ষ্য করুন যে এই স্ট্রিংটি ক্যাপিটাল সিরিলিক অক্ষর Ze দিয়ে শুরু হয়, সংখ্যা 3 দিয়ে নয়):
fn main() { let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שלום"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
আপনাকে যদি জিজ্ঞাসা করা হয় স্ট্রিংটি কত দীর্ঘ, আপনি হয়তো বলবেন ১২। আসলে, Rust-এর উত্তর হলো ২৪: এটি "Здравствуйте" শব্দটি UTF-8-এ এনকোড করতে প্রয়োজনীয় বাইটের সংখ্যা, কারণ সেই স্ট্রিং-এর প্রতিটি ইউনিকোড স্কেলার ভ্যালু ২ বাইট স্টোরেজ নেয়। অতএব, স্ট্রিং-এর বাইটগুলিতে একটি ইনডেক্স সবসময় একটি বৈধ ইউনিকোড স্কেলার ভ্যালুর সাথে মিলবে না। এটি দেখানোর জন্য, এই অবৈধ Rust কোডটি বিবেচনা করুন:
let hello = "Здравствуйте";
let answer = &hello[0];
আপনি ಈಗಾಗಲೇ জানেন যে answer
З
হবে না, যা প্রথম অক্ষর। UTF-8-এ এনকোড করা হলে, З
-এর প্রথম বাইট হলো 208
এবং দ্বিতীয়টি হলো 151
, তাই মনে হতে পারে যে answer
আসলে 208
হওয়া উচিত, কিন্তু 208
নিজে থেকে একটি বৈধ অক্ষর নয়। যদি কেউ এই স্ট্রিংয়ের প্রথম অক্ষরের জন্য জিজ্ঞাসা করে তবে 208
ফেরত দেওয়া সম্ভবত ব্যবহারকারীর কাঙ্ক্ষিত হবে না; তবে, বাইট ইনডেক্স ০-তে Rust-এর কাছে কেবল এই ডেটাই আছে। ব্যবহারকারীরা সাধারণত বাইট ভ্যালু ফেরত চান না, এমনকি যদি স্ট্রিংটিতে শুধুমাত্র ল্যাটিন অক্ষর থাকে: যদি &"hi"[0]
বৈধ কোড হতো যা বাইট ভ্যালু ফেরত দিত, তবে এটি h
-এর পরিবর্তে 104
ফেরত দিত।
উত্তরটি হলো, একটি অপ্রত্যাশিত মান ফেরত দেওয়া এবং এমন বাগ তৈরি করা এড়াতে যা অবিলম্বে আবিষ্কৃত নাও হতে পারে, Rust এই কোডটি মোটেই কম্পাইল করে না এবং ডেভেলপমেন্ট প্রক্রিয়ার শুরুতেই ভুল বোঝাবুঝি প্রতিরোধ করে।
বাইট, স্কেলার ভ্যালু এবং গ্রাফিম ক্লাস্টার! এলাহি কাণ্ড!
UTF-8 সম্পর্কে আরেকটি বিষয় হলো যে Rust-এর দৃষ্টিকোণ থেকে স্ট্রিং দেখার তিনটি প্রাসঙ্গিক উপায় রয়েছে: বাইট হিসাবে, স্কেলার ভ্যালু হিসাবে, এবং গ্রাফিম ক্লাস্টার হিসাবে (যাকে আমরা অক্ষর বলি তার সবচেয়ে কাছের জিনিস)।
যদি আমরা দেবনাগরী লিপিতে লেখা হিন্দি শব্দ "नमस्ते" দেখি, এটি u8
ভ্যালুর একটি ভেক্টর হিসাবে স্টোর করা হয় যা দেখতে এইরকম:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
এটি ১৮ বাইট এবং কম্পিউটারগুলি এভাবেই ডেটা সঞ্চয় করে। যদি আমরা এগুলিকে ইউনিকোড স্কেলার ভ্যালু হিসাবে দেখি, যা Rust-এর char
টাইপ, তবে সেই বাইটগুলি দেখতে এইরকম:
['न', 'म', 'स', '्', 'त', 'े']
এখানে ছয়টি char
ভ্যালু রয়েছে, কিন্তু চতুর্থ এবং ষষ্ঠটি অক্ষর নয়: সেগুলি ডায়াক্রিটিক যা নিজে থেকে অর্থপূর্ণ নয়। অবশেষে, যদি আমরা এগুলিকে গ্রাফিম ক্লাস্টার হিসাবে দেখি, তবে আমরা সেই চারটি অক্ষর পাব যা একজন ব্যক্তি হিন্দি শব্দটি তৈরি করতে ব্যবহার করবে:
["न", "म", "स्", "ते"]
Rust কম্পিউটার দ্বারা সংরক্ষিত কাঁচা স্ট্রিং ডেটা ব্যাখ্যা করার বিভিন্ন উপায় সরবরাহ করে যাতে প্রতিটি প্রোগ্রাম তার প্রয়োজনীয় ব্যাখ্যা বেছে নিতে পারে, ডেটা যে কোনো মানব ভাষায়ই হোক না কেন।
Rust আমাদের একটি অক্ষর পেতে একটি String
-এ ইনডেক্স করার অনুমতি না দেওয়ার একটি চূড়ান্ত কারণ হলো যে ইনডেক্সিং অপারেশনগুলি সর্বদা ধ্রুবক সময়ে (O(1)) সম্পন্ন হবে বলে আশা করা হয়। কিন্তু একটি String
-এর সাথে সেই পারফরম্যান্সের গ্যারান্টি দেওয়া সম্ভব নয়, কারণ Rust-কে শুরু থেকে ইনডেক্স পর্যন্ত বিষয়বস্তুর মধ্যে দিয়ে হেঁটে যেতে হবে কতগুলি বৈধ অক্ষর ছিল তা নির্ধারণ করার জন্য।
স্ট্রিং স্লাইস করা
একটি স্ট্রিং-এ ইনডেক্স করা প্রায়শই একটি খারাপ ধারণা কারণ এটি স্পষ্ট নয় যে স্ট্রিং-ইনডেক্সিং অপারেশনের রিটার্ন টাইপ কী হওয়া উচিত: একটি বাইট ভ্যালু, একটি অক্ষর, একটি গ্রাফিম ক্লাস্টার, বা একটি স্ট্রিং স্লাইস। অতএব, যদি আপনার সত্যিই স্ট্রিং স্লাইস তৈরি করার জন্য ইনডেক্স ব্যবহার করার প্রয়োজন হয়, Rust আপনাকে আরও নির্দিষ্ট হতে বলে।
একটি একক সংখ্যা দিয়ে []
ব্যবহার করে ইনডেক্স করার পরিবর্তে, আপনি নির্দিষ্ট বাইট ধারণকারী একটি স্ট্রিং স্লাইস তৈরি করতে একটি পরিসীমা (range) সহ []
ব্যবহার করতে পারেন:
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4]; }
এখানে, s
একটি &str
হবে যা স্ট্রিং-এর প্রথম চারটি বাইট ধারণ করে। আগে, আমরা উল্লেখ করেছি যে এই অক্ষরগুলির প্রতিটি দুই বাইট করে ছিল, যার মানে s
হবে Зд
।
যদি আমরা &hello[0..1]
-এর মতো কিছু দিয়ে একটি অক্ষরের বাইটের কেবল একটি অংশ স্লাইস করার চেষ্টা করতাম, তাহলে Rust রানটাইমে প্যানিক করত, যেমন একটি ভেক্টরে একটি অবৈধ ইনডেক্স অ্যাক্সেস করা হলে হয়:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
রেঞ্জ ব্যবহার করে স্ট্রিং স্লাইস তৈরি করার সময় আপনার সতর্কতা অবলম্বন করা উচিত, কারণ এটি আপনার প্রোগ্রাম ক্র্যাশ করতে পারে।
স্ট্রিং-এর উপর ইটারেট করার মেথড
স্ট্রিং-এর অংশে কাজ করার সেরা উপায় হলো আপনি অক্ষর চান নাকি বাইট চান সে সম্পর্কে স্পষ্ট হওয়া। স্বতন্ত্র ইউনিকোড স্কেলার ভ্যালুর জন্য, chars
মেথড ব্যবহার করুন। "Зд"-এর উপর chars
কল করা হলে এটি দুটি char
টাইপের ভ্যালু আলাদা করে ফেরত দেয়, এবং আপনি প্রতিটি এলিমেন্ট অ্যাক্সেস করতে ফলাফলের উপর ইটারেট করতে পারেন:
#![allow(unused)] fn main() { for c in "Зд".chars() { println!("{c}"); } }
এই কোডটি নিম্নলিখিত আউটপুট প্রিন্ট করবে:
З
д
বিকল্পভাবে, bytes
মেথড প্রতিটি কাঁচা বাইট ফেরত দেয়, যা আপনার ডোমেনের জন্য উপযুক্ত হতে পারে:
#![allow(unused)] fn main() { for b in "Зд".bytes() { println!("{b}"); } }
এই কোডটি এই স্ট্রিংটি গঠনকারী চারটি বাইট প্রিন্ট করবে:
208
151
208
180
তবে মনে রাখতে ভুলবেন না যে বৈধ ইউনিকোড স্কেলার ভ্যালু একাধিক বাইট দিয়ে গঠিত হতে পারে।
দেবনাগরী লিপির মতো স্ট্রিং থেকে গ্রাফিম ক্লাস্টার পাওয়া জটিল, তাই এই কার্যকারিতা standard library দ্বারা সরবরাহ করা হয় না। যদি আপনার এই কার্যকারিতার প্রয়োজন হয়, তাহলে crates.io-তে ক্রেট উপলব্ধ আছে।
স্ট্রিং অতটা সহজ নয়
সংক্ষেপে, স্ট্রিং বেশ জটিল। বিভিন্ন প্রোগ্রামিং ল্যাঙ্গুয়েজ প্রোগ্রামারদের কাছে এই জটিলতা কীভাবে উপস্থাপন করা হবে সে সম্পর্কে বিভিন্ন সিদ্ধান্ত নেয়। Rust String
ডেটার সঠিক হ্যান্ডলিং-কে সমস্ত Rust প্রোগ্রামের জন্য ডিফল্ট আচরণ হিসাবে বেছে নিয়েছে, যার মানে প্রোগ্রামারদের UTF-8 ডেটা হ্যান্ডলিং নিয়ে আগে থেকেই আরও বেশি ভাবতে হবে। এই ট্রেড-অফটি অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজে দৃশ্যমান স্ট্রিং-এর জটিলতার চেয়ে বেশি প্রকাশ করে, তবে এটি আপনাকে আপনার ডেভেলপমেন্ট লাইফ সাইকেলের পরবর্তী পর্যায়ে নন-ASCII অক্ষর সম্পর্কিত এরর হ্যান্ডলিং করা থেকে বিরত রাখে।
সুখবর হলো, standard library String
এবং &str
টাইপের উপর ভিত্তি করে অনেক কার্যকারিতা সরবরাহ করে যা এই জটিল পরিস্থিতিগুলি সঠিকভাবে পরিচালনা করতে সহায়তা করে। স্ট্রিং-এ অনুসন্ধানের জন্য contains
এবং স্ট্রিং-এর অংশ অন্য স্ট্রিং দিয়ে প্রতিস্থাপনের জন্য replace
-এর মতো দরকারি মেথডগুলির জন্য ডকুমেন্টেশন দেখতে ভুলবেন না।
চলুন এবার একটু কম জটিল কিছুতে যাওয়া যাক: hash maps
Hash Map ব্যবহার করে Key এবং সংশ্লিষ্ট Value স্টোর করা
আমাদের সাধারণ collection-গুলোর মধ্যে সর্বশেষটি হলো hash map। HashMap<K, V>
টাইপটি K
টাইপের key-এর সাথে V
টাইপের value-এর একটি ম্যাপিং সংরক্ষণ করে। এটি একটি hashing function ব্যবহার করে নির্ধারণ করে কীভাবে মেমরিতে এই key এবং value-গুলো রাখা হবে। অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজ এই ধরনের ডেটা স্ট্রাকচার সমর্থন করে, কিন্তু তারা প্রায়শই ভিন্ন নাম ব্যবহার করে, যেমন hash, map, object, hash table, dictionary, বা associative array ইত্যাদি।
Hash map তখন দরকারী যখন আপনি কোনো index ব্যবহার করে ডেটা খুঁজতে চান না (যেমনটা vector-এর ক্ষেত্রে করা হয়), বরং একটি key ব্যবহার করে ডেটা খুঁজতে চান যা যেকোনো টাইপের হতে পারে। উদাহরণস্বরূপ, একটি গেমে, আপনি প্রতিটি দলের স্কোর একটি hash map-এ রাখতে পারেন, যেখানে প্রতিটি key হলো দলের নাম এবং value হলো সেই দলের স্কোর। একটি দলের নাম দিয়ে আপনি তার স্কোর পুনরুদ্ধার করতে পারবেন।
এই বিভাগে আমরা hash map-এর বেসিক API নিয়ে আলোচনা করব, কিন্তু standard library দ্বারা HashMap<K, V>
-তে সংজ্ঞায়িত ফাংশনগুলিতে আরও অনেক সুবিধা লুকিয়ে আছে। বরাবরের মতো, আরও তথ্যের জন্য standard library-এর ডকুমেন্টেশন দেখুন।
নতুন Hash Map তৈরি করা
একটি খালি hash map তৈরি করার একটি উপায় হলো new
ব্যবহার করা এবং insert
দিয়ে element যোগ করা। লিস্টিং ৮-২০-এ, আমরা দুটি দলের স্কোর ট্র্যাক করছি যাদের নাম Blue এবং Yellow। Blue দলের স্কোর ১০ দিয়ে শুরু হয় এবং Yellow দলের ৫০ দিয়ে।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); }
লক্ষ্য করুন যে আমাদের প্রথমে standard library-এর collections অংশ থেকে HashMap
use
করতে হবে। আমাদের তিনটি সাধারণ collection-এর মধ্যে এটি সবচেয়ে কম ব্যবহৃত হয়, তাই এটি prelude-এ স্বয়ংক্রিয়ভাবে স্কোপে আনা ফিচারগুলোর অন্তর্ভুক্ত নয়। Hash map-এর জন্য standard library থেকে কম সমর্থনও রয়েছে; উদাহরণস্বরূপ, এটি তৈরি করার জন্য কোনো বিল্ট-ইন ম্যাক্রো নেই।
Vector-এর মতোই, hash map তাদের ডেটা heap-এ স্টোর করে। এই HashMap
-এর key-গুলো String
টাইপের এবং value-গুলো i32
টাইপের। Vector-এর মতোই, hash map-ও সমজাতীয় (homogeneous): সমস্ত key-এর টাইপ একই হতে হবে এবং সমস্ত value-এর টাইপও একই হতে হবে।
Hash Map-এর Value অ্যাক্সেস করা
আমরা hash map থেকে একটি value পেতে পারি তার key get
মেথডে সরবরাহ করে, যেমনটি লিস্টিং ৮-২১-এ দেখানো হয়েছে।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name).copied().unwrap_or(0); }
এখানে, score
-এর ভ্যালু হবে Blue দলের সাথে যুক্ত ভ্যালুটি, এবং ফলাফল হবে 10
। get
মেথডটি একটি Option<&V>
রিটার্ন করে; যদি hash map-এ সেই key-এর জন্য কোনো ভ্যালু না থাকে, get
None
রিটার্ন করবে। এই প্রোগ্রামটি Option
-কে copied
কল করে একটি Option<i32>
পায় (Option<&i32>
-এর পরিবর্তে), তারপর unwrap_or
ব্যবহার করে score
-কে শূন্যতে সেট করে যদি scores
-এ key-টির জন্য কোনো এন্ট্রি না থাকে।
আমরা একটি for
লুপ ব্যবহার করে hash map-এর প্রতিটি key-value পেয়ারের উপর দিয়ে ইটারেট করতে পারি, যেমনটা আমরা vector-এর সাথে করি:
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{key}: {value}"); } }
এই কোডটি প্রতিটি জোড়া একটি অনির্দিষ্ট ক্রমে প্রিন্ট করবে:
Yellow: 50
Blue: 10
Hash Map এবং Ownership
যেসব টাইপ Copy
trait ইমপ্লিমেন্ট করে, যেমন i32
, সেগুলোর ভ্যালু hash map-এ কপি হয়। String
-এর মতো owned ভ্যালুর ক্ষেত্রে, ভ্যালুগুলো মুভ (move) হবে এবং hash map সেই ভ্যালুগুলোর মালিক হবে, যেমনটি লিস্টিং ৮-২২-এ দেখানো হয়েছে।
fn main() { use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name and field_value are invalid at this point, try using them and // see what compiler error you get! }
insert
কলের মাধ্যমে field_name
এবং field_value
ভ্যারিয়েবলগুলো hash map-এ মুভ হয়ে যাওয়ার পরে আমরা আর সেগুলো ব্যবহার করতে পারি না।
যদি আমরা hash map-এ ভ্যালুর রেফারেন্স যুক্ত করি, তবে ভ্যালুগুলো hash map-এ মুভ হবে না। রেফারেন্সগুলো যে ভ্যালুকে নির্দেশ করে, সেই ভ্যালুগুলো অন্তত hash map যতদিন বৈধ থাকবে, ততদিন বৈধ থাকতে হবে। আমরা এই বিষয়গুলো নিয়ে অধ্যায় ১০-এর “Validating References with Lifetimes” বিভাগে আরও আলোচনা করব।
একটি Hash Map আপডেট করা
যদিও key-value পেয়ারের সংখ্যা বাড়ানো যায়, প্রতিটি স্বতন্ত্র key-এর সাথে একবারে কেবল একটিই value যুক্ত থাকতে পারে (কিন্তু এর বিপরীতটি সত্য নয়: উদাহরণস্বরূপ, Blue এবং Yellow উভয় দলেরই scores
hash map-এ 10
ভ্যালুটি থাকতে পারে)।
যখন আপনি একটি hash map-এর ডেটা পরিবর্তন করতে চান, তখন আপনাকে সিদ্ধান্ত নিতে হবে যে একটি key-তে যখন আগে থেকেই একটি value বরাদ্দ থাকে তখন কী করবেন। আপনি পুরানো ভ্যালুটিকে সম্পূর্ণ উপেক্ষা করে নতুন ভ্যালু দিয়ে প্রতিস্থাপন করতে পারেন। আপনি পুরানো ভ্যালুটি রেখে নতুন ভ্যালুটি উপেক্ষা করতে পারেন, এবং শুধুমাত্র যদি key-টির কোনো ভ্যালু না থাকে তবেই নতুন ভ্যালু যোগ করতে পারেন। অথবা আপনি পুরানো এবং নতুন ভ্যালু একত্রিত করতে পারেন। আসুন দেখি কীভাবে এর প্রতিটি করা যায়!
একটি ভ্যালু ওভাররাইট করা
যদি আমরা একটি hash map-এ একটি key এবং একটি value যুক্ত করি এবং তারপরে একই key দিয়ে ভিন্ন একটি value যুক্ত করি, তবে সেই key-এর সাথে যুক্ত value প্রতিস্থাপিত হবে। যদিও লিস্টিং ৮-২৩-এর কোডটি দুবার insert
কল করে, hash map-এ কেবল একটি key-value পেয়ার থাকবে কারণ আমরা উভয়বারই Blue দলের key-এর জন্য value যুক্ত করছি।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{scores:?}"); }
এই কোডটি {"Blue": 25}
প্রিন্ট করবে। আসল 10
ভ্যালুটি ওভাররাইট করা হয়েছে।
শুধুমাত্র Key উপস্থিত না থাকলে একটি Key এবং Value যোগ করা
একটি সাধারণ কাজ হলো hash map-এ একটি নির্দিষ্ট key-এর জন্য কোনো value আছে কিনা তা পরীক্ষা করা এবং তারপরে নিম্নলিখিত পদক্ষেপ নেওয়া: যদি key-টি hash map-এ থাকে, তবে বিদ্যমান value অপরিবর্তিত থাকবে; যদি key-টি না থাকে, তবে এটি এবং এর জন্য একটি value যুক্ত করা হবে।
Hash map-এর এর জন্য একটি বিশেষ API আছে যার নাম entry
, যা প্যারামিটার হিসাবে আপনি যে key পরীক্ষা করতে চান তা নেয়। entry
মেথডের রিটার্ন ভ্যালু হলো Entry
নামের একটি enum, যা এমন একটি ভ্যালুকে প্রতিনিধিত্ব করে যা থাকতেও পারে বা নাও থাকতে পারে। ধরা যাক আমরা পরীক্ষা করতে চাই Yellow দলের key-এর সাথে কোনো value যুক্ত আছে কিনা। যদি না থাকে, আমরা 50
ভ্যালুটি যুক্ত করতে চাই, এবং Blue দলের জন্যও একই কাজ করতে চাই। entry
API ব্যবহার করে কোডটি লিস্টিং ৮-২৪-এর মতো দেখায়।
fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.entry(String::from("Yellow")).or_insert(50); scores.entry(String::from("Blue")).or_insert(50); println!("{scores:?}"); }
Entry
-এর উপর or_insert
মেথডটি এমনভাবে সংজ্ঞায়িত করা হয়েছে যে এটি সংশ্লিষ্ট Entry
key-এর ভ্যালুর একটি mutable রেফারেন্স রিটার্ন করে যদি সেই key বিদ্যমান থাকে, এবং যদি না থাকে, তবে এটি প্যারামিটারটিকে এই key-এর নতুন ভ্যালু হিসাবে যুক্ত করে এবং নতুন ভ্যালুর একটি mutable রেফারেন্স রিটার্ন করে। এই কৌশলটি নিজেরা লজিক লেখার চেয়ে অনেক পরিষ্কার এবং borrow checker-এর সাথে আরও ভালোভাবে কাজ করে।
লিস্টিং ৮-২৪-এর কোডটি চালালে {"Yellow": 50, "Blue": 10}
প্রিন্ট হবে। প্রথম entry
কলটি Yellow দলের key 50
ভ্যালুসহ যুক্ত করবে কারণ Yellow দলের আগে থেকে কোনো ভ্যালু নেই। দ্বিতীয় entry
কলটি hash map পরিবর্তন করবে না কারণ Blue দলের আগে থেকেই 10
ভ্যালুটি রয়েছে।
পুরানো ভ্যালুর উপর ভিত্তি করে একটি ভ্যালু আপডেট করা
Hash map-এর আরেকটি সাধারণ ব্যবহার হলো একটি key-এর ভ্যালু খুঁজে বের করা এবং তারপর পুরানো ভ্যালুর উপর ভিত্তি করে এটি আপডেট করা। উদাহরণস্বরূপ, লিস্টিং ৮-২৫-এর কোডটি গণনা করে যে কিছু টেক্সটে প্রতিটি শব্দ কতবার আসে। আমরা শব্দগুলোকে key হিসাবে এবং তাদের সংখ্যা ট্র্যাক করার জন্য ভ্যালু বৃদ্ধি করে একটি hash map ব্যবহার করি। যদি আমরা প্রথমবার কোনো শব্দ দেখি, আমরা প্রথমে 0
ভ্যালুটি যুক্ত করব।
fn main() { use std::collections::HashMap; let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{map:?}"); }
এই কোডটি {"world": 2, "hello": 1, "wonderful": 1}
প্রিন্ট করবে। আপনি একই key-value পেয়ারগুলো ভিন্ন ক্রমে প্রিন্ট করা দেখতে পারেন: “Accessing Values in a Hash Map” থেকে মনে করুন যে একটি hash map-এর উপর ইটারেট করা একটি অনির্দিষ্ট ক্রমে ঘটে।
split_whitespace
মেথডটি text
-এর ভ্যালুর হোয়াইটস্পেস দ্বারা পৃথক করা সাবস্লাইসের উপর একটি ইটারেটর রিটার্ন করে। or_insert
মেথডটি নির্দিষ্ট key-এর ভ্যালুর একটি mutable রেফারেন্স (&mut V
) রিটার্ন করে। এখানে, আমরা সেই mutable রেফারেন্সটি count
ভ্যারিয়েবলে সংরক্ষণ করি, তাই সেই ভ্যালুতে অ্যাসাইন করার জন্য, আমাদের প্রথমে অ্যাস্টারিস্ক (*
) ব্যবহার করে count
-কে dereference করতে হবে। Mutable রেফারেন্সটি for
লুপের শেষে স্কোপের বাইরে চলে যায়, তাই এই সমস্ত পরিবর্তনগুলি নিরাপদ এবং borrowing rules দ্বারা অনুমোদিত।
হ্যাশিং ফাংশন (Hashing Functions)
ডিফল্টরূপে, HashMap
SipHash নামের একটি হ্যাশিং ফাংশন ব্যবহার করে যা denial-of-service (DoS) আক্রমণের বিরুদ্ধে প্রতিরোধ প্রদান করতে পারে1। এটি উপলব্ধ দ্রুততম হ্যাশিং অ্যালগরিদম নয়, কিন্তু পারফরম্যান্স হ্রাসের বিনিময়ে যে উন্নত নিরাপত্তা পাওয়া যায় তা মূল্যবান। যদি আপনি আপনার কোড প্রোফাইল করেন এবং দেখেন যে ডিফল্ট হ্যাশ ফাংশনটি আপনার উদ্দেশ্যের জন্য খুব ধীর, আপনি একটি ভিন্ন হ্যাশার নির্দিষ্ট করে অন্য ফাংশনে স্যুইচ করতে পারেন। একটি hasher হলো এমন একটি টাইপ যা BuildHasher
trait ইমপ্লিমেন্ট করে। আমরা অধ্যায় ১০-এ trait এবং কীভাবে সেগুলি ইমপ্লিমেন্ট করতে হয় সে সম্পর্কে কথা বলব। আপনাকে স্ক্র্যাচ থেকে নিজের হ্যাশার ইমপ্লিমেন্ট করতে হবে না; crates.io-তে অন্যান্য Rust ব্যবহারকারীদের দ্বারা শেয়ার করা লাইব্রেরি রয়েছে যা অনেক সাধারণ হ্যাশিং অ্যালগরিদম ইমপ্লিমেন্ট করে এমন হ্যাশার সরবরাহ করে।
সারসংক্ষেপ
Vector, string, এবং hash map প্রোগ্রামগুলিতে যখন ডেটা সংরক্ষণ, অ্যাক্সেস এবং পরিবর্তন করার প্রয়োজন হয় তখন একটি বিশাল পরিমাণ কার্যকারিতা প্রদান করবে। এখানে কিছু অনুশীলন রয়েছে যা আপনি এখন সমাধান করার জন্য সজ্জিত থাকা উচিত:
১. পূর্ণসংখ্যার একটি তালিকা দেওয়া হলে, একটি vector ব্যবহার করে তালিকাটির মিডিয়ান (median - সাজানো হলে মাঝের অবস্থানের মান) এবং মোড (mode - যে মানটি সবচেয়ে বেশিবার ঘটে; এখানে একটি hash map সহায়ক হবে) রিটার্ন করুন। ২. স্ট্রিংগুলিকে পিগ ল্যাটিনে (pig latin) রূপান্তর করুন। প্রতিটি শব্দের প্রথম কনসোনেন্ট (consonant) শব্দের শেষে সরানো হয় এবং ay যোগ করা হয়, তাই first হয়ে যায় irst-fay। যে শব্দগুলি ভাওয়েল (vowel) দিয়ে শুরু হয় সেগুলির শেষে hay যোগ করা হয় (apple হয়ে যায় apple-hay)। UTF-8 এনকোডিং সম্পর্কে বিস্তারিত মনে রাখবেন! ৩. একটি hash map এবং vector ব্যবহার করে, একটি টেক্সট ইন্টারফেস তৈরি করুন যা একজন ব্যবহারকারীকে একটি কোম্পানির একটি বিভাগে কর্মচারীর নাম যুক্ত করার অনুমতি দেয়; উদাহরণস্বরূপ, “Add Sally to Engineering” বা “Add Amir to Sales”। তারপরে ব্যবহারকারীকে একটি বিভাগের সমস্ত ব্যক্তির তালিকা বা বিভাগ অনুসারে কোম্পানির সমস্ত ব্যক্তির তালিকা বর্ণানুক্রমিকভাবে সাজানো অবস্থায় পুনরুদ্ধার করার অনুমতি দিন।
Standard library API ডকুমেন্টেশন vector, string, এবং hash map-এর এমন মেথডগুলো বর্ণনা করে যা এই অনুশীলনগুলির জন্য সহায়ক হবে!
আমরা আরও জটিল প্রোগ্রামগুলিতে প্রবেশ করছি যেখানে অপারেশন ব্যর্থ হতে পারে, তাই error handling নিয়ে আলোচনা করার জন্য এটি একটি উপযুক্ত সময়। আমরা এর পরেই তা করব!
এরর হ্যান্ডলিং (Error Handling)
সফটওয়্যারে এরর (error) একটি বাস্তব সত্য, তাই কোনো কিছু ভুল হলে সেই পরিস্থিতি সামলানোর জন্য Rust-এ বেশ কিছু ফিচার (feature) রয়েছে। অনেক ক্ষেত্রে, Rust আপনার কোড কম্পাইল (compile) হওয়ার আগেই আপনাকে এররের সম্ভাবনা স্বীকার করতে এবং কিছু পদক্ষেপ নিতে বাধ্য করে। এই বাধ্যবাধকতা আপনার প্রোগ্রামকে আরও শক্তিশালী (robust) করে তোলে। এটি নিশ্চিত করে যে আপনি আপনার কোড প্রোডাকশনে (production) পাঠানোর আগেই এরর খুঁজে বের করবেন এবং সঠিকভাবে তা হ্যান্ডেল (handle) করবেন।
Rust এররগুলোকে প্রধান দুটি ভাগে ভাগ করে: recoverable (পুনরুদ্ধারযোগ্য) এবং unrecoverable (অপুনরুদ্ধারযোগ্য) এরর। একটি recoverable এরর, যেমন file not found এরর, এর ক্ষেত্রে আমরা সাধারণত ব্যবহারকারীকে সমস্যাটি জানাতে এবং অপারেশনটি আবার চেষ্টা করতে চাই। Unrecoverable এররগুলো সবসময় বাগের (bug) লক্ষণ, যেমন একটি array-এর সীমার বাইরের কোনো লোকেশন অ্যাক্সেস করার চেষ্টা করা। এক্ষেত্রে আমরা প্রোগ্রামটি অবিলম্বে বন্ধ করে দিতে চাই।
বেশিরভাগ ল্যাঙ্গুয়েজ এই দুই ধরনের এররের মধ্যে পার্থক্য করে না এবং উভয়কেই একই উপায়ে, যেমন exceptions ব্যবহার করে, হ্যান্ডেল করে। Rust-এ exceptions নেই। এর পরিবর্তে, recoverable এররের জন্য Result<T, E>
টাইপ এবং unrecoverable এররের ক্ষেত্রে প্রোগ্রাম থামিয়ে দেওয়ার জন্য panic!
ম্যাক্রো রয়েছে। এই অধ্যায়ে প্রথমে panic!
কল করা এবং তারপর Result<T, E>
ভ্যালু রিটার্ন করা নিয়ে আলোচনা করা হবে। এছাড়াও, আমরা একটি এরর থেকে রিকভার করার চেষ্টা করা হবে নাকি এক্সিকিউশন বন্ধ করে দেওয়া হবে, এই সিদ্ধান্ত নেওয়ার সময় বিবেচ্য বিষয়গুলোও খতিয়ে দেখব।
panic!
দিয়ে অপুনরুদ্ধারযোগ্য এরর (Unrecoverable Errors)
কখনও কখনও আপনার কোডে খারাপ কিছু ঘটে, এবং আপনি তা নিয়ে কিছুই করতে পারেন না। এই ধরনের ক্ষেত্রে, Rust-এ panic!
ম্যাক্রো রয়েছে। বাস্তবে প্যানিক (panic) ঘটানোর দুটি উপায় আছে: এমন কোনো কাজ করা যা আমাদের কোডকে প্যানিক করায় (যেমন একটি array-এর সীমার বাইরে অ্যাক্সেস করা) অথবা স্পষ্টভাবে panic!
ম্যাক্রো কল করা। উভয় ক্ষেত্রেই, আমরা আমাদের প্রোগ্রামে একটি প্যানিক ঘটাই। ডিফল্টভাবে, এই প্যানিকগুলো একটি ব্যর্থতার বার্তা প্রিন্ট করবে, স্ট্যাক আনওয়াইন্ড (unwind) করবে, পরিষ্কার করবে এবং প্রোগ্রাম থেকে বেরিয়ে যাবে। একটি এনভায়রনমেন্ট ভেরিয়েবলের মাধ্যমে, আপনি প্যানিকের উৎস খুঁজে বের করা সহজ করার জন্য প্যানিক ঘটলে Rust-কে কল স্ট্যাক (call stack) প্রদর্শন করাতেও পারেন।
প্যানিকের প্রতিক্রিয়ায় স্ট্যাক আনওয়াইন্ড করা বা অ্যাবোর্ট করা (Unwinding the Stack or Aborting in Response to a Panic)
ডিফল্টভাবে, যখন একটি প্যানিক ঘটে, প্রোগ্রামটি unwinding শুরু করে, যার মানে হলো Rust স্ট্যাকের উপরে ফিরে যায় এবং প্রতিটি ফাংশন থেকে ডেটা পরিষ্কার করে। তবে, এভাবে ফিরে যাওয়া এবং পরিষ্কার করা অনেক কাজ। তাই Rust আপনাকে অবিলম্বে aborting (বন্ধ করা) এর বিকল্প বেছে নেওয়ার সুযোগ দেয়, যা কোনো কিছু পরিষ্কার না করেই প্রোগ্রামটি শেষ করে দেয়।
প্রোগ্রাম যে মেমরি ব্যবহার করছিল তা তখন অপারেটিং সিস্টেম দ্বারা পরিষ্কার করার প্রয়োজন হবে। যদি আপনার প্রকল্পে ফলস্বরূপ বাইনারিটিকে যতটা সম্ভব ছোট করার প্রয়োজন হয়, তবে আপনি আপনার Cargo.toml ফাইলের উপযুক্ত
[profile]
বিভাগেpanic = 'abort'
যোগ করে প্যানিকের সময় unwinding থেকে aborting-এ স্যুইচ করতে পারেন। উদাহরণস্বরূপ, যদি আপনি রিলিজ মোডে প্যানিকের সময় অ্যাবোর্ট করতে চান, তবে এটি যোগ করুন:[profile.release] panic = 'abort'
আসুন একটি সহজ প্রোগ্রামে panic!
কল করার চেষ্টা করি:
fn main() { panic!("crash and burn"); }
আপনি যখন প্রোগ্রামটি চালাবেন, তখন আপনি এইরকম কিছু দেখতে পাবেন:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic!
-এর কলটি শেষ দুটি লাইনে থাকা এরর বার্তাটির কারণ। প্রথম লাইনটি আমাদের প্যানিক বার্তা এবং আমাদের সোর্স কোডের সেই স্থানটি দেখায় যেখানে প্যানিক ঘটেছে: src/main.rs:2:5 নির্দেশ করে যে এটি আমাদের src/main.rs ফাইলের দ্বিতীয় লাইনের পঞ্চম অক্ষর।
এই ক্ষেত্রে, নির্দেশিত লাইনটি আমাদের কোডের অংশ, এবং যদি আমরা সেই লাইনে যাই, আমরা panic!
ম্যাক্রো কলটি দেখতে পাই। অন্যান্য ক্ষেত্রে, panic!
কলটি এমন কোডে থাকতে পারে যা আমাদের কোড কল করে, এবং এরর বার্তা দ্বারা রিপোর্ট করা ফাইলের নাম এবং লাইন নম্বর অন্য কারো কোডের হবে যেখানে panic!
ম্যাক্রো কল করা হয়েছে, আমাদের কোডের সেই লাইন নয় যা অবশেষে panic!
কলের কারণ হয়েছে।
আমরা panic!
কলটি যে ফাংশনগুলো থেকে এসেছে তার ব্যাকট্রেস (backtrace) ব্যবহার করে আমাদের কোডের কোন অংশটি সমস্যার কারণ তা খুঁজে বের করতে পারি। একটি panic!
ব্যাকট্রেস কীভাবে ব্যবহার করতে হয় তা বোঝার জন্য, আসুন আরেকটি উদাহরণ দেখি এবং দেখি যখন আমাদের কোডের কোনো বাগের কারণে কোনো লাইব্রেরি থেকে panic!
কল আসে, আমাদের কোড সরাসরি ম্যাক্রো কল করার পরিবর্তে, তখন কেমন হয়। লিস্টিং ৯-১ এ কিছু কোড রয়েছে যা একটি ভেক্টরের বৈধ ইনডেক্সের সীমার বাইরে একটি ইনডেক্স অ্যাক্সেস করার চেষ্টা করে।
fn main() { let v = vec![1, 2, 3]; v[99]; }
এখানে, আমরা আমাদের ভেক্টরের ১০০তম এলিমেন্টটি (যা ইনডেক্স ৯৯-এ রয়েছে কারণ ইনডেক্সিং শূন্য থেকে শুরু হয়) অ্যাক্সেস করার চেষ্টা করছি, কিন্তু ভেক্টরটিতে মাত্র তিনটি এলিমেন্ট রয়েছে। এই পরিস্থিতিতে, Rust প্যানিক করবে। []
ব্যবহার করার কথা একটি এলিমেন্ট রিটার্ন করা, কিন্তু যদি আপনি একটি অবৈধ ইনডেক্স পাস করেন, তবে এখানে এমন কোনো এলিমেন্ট নেই যা Rust সঠিকভাবে রিটার্ন করতে পারত।
C ভাষায়, একটি ডেটা স্ট্রাকচারের শেষের বাইরে পড়ার চেষ্টা করা আনডিফাইন্ড বিহেভিয়ার (undefined behavior)। আপনি মেমরির সেই অবস্থানে যা কিছু আছে তা পেতে পারেন যা ডেটা স্ট্রাকচারের সেই এলিমেন্টের সাথে সঙ্গতিপূর্ণ, যদিও মেমরিটি সেই স্ট্রাকচারের অন্তর্গত নয়। এটিকে buffer overread বলা হয় এবং এটি নিরাপত্তা দুর্বলতার কারণ হতে পারে যদি কোনো আক্রমণকারী ইনডেক্সটিকে এমনভাবে ম্যানিপুলেট করতে সক্ষম হয় যাতে সে এমন ডেটা পড়তে পারে যা তার পড়ার অনুমতি নেই এবং যা ডেটা স্ট্রাকচারের পরে সংরক্ষণ করা হয়েছে।
আপনার প্রোগ্রামকে এই ধরনের দুর্বলতা থেকে রক্ষা করার জন্য, যদি আপনি এমন একটি ইনডেক্সে একটি এলিমেন্ট পড়ার চেষ্টা করেন যা বিদ্যমান নেই, Rust এক্সিকিউশন বন্ধ করে দেবে এবং চলতে অস্বীকার করবে। আসুন এটি চেষ্টা করে দেখি:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace```
এই এররটি আমাদের _main.rs_ এর ৪ নম্বর লাইনকে নির্দেশ করে যেখানে আমরা `v` ভেক্টরের ইনডেক্স `99` অ্যাক্সেস করার চেষ্টা করছি।
`note:` লাইনটি আমাদের বলছে যে আমরা `RUST_BACKTRACE` এনভায়রনমেন্ট ভেরিয়েবল সেট করে এররের কারণ কী ঘটেছে তার একটি সঠিক ব্যাকট্রেস পেতে পারি। একটি _ব্যাকট্রেস_ হলো এই পয়েন্টে পৌঁছানোর জন্য কল করা সমস্ত ফাংশনের একটি তালিকা। Rust-এ ব্যাকট্রেস অন্যান্য ল্যাঙ্গুয়েজের মতোই কাজ করে: ব্যাকট্রেস পড়ার মূল চাবিকাঠি হলো উপর থেকে শুরু করে পড়া যতক্ষণ না আপনি আপনার লেখা ফাইল দেখতে পান। সেখানেই সমস্যার উৎপত্তি। সেই স্থানের উপরের লাইনগুলো হলো কোড যা আপনার কোড কল করেছে; নীচের লাইনগুলো হলো কোড যা আপনার কোডকে কল করেছে। এই আগের এবং পরের লাইনগুলিতে কোর Rust কোড, স্ট্যান্ডার্ড লাইব্রেরি কোড, বা আপনার ব্যবহার করা ক্রেট অন্তর্ভুক্ত থাকতে পারে। আসুন `RUST_BACKTRACE` এনভায়রনমেন্ট ভেরিয়েবলকে `0` ছাড়া যেকোনো মান দিয়ে সেট করে একটি ব্যাকট্রেস পাওয়ার চেষ্টা করি। লিস্টিং ৯-২ আপনার যা দেখার সম্ভাবনা তার অনুরূপ আউটপুট দেখায়।
<!-- manual-regeneration
cd listings/ch09-error-handling/listing-09-01
RUST_BACKTRACE=1 cargo run
copy the backtrace output below
check the backtrace number mentioned in the text below the listing
-->
<Listing number="9-2" caption="`RUST_BACKTRACE` এনভায়রনমেন্ট ভেরিয়েবল সেট করা হলে `panic!` কলের দ্বারা তৈরি ব্যাকট্রেস">
```console
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
এটি অনেক বড় আউটপুট! আপনার অপারেটিং সিস্টেম এবং Rust সংস্করণের উপর নির্ভর করে আপনি যে সঠিক আউটপুটটি দেখবেন তা ভিন্ন হতে পারে। এই তথ্য সহ ব্যাকট্রেস পেতে, ডিবাগ চিহ্ন (debug symbols) সক্রিয় থাকতে হবে। cargo build
বা cargo run
ব্যবহার করার সময় --release
ফ্ল্যাগ ছাড়া ডিবাগ চিহ্ন ডিফল্টভাবে সক্রিয় থাকে, যেমনটি আমরা এখানে করেছি।
লিস্টিং ৯-২ এর আউটপুটে, ব্যাকট্রেসের ৬ নম্বর লাইনটি আমাদের প্রকল্পের সেই লাইনটিকে নির্দেশ করে যা সমস্যার কারণ: src/main.rs এর ৪ নম্বর লাইন। যদি আমরা আমাদের প্রোগ্রামকে প্যানিক করতে না চাই, তবে আমাদের তদন্ত শুরু করা উচিত আমাদের লেখা একটি ফাইলের উল্লেখ করা প্রথম লাইন দ্বারা নির্দেশিত অবস্থান থেকে। লিস্টিং ৯-১-এ, যেখানে আমরা ইচ্ছাকৃতভাবে এমন কোড লিখেছিলাম যা প্যানিক করবে, প্যানিক ঠিক করার উপায় হলো ভেক্টরের ইনডেক্সের সীমার বাইরের কোনো এলিমেন্ট অনুরোধ না করা। ভবিষ্যতে যখন আপনার কোড প্যানিক করবে, তখন আপনাকে বের করতে হবে কোডটি কোন মান দিয়ে কোন কাজটি করার কারণে প্যানিক করছে এবং কোডের পরিবর্তে কী করা উচিত।
আমরা এই অধ্যায়ের পরে “To panic!
or Not to panic!
” বিভাগে panic!
এবং কখন আমাদের এরর পরিস্থিতি হ্যান্ডেল করার জন্য panic!
ব্যবহার করা উচিত এবং কখন উচিত নয় সে বিষয়ে ফিরে আসব। এর পরে, আমরা দেখব কীভাবে Result
ব্যবহার করে একটি এরর থেকে পুনরুদ্ধার করা যায়।
Result
দিয়ে পুনরুদ্ধারযোগ্য এরর (Recoverable Errors)
বেশিরভাগ এরর এতটাই গুরুতর নয় যে প্রোগ্রামটি পুরোপুরি বন্ধ করে দেওয়ার প্রয়োজন হয়। কখনও কখনও যখন একটি ফাংশন ব্যর্থ হয়, তখন তার কারণটি আপনি সহজেই বুঝতে পারেন এবং সেই অনুযায়ী ব্যবস্থা নিতে পারেন। উদাহরণস্বরূপ, যদি আপনি একটি ফাইল খোলার চেষ্টা করেন এবং ফাইলটি না থাকার কারণে সেই অপারেশনটি ব্যর্থ হয়, তাহলে আপনি প্রসেসটি বন্ধ করে দেওয়ার পরিবর্তে ফাইলটি তৈরি করতে চাইতে পারেন।
অধ্যায় ২-এর “Handling Potential Failure with Result
” থেকে মনে করুন যে Result
enum-কে দুটি ভ্যারিয়েন্ট Ok
এবং Err
সহ সংজ্ঞায়িত করা হয়েছে, যা নিম্নরূপ:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
এবং E
হলো জেনেরিক টাইপ প্যারামিটার (generic type parameters): আমরা অধ্যায় ১০-এ জেনেরিক সম্পর্কে আরও বিস্তারিত আলোচনা করব। এখন আপনার যা জানা দরকার তা হলো, T
সফল ক্ষেত্রে Ok
ভ্যারিয়েন্টের মধ্যে ফেরত আসা ভ্যালুর টাইপকে প্রতিনিধিত্ব করে, এবং E
ব্যর্থতার ক্ষেত্রে Err
ভ্যারিয়েন্টের মধ্যে ফেরত আসা এররের টাইপকে প্রতিনিধিত্ব করে। যেহেতু Result
-এর এই জেনেরিক টাইপ প্যারামিটারগুলো রয়েছে, তাই আমরা Result
টাইপ এবং এর উপর সংজ্ঞায়িত ফাংশনগুলো বিভিন্ন পরিস্থিতিতে ব্যবহার করতে পারি যেখানে আমরা যে সফল ভ্যালু এবং এরর ভ্যালু ফেরত দিতে চাই তা ভিন্ন হতে পারে।
আসুন এমন একটি ফাংশন কল করি যা একটি Result
ভ্যালু রিটার্ন করে কারণ ফাংশনটি ব্যর্থ হতে পারে। লিস্টিং ৯-৩-এ আমরা একটি ফাইল খোলার চেষ্টা করছি।
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
File::open
-এর রিটার্ন টাইপ হলো একটি Result<T, E>
। জেনেরিক প্যারামিটার T
File::open
-এর ইমপ্লিমেন্টেশনে সফল ভ্যালুর টাইপ std::fs::File
দ্বারা পূর্ণ হয়েছে, যা একটি ফাইল হ্যান্ডেল (file handle)। এরর ভ্যালুতে ব্যবহৃত E
-এর টাইপ হলো std::io::Error
। এই রিটার্ন টাইপের মানে হলো File::open
কলটি সফল হতে পারে এবং একটি ফাইল হ্যান্ডেল রিটার্ন করতে পারে যা থেকে আমরা পড়তে বা লিখতে পারি। ফাংশন কলটি ব্যর্থও হতে পারে: উদাহরণস্বরূপ, ফাইলটি নাও থাকতে পারে, অথবা আমাদের ফাইল অ্যাক্সেস করার অনুমতি নাও থাকতে পারে। File::open
ফাংশনটির আমাদের জানানোর একটি উপায় থাকা দরকার যে এটি সফল হয়েছে নাকি ব্যর্থ হয়েছে এবং একই সাথে আমাদের ফাইল হ্যান্ডেল বা এররের তথ্য দেওয়া দরকার। Result
enum ঠিক এই তথ্যই বহন করে।
যে ক্ষেত্রে File::open
সফল হয়, greeting_file_result
ভ্যারিয়েবলের ভ্যালুটি হবে Ok
-এর একটি ইনস্ট্যান্স যা একটি ফাইল হ্যান্ডেল ধারণ করে। যে ক্ষেত্রে এটি ব্যর্থ হয়, greeting_file_result
-এর ভ্যালুটি হবে Err
-এর একটি ইনস্ট্যান্স যা কী ধরনের এরর ঘটেছে সে সম্পর্কে আরও তথ্য ধারণ করে।
File::open
যে ভ্যালু রিটার্ন করে তার উপর নির্ভর করে বিভিন্ন পদক্ষেপ নেওয়ার জন্য আমাদের লিস্টিং ৯-৩-এর কোডে আরও কিছু যোগ করতে হবে। লিস্টিং ৯-৪ Result
হ্যান্ডেল করার একটি উপায় দেখায়, যেখানে একটি বেসিক টুল, match
এক্সপ্রেশন ব্যবহার করা হয়েছে যা আমরা অধ্যায় ৬-এ আলোচনা করেছি।
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {error:?}"), }; }
লক্ষ্য করুন যে, Option
enum-এর মতো, Result
enum এবং এর ভ্যারিয়েন্টগুলো prelude দ্বারা স্কোপে আনা হয়েছে, তাই আমাদের match
arm-গুলোতে Ok
এবং Err
ভ্যারিয়েন্টের আগে Result::
নির্দিষ্ট করার প্রয়োজন নেই।
যখন ফলাফল Ok
হয়, এই কোডটি Ok
ভ্যারিয়েন্ট থেকে ভেতরের file
ভ্যালুটি রিটার্ন করবে, এবং আমরা তারপর সেই ফাইল হ্যান্ডেল ভ্যালুটি greeting_file
ভ্যারিয়েবলে অ্যাসাইন করি। match
-এর পরে, আমরা ফাইল হ্যান্ডেলটি পড়া বা লেখার জন্য ব্যবহার করতে পারি।
match
-এর অন্য arm-টি সেই কেসটি হ্যান্ডেল করে যেখানে আমরা File::open
থেকে একটি Err
ভ্যালু পাই। এই উদাহরণে, আমরা panic!
ম্যাক্রো কল করতে বেছে নিয়েছি। যদি আমাদের বর্তমান ডিরেক্টরিতে hello.txt নামে কোনো ফাইল না থাকে এবং আমরা এই কোডটি চালাই, আমরা panic!
ম্যাক্রো থেকে নিম্নলিখিত আউটপুট দেখতে পাব:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
বরাবরের মতো, এই আউটপুটটি আমাদের ঠিক কী ভুল হয়েছে তা বলে দেয়।
বিভিন্ন এররের উপর ম্যাচিং (Matching on Different Errors)
লিস্টিং ৯-৪-এর কোডটি File::open
কেন ব্যর্থ হয়েছে তা নির্বিশেষে panic!
করবে। তবে, আমরা বিভিন্ন ব্যর্থতার কারণের জন্য বিভিন্ন পদক্ষেপ নিতে চাই। যদি ফাইলটি না থাকার কারণে File::open
ব্যর্থ হয়, আমরা ফাইলটি তৈরি করতে এবং নতুন ফাইলের হ্যান্ডেল রিটার্ন করতে চাই। যদি File::open
অন্য কোনো কারণে ব্যর্থ হয়—উদাহরণস্বরূপ, কারণ আমাদের ফাইল খোলার অনুমতি ছিল না—আমরা এখনও চাই কোডটি লিস্টিং ৯-৪-এর মতোই panic!
করুক। এর জন্য, আমরা একটি অভ্যন্তরীণ match
এক্সপ্রেশন যোগ করি, যা লিস্টিং ৯-৫-এ দেখানো হয়েছে।
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
File::open
Err
ভ্যারিয়েন্টের ভিতরে যে ভ্যালুটি রিটার্ন করে তার টাইপ হলো io::Error
, যা standard library দ্বারা সরবরাহ করা একটি struct। এই struct-টির একটি মেথড kind
আছে যা আমরা একটি io::ErrorKind
ভ্যালু পেতে কল করতে পারি। io::ErrorKind
enum-টি standard library দ্বারা সরবরাহ করা হয় এবং এতে এমন ভ্যারিয়েন্ট রয়েছে যা একটি io
অপারেশনের ফলে হতে পারে এমন বিভিন্ন ধরনের এররকে প্রতিনিধিত্ব করে। আমরা যে ভ্যারিয়েন্টটি ব্যবহার করতে চাই তা হলো ErrorKind::NotFound
, যা নির্দেশ করে যে আমরা যে ফাইলটি খোলার চেষ্টা করছি তা এখনও বিদ্যমান নেই। তাই আমরা greeting_file_result
-এর উপর ম্যাচ করি, কিন্তু আমাদের error.kind()
-এর উপর একটি অভ্যন্তরীণ ম্যাচও রয়েছে।
অভ্যন্তরীণ ম্যাচে আমরা যে শর্তটি পরীক্ষা করতে চাই তা হলো error.kind()
দ্বারা রিটার্ন করা ভ্যালুটি ErrorKind
enum-এর NotFound
ভ্যারিয়েন্ট কিনা। যদি তাই হয়, আমরা File::create
দিয়ে ফাইলটি তৈরি করার চেষ্টা করি। তবে, যেহেতু File::create
-ও ব্যর্থ হতে পারে, তাই আমাদের অভ্যন্তরীণ match
এক্সপ্রেশনে একটি দ্বিতীয় arm দরকার। যখন ফাইলটি তৈরি করা যায় না, তখন একটি ভিন্ন এরর বার্তা প্রিন্ট করা হয়। বাইরের match
-এর দ্বিতীয় arm-টি একই থাকে, তাই প্রোগ্রামটি ফাইল না পাওয়ার এরর ছাড়া অন্য যেকোনো এররের জন্য প্যানিক করে।
Result<T, E>
-এর সাথেmatch
ব্যবহারের বিকল্পএখানে অনেক
match
ব্যবহার হয়েছে!match
এক্সপ্রেশনটি খুব দরকারী কিন্তু এটি একটি বেশ আদিম (primitive) টুল। অধ্যায় ১৩-তে, আপনি closures সম্পর্কে শিখবেন, যাResult<T, E>
-তে সংজ্ঞায়িত অনেক মেথডের সাথে ব্যবহৃত হয়। আপনার কোডেResult<T, E>
ভ্যালু হ্যান্ডেল করার সময় এই মেথডগুলোmatch
ব্যবহারের চেয়ে বেশি সংক্ষিপ্ত হতে পারে।উদাহরণস্বরূপ, লিস্টিং ৯-৫-এর মতো একই লজিক লেখার আরেকটি উপায় এখানে দেওয়া হলো, এবার closures এবং
unwrap_or_else
মেথড ব্যবহার করে:use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {error:?}"); }) } else { panic!("Problem opening the file: {error:?}"); } }); }
যদিও এই কোডটির আচরণ লিস্টিং ৯-৫-এর মতোই, এতে কোনো
match
এক্সপ্রেশন নেই এবং এটি পড়তে আরও পরিষ্কার। অধ্যায় ১৩ পড়ার পরে এই উদাহরণে ফিরে আসুন, এবং standard library ডকুমেন্টেশনেunwrap_or_else
মেথডটি দেখুন। এরর নিয়ে কাজ করার সময় এরকম আরও অনেক মেথড আছে যা বিশাল নেস্টেডmatch
এক্সপ্রেশনকে পরিষ্কার করতে পারে।
এররের উপর প্যানিকের জন্য শর্টকাট: unwrap
এবং expect
match
ব্যবহার করা যথেষ্ট ভালো কাজ করে, তবে এটি কিছুটা দীর্ঘ হতে পারে এবং সবসময় উদ্দেশ্য ভালোভাবে বোঝাতে পারে না। Result<T, E>
টাইপের উপর বিভিন্ন, আরও নির্দিষ্ট কাজ করার জন্য অনেক হেল্পার মেথড সংজ্ঞায়িত করা আছে। unwrap
মেথডটি একটি শর্টকাট মেথড যা আমরা লিস্টিং ৯-৪-এ লেখা match
এক্সপ্রেশনের মতোই প্রয়োগ করা হয়েছে। যদি Result
ভ্যালুটি Ok
ভ্যারিয়েন্ট হয়, unwrap
Ok
-এর ভিতরের ভ্যালুটি রিটার্ন করবে। যদি Result
Err
ভ্যারিয়েন্ট হয়, unwrap
আমাদের জন্য panic!
ম্যাক্রো কল করবে। এখানে unwrap
-এর একটি উদাহরণ দেওয়া হলো:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
যদি আমরা hello.txt ফাইল ছাড়া এই কোডটি চালাই, আমরা unwrap
মেথডের করা panic!
কল থেকে একটি এরর বার্তা দেখতে পাব:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
একইভাবে, expect
মেথডটি আমাদের panic!
এরর বার্তাও বেছে নিতে দেয়। unwrap
-এর পরিবর্তে expect
ব্যবহার করা এবং ভালো এরর বার্তা সরবরাহ করা আপনার উদ্দেশ্য বোঝাতে পারে এবং প্যানিকের উৎস খুঁজে বের করা সহজ করে তুলতে পারে। expect
-এর সিনট্যাক্সটি এইরকম:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
আমরা expect
unwrap
-এর মতোই ব্যবহার করি: ফাইল হ্যান্ডেল রিটার্ন করতে বা panic!
ম্যাক্রো কল করতে। expect
-এর panic!
কলে ব্যবহৃত এরর বার্তাটি হবে expect
-এ পাস করা প্যারামিটার, unwrap
-এর ব্যবহৃত ডিফল্ট panic!
বার্তার পরিবর্তে। এটি দেখতে এইরকম:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
প্রোডাকশন-মানের কোডে, বেশিরভাগ রাস্টেসিয়ান (Rustaceans) unwrap
-এর পরিবর্তে expect
বেছে নেয় এবং অপারেশনটি কেন সবসময় সফল হবে বলে আশা করা হচ্ছে সে সম্পর্কে আরও প্রসঙ্গ দেয়। এভাবে, যদি আপনার অনুমান কখনও ভুল প্রমাণিত হয়, আপনার ডিবাগিংয়ে ব্যবহার করার জন্য আরও তথ্য থাকবে।
এরর প্রচার করা (Propagating Errors)
যখন একটি ফাংশনের ইমপ্লিমেন্টেশন এমন কিছু কল করে যা ব্যর্থ হতে পারে, তখন ফাংশনের মধ্যেই এররটি হ্যান্ডেল করার পরিবর্তে, আপনি এররটি কলিং কোডে ফেরত দিতে পারেন যাতে এটি কী করতে হবে তা সিদ্ধান্ত নিতে পারে। এটিকে এরর প্রচার করা (propagating) বলা হয় এবং এটি কলিং কোডকে আরও নিয়ন্ত্রণ দেয়, যেখানে আপনার কোডের প্রেক্ষাপটে আপনার কাছে যা উপলব্ধ তার চেয়ে বেশি তথ্য বা লজিক থাকতে পারে যা নির্দেশ করে যে এররটি কীভাবে হ্যান্ডেল করা উচিত।
উদাহরণস্বরূপ, লিস্টিং ৯-৬ একটি ফাংশন দেখায় যা একটি ফাইল থেকে একটি ব্যবহারকারীর নাম পড়ে। যদি ফাইলটি বিদ্যমান না থাকে বা পড়া না যায়, এই ফাংশনটি সেই এররগুলো ফাংশনটিকে কল করা কোডে ফেরত দেবে।
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } }
এই ফাংশনটি অনেক ছোট করে লেখা যায়, কিন্তু আমরা এরর হ্যান্ডলিং অন্বেষণ করার জন্য প্রথমে এটি ম্যানুয়ালি অনেক কিছু করব; শেষে, আমরা ছোট উপায়টি দেখাব। আসুন প্রথমে ফাংশনের রিটার্ন টাইপটি দেখি: Result<String, io::Error>
। এর মানে হলো ফাংশনটি Result<T, E>
টাইপের একটি ভ্যালু রিটার্ন করছে, যেখানে জেনেরিক প্যারামিটার T
কংক্রিট টাইপ String
দিয়ে এবং জেনেরিক টাইপ E
কংক্রিট টাইপ io::Error
দিয়ে পূর্ণ করা হয়েছে।
যদি এই ফাংশনটি কোনো সমস্যা ছাড়াই সফল হয়, তবে এই ফাংশনটি কল করা কোডটি একটি Ok
ভ্যালু পাবে যা একটি String
ধারণ করে—এই ফাংশনটি ফাইল থেকে যে username
পড়েছে। যদি এই ফাংশনটি কোনো সমস্যার সম্মুখীন হয়, তবে কলিং কোডটি একটি Err
ভ্যালু পাবে যা io::Error
-এর একটি ইনস্ট্যান্স ধারণ করে যা সমস্যাগুলো কী ছিল সে সম্পর্কে আরও তথ্য ধারণ করে। আমরা এই ফাংশনের রিটার্ন টাইপ হিসাবে io::Error
বেছে নিয়েছি কারণ এই ফাংশনের বডিতে আমরা যে দুটি অপারেশন কল করছি যা ব্যর্থ হতে পারে—File::open
ফাংশন এবং read_to_string
মেথড—উভয় থেকেই রিটার্ন করা এরর ভ্যালুর টাইপ এটি।
ফাংশনের বডি File::open
ফাংশন কল করে শুরু হয়। তারপর আমরা লিস্টিং ৯-৪-এর match
-এর মতো একটি match
দিয়ে Result
ভ্যালুটি হ্যান্ডেল করি। যদি File::open
সফল হয়, প্যাটার্ন ভ্যারিয়েবল file
-এর ফাইল হ্যান্ডেলটি মিউটেবল ভ্যারিয়েবল username_file
-এর ভ্যালু হয়ে যায় এবং ফাংশনটি চলতে থাকে। Err
ক্ষেত্রে, panic!
কল করার পরিবর্তে, আমরা return
কীওয়ার্ড ব্যবহার করে ফাংশন থেকে পুরোপুরি আগেভাগে রিটার্ন করি এবং File::open
থেকে এরর ভ্যালুটি, এখন প্যাটার্ন ভ্যারিয়েবল e
-তে, এই ফাংশনের এরর ভ্যালু হিসাবে কলিং কোডে ফেরত পাঠাই।
সুতরাং, যদি আমাদের username_file
-এ একটি ফাইল হ্যান্ডেল থাকে, ফাংশনটি তখন username
ভ্যারিয়েবলে একটি নতুন String
তৈরি করে এবং username_file
-এর ফাইল হ্যান্ডেলের উপর read_to_string
মেথড কল করে ফাইলের বিষয়বস্তু username
-এ পড়ে। read_to_string
মেথডটিও একটি Result
রিটার্ন করে কারণ এটিও ব্যর্থ হতে পারে, যদিও File::open
সফল হয়েছিল। তাই আমাদের সেই Result
হ্যান্ডেল করার জন্য আরেকটি match
দরকার: যদি read_to_string
সফল হয়, তাহলে আমাদের ফাংশন সফল হয়েছে, এবং আমরা ফাইল থেকে পড়া ইউজারনেমটি, যা এখন username
-এ আছে, একটি Ok
-তে র্যাপ করে রিটার্ন করি। যদি read_to_string
ব্যর্থ হয়, আমরা এরর ভ্যালুটি একইভাবে রিটার্ন করি যেভাবে আমরা File::open
-এর রিটার্ন ভ্যালু হ্যান্ডেল করা match
-এ এরর ভ্যালু রিটার্ন করেছিলাম। তবে, আমাদের স্পষ্টভাবে return
বলার প্রয়োজন নেই, কারণ এটি ফাংশনের শেষ এক্সপ্রেশন।
এই কোডটি কল করা কোডটি তখন একটি Ok
ভ্যালু যা একটি ইউজারনেম ধারণ করে বা একটি Err
ভ্যালু যা একটি io::Error
ধারণ করে তা হ্যান্ডেল করবে। সেই ভ্যালুগুলো দিয়ে কী করতে হবে তা সিদ্ধান্ত নেওয়া কলিং কোডের উপর নির্ভর করে। যদি কলিং কোড একটি Err
ভ্যালু পায়, তবে এটি panic!
কল করে প্রোগ্রাম ক্র্যাশ করতে পারে, একটি ডিফল্ট ইউজারনেম ব্যবহার করতে পারে, অথবা ফাইল ছাড়া অন্য কোথাও থেকে ইউজারনেম খুঁজতে পারে, উদাহরণস্বরূপ। কলিং কোড আসলে কী করার চেষ্টা করছে সে সম্পর্কে আমাদের কাছে পর্যাপ্ত তথ্য নেই, তাই আমরা সমস্ত সফলতা বা এররের তথ্য উপরে প্রচার করি যাতে এটি যথাযথভাবে হ্যান্ডেল করতে পারে।
এরর প্রচারের এই প্যাটার্নটি Rust-এ এতটাই সাধারণ যে Rust এটিকে সহজ করার জন্য প্রশ্নবোধক চিহ্ন অপারেটর ?
সরবরাহ করে।
এরর প্রচারের জন্য একটি শর্টকাট: ?
অপারেটর
লিস্টিং ৯-৭ read_username_from_file
-এর একটি ইমপ্লিমেন্টেশন দেখায় যা লিস্টিং ৯-৬-এর মতোই কার্যকারিতা সম্পন্ন, কিন্তু এই ইমপ্লিমেন্টেশনটি ?
অপারেটর ব্যবহার করে।
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
একটি Result
ভ্যালুর পরে রাখা ?
অপারেটরটি প্রায় একইভাবে কাজ করার জন্য সংজ্ঞায়িত করা হয়েছে যেভাবে আমরা লিস্টিং ৯-৬-এ Result
ভ্যালুগুলো হ্যান্ডেল করার জন্য match
এক্সপ্রেশন সংজ্ঞায়িত করেছি। যদি Result
-এর ভ্যালুটি একটি Ok
হয়, তবে Ok
-এর ভিতরের ভ্যালুটি এই এক্সপ্রেশন থেকে ফেরত আসবে, এবং প্রোগ্রামটি চলতে থাকবে। যদি ভ্যালুটি একটি Err
হয়, তবে Err
পুরো ফাংশন থেকে ফেরত আসবে যেন আমরা return
কীওয়ার্ড ব্যবহার করেছি যাতে এরর ভ্যালুটি কলিং কোডে প্রচারিত হয়।
লিস্টিং ৯-৬-এর match
এক্সপ্রেশন যা করে এবং ?
অপারেটর যা করে তার মধ্যে একটি পার্থক্য রয়েছে: যে এরর ভ্যালুগুলোর উপর ?
অপারেটর কল করা হয় সেগুলি standard library-এর From
trait-এ সংজ্ঞায়িত from
ফাংশনের মধ্য দিয়ে যায়, যা এক টাইপের ভ্যালুকে অন্য টাইপে রূপান্তর করতে ব্যবহৃত হয়। যখন ?
অপারেটর from
ফাংশনটি কল করে, তখন প্রাপ্ত এরর টাইপটি বর্তমান ফাংশনের রিটার্ন টাইপে সংজ্ঞায়িত এরর টাইপে রূপান্তরিত হয়। এটি দরকারী যখন একটি ফাংশন একটি এরর টাইপ রিটার্ন করে যা ফাংশনটি ব্যর্থ হওয়ার সমস্ত উপায়কে প্রতিনিধিত্ব করে, এমনকি যদি অংশগুলি বিভিন্ন কারণে ব্যর্থ হতে পারে।
উদাহরণস্বরূপ, আমরা লিস্টিং ৯-৭-এর read_username_from_file
ফাংশনটি পরিবর্তন করে OurError
নামের একটি কাস্টম এরর টাইপ রিটার্ন করতে পারি যা আমরা সংজ্ঞায়িত করি। যদি আমরা একটি io::Error
থেকে OurError
-এর একটি ইনস্ট্যান্স তৈরি করার জন্য impl From<io::Error> for OurError
-ও সংজ্ঞায়িত করি, তবে read_username_from_file
-এর বডিতে ?
অপারেটর কলগুলো from
কল করবে এবং ফাংশনে কোনো অতিরিক্ত কোড যোগ না করেই এরর টাইপগুলো রূপান্তর করবে।
লিস্টিং ৯-৭-এর প্রেক্ষাপটে, File::open
কলের শেষে ?
একটি Ok
-এর ভিতরের ভ্যালুটি username_file
ভ্যারিয়েবলে রিটার্ন করবে। যদি একটি এরর ঘটে, ?
অপারেটরটি পুরো ফাংশন থেকে আগেভাগে রিটার্ন করবে এবং কলিং কোডকে যেকোনো Err
ভ্যালু দেবে। একই জিনিস read_to_string
কলের শেষে ?
-এর ক্ষেত্রেও প্রযোজ্য।
?
অপারেটরটি অনেক বয়লারপ্লেট (boilerplate) দূর করে এবং এই ফাংশনের ইমপ্লিমেন্টেশনকে সহজ করে তোলে। আমরা ?
-এর ঠিক পরে মেথড কল চেইন করে এই কোডটিকে আরও ছোট করতে পারি, যেমনটি লিস্টিং ৯-৮-এ দেখানো হয়েছে।
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } }
আমরা username
-এ নতুন String
তৈরি করাটা ফাংশনের শুরুতে নিয়ে এসেছি; সেই অংশটি পরিবর্তিত হয়নি। username_file
ভ্যারিয়েবল তৈরি করার পরিবর্তে, আমরা read_to_string
কলটি সরাসরি File::open("hello.txt")?
-এর ফলাফলের সাথে চেইন করেছি। read_to_string
কলের শেষে আমাদের এখনও একটি ?
রয়েছে, এবং File::open
এবং read_to_string
উভয়ই সফল হলে আমরা এখনও এরর রিটার্ন করার পরিবর্তে username
ধারণকারী একটি Ok
ভ্যালু রিটার্ন করি। কার্যকারিতা আবার লিস্টিং ৯-৬ এবং লিস্টিং ৯-৭-এর মতোই; এটি লেখার একটি ভিন্ন, আরও সুবিধাজনক উপায়।
লিস্টিং ৯-৯ fs::read_to_string
ব্যবহার করে এটিকে আরও ছোট করার একটি উপায় দেখায়।
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
একটি ফাইলকে একটি স্ট্রিং-এ পড়া একটি মোটামুটি সাধারণ অপারেশন, তাই standard library সুবিধাজনক fs::read_to_string
ফাংশন সরবরাহ করে যা ফাইলটি খোলে, একটি নতুন String
তৈরি করে, ফাইলের বিষয়বস্তু পড়ে, সেই String
-এ বিষয়বস্তু রাখে এবং এটি রিটার্ন করে। অবশ্যই, fs::read_to_string
ব্যবহার করা আমাদের সমস্ত এরর হ্যান্ডলিং ব্যাখ্যা করার সুযোগ দেয় না, তাই আমরা প্রথমে দীর্ঘ উপায়টি করেছি।
কোথায় ?
অপারেটর ব্যবহার করা যেতে পারে
?
অপারেটরটি শুধুমাত্র সেই ফাংশনগুলিতে ব্যবহার করা যেতে পারে যাদের রিটার্ন টাইপ ?
যে ভ্যালুর উপর ব্যবহৃত হয় তার সাথে সামঞ্জস্যপূর্ণ। এটি কারণ ?
অপারেটরটি একটি ফাংশন থেকে একটি ভ্যালুর আগেভাগে রিটার্ন করার জন্য সংজ্ঞায়িত করা হয়েছে, ঠিক যেমনটি আমরা লিস্টিং ৯-৬-এ সংজ্ঞায়িত match
এক্সপ্রেশনের মতো। লিস্টিং ৯-৬-এ, match
একটি Result
ভ্যালু ব্যবহার করছিল, এবং আগেভাগে রিটার্ন করা arm-টি একটি Err(e)
ভ্যালু রিটার্ন করেছিল। ফাংশনের রিটার্ন টাইপটি একটি Result
হতে হবে যাতে এটি এই return
-এর সাথে সামঞ্জস্যপূর্ণ হয়।
লিস্টিং ৯-১০-এ, আসুন দেখি আমরা যদি একটি main
ফাংশনে ?
অপারেটর ব্যবহার করি যার রিটার্ন টাইপ আমরা যে ভ্যালুর উপর ?
ব্যবহার করি তার টাইপের সাথে অসামঞ্জস্যপূর্ণ হয় তবে আমরা কী এরর পাব।
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
এই কোডটি একটি ফাইল খোলে, যা ব্যর্থ হতে পারে। ?
অপারেটরটি File::open
দ্বারা রিটার্ন করা Result
ভ্যালুটিকে অনুসরণ করে, কিন্তু এই main
ফাংশনটির রিটার্ন টাইপ ()
, Result
নয়। যখন আমরা এই কোডটি কম্পাইল করি, তখন আমরা নিম্নলিখিত এরর বার্তাটি পাই:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
এই এররটি নির্দেশ করে যে আমরা শুধুমাত্র সেই ফাংশনে ?
অপারেটর ব্যবহার করতে পারি যা Result
, Option
, বা FromResidual
ইমপ্লিমেন্ট করে এমন অন্য কোনো টাইপ রিটার্ন করে।
এররটি ঠিক করার জন্য, আপনার দুটি বিকল্প রয়েছে। একটি বিকল্প হলো আপনার ফাংশনের রিটার্ন টাইপ পরিবর্তন করে আপনি যে ভ্যালুর উপর ?
অপারেটর ব্যবহার করছেন তার সাথে সামঞ্জস্যপূর্ণ করা, যতক্ষণ না আপনার কোনো সীমাবদ্ধতা থাকে যা এটি প্রতিরোধ করে। অন্য বিকল্পটি হলো Result<T, E>
-কে যেভাবে উপযুক্ত সেভাবে হ্যান্ডেল করার জন্য একটি match
বা Result<T, E>
-এর কোনো মেথড ব্যবহার করা।
এরর বার্তাটিতে আরও উল্লেখ করা হয়েছে যে ?
Option<T>
ভ্যালুগুলোর সাথেও ব্যবহার করা যেতে পারে। Result
-এর উপর ?
ব্যবহারের মতোই, আপনি শুধুমাত্র সেই ফাংশনে Option
-এর উপর ?
ব্যবহার করতে পারেন যা একটি Option
রিটার্ন করে। Option<T>
-এর উপর কল করা হলে ?
অপারেটরের আচরণ Result<T, E>
-এর উপর কল করা হলে তার আচরণের মতোই: যদি ভ্যালুটি None
হয়, তবে সেই সময়ে ফাংশন থেকে None
আগেভাগে রিটার্ন করা হবে। যদি ভ্যালুটি Some
হয়, তবে Some
-এর ভিতরের ভ্যালুটি এক্সপ্রেশনের ফলস্বরূপ ভ্যালু হয়, এবং ফাংশনটি চলতে থাকে। লিস্টিং ৯-১১-এ একটি ফাংশনের উদাহরণ রয়েছে যা প্রদত্ত টেক্সটের প্রথম লাইনের শেষ অক্ষরটি খুঁজে বের করে।
fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } fn main() { assert_eq!( last_char_of_first_line("Hello, world\nHow are you today?"), Some('d') ); assert_eq!(last_char_of_first_line(""), None); assert_eq!(last_char_of_first_line("\nhi"), None); }``` </Listing> এই ফাংশনটি `Option<char>` রিটার্ন করে কারণ এটি সম্ভব যে সেখানে একটি অক্ষর আছে, কিন্তু এটিও সম্ভব যে সেখানে নেই। এই কোডটি `text` স্ট্রিং স্লাইস আর্গুমেন্টটি নেয় এবং এর উপর `lines` মেথড কল করে, যা স্ট্রিং-এর লাইনগুলোর উপর একটি ইটারেটর রিটার্ন করে। যেহেতু এই ফাংশনটি প্রথম লাইনটি পরীক্ষা করতে চায়, এটি ইটারেটরের প্রথম ভ্যালুটি পেতে ইটারেটরের উপর `next` কল করে। যদি `text` একটি খালি স্ট্রিং হয়, তবে এই `next` কলটি `None` রিটার্ন করবে, সেক্ষেত্রে আমরা `?` ব্যবহার করে `last_char_of_first_line` থেকে `None` রিটার্ন করে থেমে যাই। যদি `text` একটি খালি স্ট্রিং না হয়, `next` একটি `Some` ভ্যালু রিটার্ন করবে যা `text`-এর প্রথম লাইনের একটি স্ট্রিং স্লাইস ধারণ করে। `?` স্ট্রিং স্লাইসটি এক্সট্র্যাক্ট করে, এবং আমরা সেই স্ট্রিং স্লাইসের উপর `chars` কল করে এর অক্ষরগুলোর একটি ইটারেটর পেতে পারি। আমরা এই প্রথম লাইনের শেষ অক্ষরে আগ্রহী, তাই আমরা ইটারেটরের শেষ আইটেমটি রিটার্ন করার জন্য `last` কল করি। এটি একটি `Option` কারণ এটি সম্ভব যে প্রথম লাইনটি একটি খালি স্ট্রিং; উদাহরণস্বরূপ, যদি `text` একটি ফাঁকা লাইন দিয়ে শুরু হয় কিন্তু অন্য লাইনে অক্ষর থাকে, যেমন `"\nhi"`। তবে, যদি প্রথম লাইনে একটি শেষ অক্ষর থাকে, তবে এটি `Some` ভ্যারিয়েন্টে রিটার্ন করা হবে। মাঝখানে `?` অপারেটরটি আমাদের এই লজিকটি সংক্ষিপ্তভাবে প্রকাশ করার একটি উপায় দেয়, যা আমাদের ফাংশনটি এক লাইনে ইমপ্লিমেন্ট করতে দেয়। যদি আমরা `Option`-এর উপর `?` অপারেটর ব্যবহার করতে না পারতাম, তবে আমাদের এই লজিকটি আরও মেথড কল বা একটি `match` এক্সপ্রেশন ব্যবহার করে ইমপ্লিমেন্ট করতে হতো। মনে রাখবেন যে আপনি একটি ফাংশনে `Result`-এর উপর `?` অপারেটর ব্যবহার করতে পারেন যা `Result` রিটার্ন করে, এবং আপনি একটি ফাংশনে `Option`-এর উপর `?` অপারেটর ব্যবহার করতে পারেন যা `Option` রিটার্ন করে, কিন্তু আপনি মিশ্রণ করতে পারবেন না। `?` অপারেটরটি স্বয়ংক্রিয়ভাবে একটি `Result`-কে একটি `Option`-এ বা তার বিপরীতে রূপান্তর করবে না; সেই ক্ষেত্রে, আপনি স্পষ্টভাবে রূপান্তর করার জন্য `Result`-এর উপর `ok` মেথড বা `Option`-এর উপর `ok_or` মেথডের মতো মেথড ব্যবহার করতে পারেন। এখন পর্যন্ত, আমরা যে সমস্ত `main` ফাংশন ব্যবহার করেছি সেগুলি `()` রিটার্ন করে। `main` ফাংশনটি বিশেষ কারণ এটি একটি এক্সিকিউটেবল প্রোগ্রামের প্রবেশ এবং প্রস্থান বিন্দু, এবং প্রোগ্রামটি প্রত্যাশিতভাবে আচরণ করার জন্য এর রিটার্ন টাইপের উপর বিধিনিষেধ রয়েছে। সৌভাগ্যবশত, `main` একটি `Result<(), E>`-ও রিটার্ন করতে পারে। লিস্টিং ৯-১২-এ লিস্টিং ৯-১০-এর কোড রয়েছে, কিন্তু আমরা `main`-এর রিটার্ন টাইপ পরিবর্তন করে `Result<(), Box<dyn Error>>` করেছি এবং শেষে একটি রিটার্ন ভ্যালু `Ok(())` যোগ করেছি। এই কোডটি এখন কম্পাইল হবে। <Listing number="9-12" file-name="src/main.rs" caption="`main`-কে `Result<(), E>` রিটার্ন করার জন্য পরিবর্তন করা `Result` ভ্যালুগুলোর উপর `?` অপারেটর ব্যবহারের অনুমতি দেয়।"> ```rust,ignore use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; Ok(()) }
Box<dyn Error>
টাইপটি একটি trait object, যা আমরা অধ্যায় ১৮-এর “Using Trait Objects That Allow for Values of Different Types” বিভাগে আলোচনা করব। আপাতত, আপনি Box<dyn Error>
-কে “যেকোনো ধরনের এরর” হিসাবে পড়তে পারেন। main
ফাংশনে Box<dyn Error>
এরর টাইপসহ একটি Result
ভ্যালুর উপর ?
ব্যবহার করা অনুমোদিত কারণ এটি যেকোনো Err
ভ্যালুকে আগেভাগে রিটার্ন করার অনুমতি দেয়। যদিও এই main
ফাংশনের বডি শুধুমাত্র std::io::Error
টাইপের এরর রিটার্ন করবে, Box<dyn Error>
নির্দিষ্ট করার মাধ্যমে, এই সিগনেচারটি সঠিক থাকবে এমনকি যদি main
-এর বডিতে অন্য এরর রিটার্ন করে এমন আরও কোড যোগ করা হয়।
যখন একটি main
ফাংশন একটি Result<(), E>
রিটার্ন করে, তখন এক্সিকিউটেবলটি 0
ভ্যালু দিয়ে প্রস্থান করবে যদি main
Ok(())
রিটার্ন করে এবং একটি নন-জিরো ভ্যালু দিয়ে প্রস্থান করবে যদি main
একটি Err
ভ্যালু রিটার্ন করে। C-তে লেখা এক্সিকিউটেবলগুলো প্রস্থান করার সময় ইন্টিজার রিটার্ন করে: যে প্রোগ্রামগুলো সফলভাবে প্রস্থান করে সেগুলি 0
ইন্টিজার রিটার্ন করে, এবং যে প্রোগ্রামগুলো এরর করে সেগুলি 0
ছাড়া অন্য কোনো ইন্টিজার রিটার্ন করে। Rust এই কনভেনশনের সাথে সামঞ্জস্যপূর্ণ হওয়ার জন্য এক্সিকিউটেবল থেকে ইন্টিজার রিটার্ন করে।
main
ফাংশনটি যেকোনো টাইপ রিটার্ন করতে পারে যা std::process::Termination
trait ইমপ্লিমেন্ট করে, যা একটি report
ফাংশন ধারণ করে যা একটি ExitCode
রিটার্ন করে। আপনার নিজের টাইপের জন্য Termination
trait ইমপ্লিমেন্ট করার বিষয়ে আরও তথ্যের জন্য standard library ডকুমেন্টেশন দেখুন।
এখন যেহেতু আমরা panic!
কল করা বা Result
রিটার্ন করার বিস্তারিত আলোচনা করেছি, আসুন আমরা কোন ক্ষেত্রে কোনটি ব্যবহার করা উপযুক্ত তা সিদ্ধান্ত নেওয়ার বিষয়ে ফিরে যাই।
panic!
করা নাকি না করা (To panic!
or Not to panic!
)
তাহলে আপনি কীভাবে সিদ্ধান্ত নেবেন কখন panic!
কল করা উচিত এবং কখন Result
রিটার্ন করা উচিত? যখন কোড প্যানিক করে, তখন পুনরুদ্ধার (recover) করার কোনো উপায় থাকে না। আপনি যেকোনো এরর পরিস্থিতিতে panic!
কল করতে পারেন, چاہے পুনরুদ্ধারের কোনো সম্ভাব্য উপায় থাকুক বা না থাকুক, কিন্তু সেক্ষেত্রে আপনি কলিং কোডের পক্ষে সিদ্ধান্ত নিচ্ছেন যে পরিস্থিতিটি অপুনরুদ্ধারযোগ্য। যখন আপনি একটি Result
ভ্যালু রিটার্ন করতে বেছে নেন, আপনি কলিং কোডকে বিকল্প (options) দেন। কলিং কোড তার পরিস্থিতির জন্য উপযুক্ত উপায়ে পুনরুদ্ধার করার চেষ্টা করতে পারে, অথবা এটি সিদ্ধান্ত নিতে পারে যে এই ক্ষেত্রে একটি Err
ভ্যালু অপুনরুদ্ধারযোগ্য, তাই এটি panic!
কল করতে পারে এবং আপনার পুনরুদ্ধারযোগ্য এররকে অপুনরুদ্ধারযোগ্য এররে পরিণত করতে পারে। অতএব, যখন আপনি এমন একটি ফাংশন ডিফাইন করছেন যা ব্যর্থ হতে পারে, তখন Result
রিটার্ন করা একটি ভালো ডিফল্ট পছন্দ।
উদাহরণ, প্রোটোটাইপ কোড এবং টেস্টের মতো পরিস্থিতিতে, Result
রিটার্ন করার পরিবর্তে প্যানিক করে এমন কোড লেখা বেশি উপযুক্ত। চলুন探讨 করি কেন, তারপর সেই পরিস্থিতিগুলো নিয়ে আলোচনা করি যেখানে কম্পাইলার বলতে পারে না যে ব্যর্থতা অসম্ভব, কিন্তু আপনি একজন মানুষ হিসেবে তা পারেন। অধ্যায়টি লাইব্রেরি কোডে প্যানিক করার সিদ্ধান্ত নেওয়ার বিষয়ে কিছু সাধারণ নির্দেশিকা দিয়ে শেষ হবে।
উদাহরণ, প্রোটোটাইপ কোড এবং টেস্ট (Examples, Prototype Code, and Tests)
যখন আপনি কোনো ধারণা ব্যাখ্যা করার জন্য একটি উদাহরণ লিখছেন, তখন শক্তিশালী এরর-হ্যান্ডলিং কোড অন্তর্ভুক্ত করলে উদাহরণটি কম স্পষ্ট হতে পারে। উদাহরণগুলিতে, এটা বোঝা যায় যে unwrap
-এর মতো একটি মেথডের কল, যা প্যানিক করতে পারে, তা আপনার অ্যাপ্লিকেশন যেভাবে এরর হ্যান্ডেল করতে চায় তার জন্য একটি স্থানধারক (placeholder) হিসাবে বোঝানো হয়েছে, যা আপনার বাকি কোড কী করছে তার উপর ভিত্তি করে ভিন্ন হতে পারে।
একইভাবে, প্রোটোটাইপিংয়ের সময় unwrap
এবং expect
মেথডগুলি খুব সুবিধাজনক, যখন আপনি এরর কীভাবে হ্যান্ডেল করবেন তা সিদ্ধান্ত নিতে প্রস্তুত নন। আপনি যখন আপনার প্রোগ্রামকে আরও শক্তিশালী করতে প্রস্তুত হবেন, তখন এগুলি আপনার কোডে স্পষ্ট চিহ্ন রেখে যায়।
যদি একটি টেস্টে কোনো মেথড কল ব্যর্থ হয়, আপনি চাইবেন পুরো টেস্টটিই ব্যর্থ হোক, এমনকি যদি সেই মেথডটি পরীক্ষার অধীনে থাকা কার্যকারিতা না হয়। যেহেতু panic!
হলো একটি টেস্টকে ব্যর্থ হিসাবে চিহ্নিত করার উপায়, তাই unwrap
বা expect
কল করাই ঠিক যা হওয়া উচিত।
এমন ক্ষেত্র যেখানে আপনার কাছে কম্পাইলারের চেয়ে বেশি তথ্য আছে
expect
কল করাও উপযুক্ত হবে যখন আপনার কাছে অন্য কোনো যুক্তি থাকে যা নিশ্চিত করে যে Result
-এর একটি Ok
ভ্যালু থাকবে, কিন্তু সেই যুক্তিটি কম্পাইলার বুঝতে পারে না। আপনার কাছে এখনও একটি Result
ভ্যালু থাকবে যা আপনাকে হ্যান্ডেল করতে হবে: আপনি যে অপারেশনটি কল করছেন তার সাধারণভাবে ব্যর্থ হওয়ার সম্ভাবনা এখনও আছে, যদিও আপনার নির্দিষ্ট পরিস্থিতিতে এটি যৌক্তিকভাবে অসম্ভব। যদি আপনি ম্যানুয়ালি কোড পরিদর্শন করে নিশ্চিত করতে পারেন যে আপনার কাছে কখনই একটি Err
ভ্যারিয়েন্ট থাকবে না, তবে expect
কল করা এবং আর্গুমেন্ট টেক্সটে আপনি কেন মনে করেন যে আপনার কখনই একটি Err
ভ্যারিয়েন্ট থাকবে না তার কারণ নথিভুক্ত করা সম্পূর্ণ গ্রহণযোগ্য। এখানে একটি উদাহরণ:
fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1" .parse() .expect("Hardcoded IP address should be valid"); }
আমরা একটি হার্ডকোডেড স্ট্রিং পার্স করে একটি IpAddr
ইনস্ট্যান্স তৈরি করছি। আমরা দেখতে পাচ্ছি যে 127.0.0.1
একটি বৈধ আইপি অ্যাড্রেস, তাই এখানে expect
ব্যবহার করা গ্রহণযোগ্য। তবে, একটি হার্ডকোডেড, বৈধ স্ট্রিং থাকা parse
মেথডের রিটার্ন টাইপ পরিবর্তন করে না: আমরা এখনও একটি Result
ভ্যালু পাই, এবং কম্পাইলার এখনও আমাদের Result
হ্যান্ডেল করতে বাধ্য করবে যেন Err
ভ্যারিয়েন্ট একটি সম্ভাবনা, কারণ কম্পাইলার যথেষ্ট স্মার্ট নয় যে দেখতে পারে এই স্ট্রিংটি সর্বদা একটি বৈধ আইপি অ্যাড্রেস। যদি আইপি অ্যাড্রেস স্ট্রিংটি প্রোগ্রামে হার্ডকোড না হয়ে ব্যবহারকারীর কাছ থেকে আসত এবং তাই ব্যর্থতার সম্ভাবনা থাকত, আমরা অবশ্যই Result
-কে আরও শক্তিশালী উপায়ে হ্যান্ডেল করতে চাইতাম। এই আইপি অ্যাড্রেসটি হার্ডকোডেড এই অনুমানটি উল্লেখ করা আমাদের ভবিষ্যতে যদি অন্য কোনো উৎস থেকে আইপি অ্যাড্রেস পাওয়ার প্রয়োজন হয় তবে expect
-কে আরও ভালো এরর-হ্যান্ডলিং কোডে পরিবর্তন করতে উৎসাহিত করবে।
এরর হ্যান্ডলিংয়ের জন্য নির্দেশিকা (Guidelines for Error Handling)
আপনার কোড যখন একটি খারাপ অবস্থায় পড়তে পারে, তখন আপনার কোডকে প্যানিক করানো যুক্তিযুক্ত। এই প্রেক্ষাপটে, একটি খারাপ অবস্থা হলো যখন কোনো অনুমান, গ্যারান্টি, চুক্তি বা ইনভ্যারিয়েন্ট (invariant) ভেঙে যায়, যেমন যখন আপনার কোডে অবৈধ মান, পরস্পরবিরোধী মান বা অনুপস্থিত মান পাস করা হয়—এবং এর সাথে নিম্নলিখিত এক বা একাধিক বিষয় ঘটে:
- খারাপ অবস্থাটি অপ্রত্যাশিত কিছু, এমন কিছুর বিপরীতে যা সম্ভবত মাঝে মাঝে ঘটবে, যেমন একজন ব্যবহারকারীর ভুল বিন্যাসে ডেটা প্রবেশ করানো।
- এই বিন্দুর পরে আপনার কোডকে এই খারাপ অবস্থায় না থাকার উপর নির্ভর করতে হবে, প্রতিটি ধাপে সমস্যাটি পরীক্ষা করার পরিবর্তে।
- আপনি যে টাইপগুলি ব্যবহার করেন সেগুলিতে এই তথ্য এনকোড করার কোনো ভালো উপায় নেই। আমরা অধ্যায় ১৮-এর “Encoding States and Behavior as Types”-এ এর একটি উদাহরণ দেখব।
যদি কেউ আপনার কোড কল করে এবং এমন মান পাস করে যা অর্থহীন, তবে যদি আপনি পারেন তবে একটি এরর রিটার্ন করাই ভালো যাতে লাইব্রেরির ব্যবহারকারী সিদ্ধান্ত নিতে পারে যে সেই ক্ষেত্রে তারা কী করতে চায়। তবে, যে ক্ষেত্রে চালিয়ে যাওয়া असुरक्षित বা ক্ষতিকারক হতে পারে, সেরা পছন্দ হতে পারে panic!
কল করা এবং আপনার লাইব্রেরি ব্যবহারকারীকে তাদের কোডের বাগ সম্পর্কে সতর্ক করা যাতে তারা ডেভেলপমেন্টের সময় এটি ঠিক করতে পারে। একইভাবে, panic!
প্রায়শই উপযুক্ত যদি আপনি আপনার নিয়ন্ত্রণের বাইরের এক্সটার্নাল কোড কল করছেন এবং এটি একটি অবৈধ অবস্থা রিটার্ন করে যা আপনার ঠিক করার কোনো উপায় নেই।
তবে, যখন ব্যর্থতা প্রত্যাশিত হয়, তখন panic!
কল করার চেয়ে Result
রিটার্ন করা বেশি উপযুক্ত। উদাহরণগুলির মধ্যে রয়েছে একটি পার্সারকে ভুল ফরম্যাটের ডেটা দেওয়া বা একটি HTTP অনুরোধ এমন একটি স্ট্যাটাস রিটার্ন করা যা নির্দেশ করে যে আপনি একটি রেট লিমিটে পৌঁছেছেন। এই ক্ষেত্রে, একটি Result
রিটার্ন করা নির্দেশ করে যে ব্যর্থতা একটি প্রত্যাশিত সম্ভাবনা যা কলিং কোডকে সিদ্ধান্ত নিতে হবে কীভাবে হ্যান্ডেল করতে হবে।
যখন আপনার কোড এমন একটি অপারেশন সম্পাদন করে যা অবৈধ মান ব্যবহার করে কল করা হলে ব্যবহারকারীকে ঝুঁকির মধ্যে ফেলতে পারে, তখন আপনার কোডকে প্রথমে মানগুলি বৈধ কিনা তা যাচাই করা উচিত এবং মানগুলি বৈধ না হলে প্যানিক করা উচিত। এটি মূলত নিরাপত্তার কারণে: অবৈধ ডেটার উপর অপারেশন করার চেষ্টা আপনার কোডকে দুর্বলতার সম্মুখীন করতে পারে। এটিই প্রধান কারণ যে স্ট্যান্ডার্ড লাইব্রেরি panic!
কল করবে যদি আপনি সীমার বাইরে মেমরি অ্যাক্সেসের চেষ্টা করেন: বর্তমান ডেটা স্ট্রাকচারের অন্তর্গত নয় এমন মেমরি অ্যাক্সেস করার চেষ্টা একটি সাধারণ নিরাপত্তা সমস্যা। ফাংশনগুলির প্রায়শই চুক্তি (contracts) থাকে: তাদের আচরণ শুধুমাত্র তখনই নিশ্চিত করা হয় যদি ইনপুটগুলি নির্দিষ্ট প্রয়োজনীয়তা পূরণ করে। চুক্তি লঙ্ঘন হলে প্যানিক করা অর্থপূর্ণ কারণ একটি চুক্তি লঙ্ঘন সর্বদা একটি কলার-সাইড বাগ নির্দেশ করে, এবং এটি এমন এক ধরনের এরর নয় যা আপনি চান কলিং কোডকে স্পষ্টভাবে হ্যান্ডেল করতে হোক। আসলে, কলিং কোডের পুনরুদ্ধার করার কোনো যুক্তিসঙ্গত উপায় নেই; কলিং প্রোগ্রামারদের কোড ঠিক করতে হবে। একটি ফাংশনের জন্য চুক্তি, বিশেষ করে যখন একটি লঙ্ঘন প্যানিক ঘটাবে, ফাংশনের জন্য API ডকুমেন্টেশনে ব্যাখ্যা করা উচিত।
তবে, আপনার সমস্ত ফাংশনে প্রচুর এরর চেক থাকা দীর্ঘ এবং বিরক্তিকর হবে। সৌভাগ্যবশত, আপনি Rust-এর টাইপ সিস্টেম (এবং এইভাবে কম্পাইলার দ্বারা করা টাইপ চেকিং) ব্যবহার করে আপনার জন্য অনেক চেক করতে পারেন। যদি আপনার ফাংশনের একটি নির্দিষ্ট টাইপ প্যারামিটার হিসাবে থাকে, আপনি আপনার কোডের যুক্তি নিয়ে এগিয়ে যেতে পারেন এটা জেনে যে কম্পাইলার ইতিমধ্যে নিশ্চিত করেছে যে আপনার কাছে একটি বৈধ মান আছে। উদাহরণস্বরূপ, যদি আপনার কাছে একটি Option
-এর পরিবর্তে একটি টাইপ থাকে, আপনার প্রোগ্রাম কিছু না থাকার পরিবর্তে কিছু থাকার আশা করে। আপনার কোডকে তখন Some
এবং None
ভ্যারিয়েন্টের জন্য দুটি কেস হ্যান্ডেল করতে হবে না: এটি শুধুমাত্র নিশ্চিতভাবে একটি মান থাকার জন্য একটি কেস থাকবে। আপনার ফাংশনে কিছুই পাস করার চেষ্টা করা কোড এমনকি কম্পাইলও হবে না, তাই আপনার ফাংশনকে রানটাইমে সেই কেসটি পরীক্ষা করতে হবে না। আরেকটি উদাহরণ হলো একটি আনসাইন্ড ইন্টিজার টাইপ যেমন u32
ব্যবহার করা, যা নিশ্চিত করে যে প্যারামিটারটি কখনই নেতিবাচক নয়।
বৈধতা যাচাইয়ের জন্য কাস্টম টাইপ তৈরি করা (Creating Custom Types for Validation)
আসুন আমরা একটি বৈধ মান নিশ্চিত করার জন্য Rust-এর টাইপ সিস্টেম ব্যবহার করার ধারণাটিকে এক ধাপ এগিয়ে নিয়ে যাই এবং বৈধতা যাচাইয়ের জন্য একটি কাস্টম টাইপ তৈরি করার দিকে নজর দিই। অধ্যায় ২-এর অনুমান করার গেমটি মনে করুন যেখানে আমাদের কোড ব্যবহারকারীকে ১ থেকে ১০০-এর মধ্যে একটি সংখ্যা অনুমান করতে বলেছিল। আমরা আমাদের গোপন সংখ্যার সাথে এটি পরীক্ষা করার আগে ব্যবহারকারীর অনুমানটি সেই সংখ্যাগুলির মধ্যে ছিল কিনা তা কখনই যাচাই করিনি; আমরা কেবল যাচাই করেছি যে অনুমানটি পজিটিভ ছিল। এই ক্ষেত্রে, পরিণতি খুব গুরুতর ছিল না: আমাদের "Too high" বা "Too low" আউটপুট এখনও সঠিক হতো। কিন্তু ব্যবহারকারীকে বৈধ অনুমানের দিকে পরিচালিত করা এবং ব্যবহারকারী যখন সীমার বাইরের একটি সংখ্যা অনুমান করে তার জন্য ভিন্ন আচরণ করা একটি দরকারী উন্নতি হবে, যখন ব্যবহারকারী, উদাহরণস্বরূপ, অক্ষরের পরিবর্তে অক্ষর টাইপ করে।
এটি করার একটি উপায় হতে পারে অনুমানটিকে শুধুমাত্র একটি u32
এর পরিবর্তে একটি i32
হিসাবে পার্স করা যাতে সম্ভাব্য নেতিবাচক সংখ্যা அனுமதிக்க হয়, এবং তারপর সংখ্যাটি সীমার মধ্যে আছে কিনা তার জন্য একটি চেক যোগ করা, যেমন:
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 {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
if
এক্সপ্রেশনটি পরীক্ষা করে যে আমাদের মান সীমার বাইরে কিনা, ব্যবহারকারীকে সমস্যা সম্পর্কে জানায়, এবং লুপের পরবর্তী পুনরাবৃত্তি শুরু করতে এবং আরেকটি অনুমানের জন্য জিজ্ঞাসা করতে continue
কল করে। if
এক্সপ্রেশনের পরে, আমরা guess
এবং গোপন সংখ্যার মধ্যে তুলনা নিয়ে এগিয়ে যেতে পারি এটা জেনে যে guess
১ থেকে ১০০-এর মধ্যে রয়েছে।
তবে, এটি একটি আদর্শ সমাধান নয়: যদি এটি একেবারে গুরুত্বপূর্ণ হতো যে প্রোগ্রামটি শুধুমাত্র ১ থেকে ১০০-এর মধ্যে মান নিয়ে কাজ করবে, এবং এটির এই প্রয়োজনীয়তা সহ অনেক ফাংশন থাকত, তবে প্রতিটি ফাংশনে এইরকম একটি চেক থাকা ক্লান্তিকর হতো (এবং পারফরম্যান্সের উপর প্রভাব ফেলতে পারতো)।
পরিবর্তে, আমরা একটি ডেডিকেটেড মডিউলে একটি নতুন টাইপ তৈরি করতে পারি এবং বৈধতা যাচাইগুলি সর্বত্র পুনরাবৃত্তি করার পরিবর্তে টাইপের একটি ইনস্ট্যান্স তৈরি করার জন্য একটি ফাংশনে রাখতে পারি। এইভাবে, ফাংশনগুলির জন্য তাদের সিগনেচারে নতুন টাইপ ব্যবহার করা এবং তারা যে মানগুলি পায় তা আত্মবিশ্বাসের সাথে ব্যবহার করা নিরাপদ। লিস্টিং ৯-১৩ একটি Guess
টাইপ সংজ্ঞায়িত করার একটি উপায় দেখায় যা শুধুমাত্র তখনই Guess
-এর একটি ইনস্ট্যান্স তৈরি করবে যদি new
ফাংশনটি ১ থেকে ১০০-এর মধ্যে একটি মান পায়।
#![allow(unused)] fn main() { pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!("Guess value must be between 1 and 100, got {value}."); } Guess { value } } pub fn value(&self) -> i32 { self.value } } }
উল্লেখ্য যে src/guessing_game.rs-এর এই কোডটি src/lib.rs-এ একটি মডিউল ডিক্লারেশন mod guessing_game;
যোগ করার উপর নির্ভর করে যা আমরা এখানে দেখাইনি। এই নতুন মডিউলের ফাইলের মধ্যে, আমরা সেই মডিউলে Guess
নামে একটি struct সংজ্ঞায়িত করি যার একটি value
নামের ফিল্ড আছে যা একটি i32
ধারণ করে। এখানেই সংখ্যাটি সংরক্ষণ করা হবে।
তারপর আমরা Guess
-এর উপর new
নামে একটি অ্যাসোসিয়েটেড ফাংশন ইমপ্লিমেন্ট করি যা Guess
মানের ইনস্ট্যান্স তৈরি করে। new
ফাংশনটি value
নামে একটি প্যারামিটার থাকার জন্য সংজ্ঞায়িত করা হয়েছে যার টাইপ i32
এবং একটি Guess
রিটার্ন করার জন্য। new
ফাংশনের বডির কোডটি value
পরীক্ষা করে নিশ্চিত করে যে এটি ১ থেকে ১০০-এর মধ্যে আছে। যদি value
এই পরীক্ষাটি পাস না করে, আমরা একটি panic!
কল করি, যা কলিং কোড লিখছেন এমন প্রোগ্রামারকে সতর্ক করবে যে তাদের একটি বাগ আছে যা তাদের ঠিক করতে হবে, কারণ এই সীমার বাইরের একটি value
দিয়ে একটি Guess
তৈরি করা Guess::new
যে চুক্তির উপর নির্ভর করছে তা লঙ্ঘন করবে। Guess::new
যে শর্তে প্যানিক করতে পারে তা তার পাবলিক-ফেসিং API ডকুমেন্টেশনে আলোচনা করা উচিত; আমরা অধ্যায় ১৪-তে আপনার তৈরি করা API ডকুমেন্টেশনে একটি panic!
-এর সম্ভাবনা নির্দেশকারী ডকুমেন্টেশন কনভেনশনগুলি কভার করব। যদি value
পরীক্ষাটি পাস করে, আমরা একটি নতুন Guess
তৈরি করি যার value
ফিল্ড value
প্যারামিটারে সেট করা হয় এবং Guess
রিটার্ন করি।
এর পরে, আমরা value
নামে একটি মেথড ইমপ্লিমেন্ট করি যা self
ধার নেয়, অন্য কোনো প্যারামিটার নেই, এবং একটি i32
রিটার্ন করে। এই ধরনের মেথডকে কখনও কখনও getter বলা হয় কারণ এর উদ্দেশ্য হলো এর ফিল্ড থেকে কিছু ডেটা পাওয়া এবং তা রিটার্ন করা। এই পাবলিক মেথডটি প্রয়োজনীয় কারণ Guess
struct-এর value
ফিল্ডটি প্রাইভেট। এটা গুরুত্বপূর্ণ যে value
ফিল্ডটি প্রাইভেট হোক যাতে Guess
struct ব্যবহারকারী কোড সরাসরি value
সেট করতে அனுமதிக்க না হয়: guessing_game
মডিউলের বাইরের কোডকে একটি Guess
-এর ইনস্ট্যান্স তৈরি করার জন্য অবশ্যই Guess::new
ফাংশন ব্যবহার করতে হবে, যার ফলে নিশ্চিত করা হয় যে Guess::new
ফাংশনের শর্ত দ্বারা পরীক্ষা করা হয়নি এমন কোনো value
সহ একটি Guess
থাকার কোনো উপায় নেই।
একটি ফাংশন যার একটি প্যারামিটার আছে বা শুধুমাত্র ১ থেকে ১০০-এর মধ্যে সংখ্যা রিটার্ন করে, সে তার সিগনেচারে ঘোষণা করতে পারে যে এটি একটি i32
-এর পরিবর্তে একটি Guess
নেয় বা রিটার্ন করে এবং তার বডিতে কোনো অতিরিক্ত চেক করার প্রয়োজন হবে না।
সারাংশ (Summary)
Rust-এর এরর-হ্যান্ডলিং ফিচারগুলি আপনাকে আরও শক্তিশালী কোড লিখতে সাহায্য করার জন্য ডিজাইন করা হয়েছে। panic!
ম্যাক্রো সংকেত দেয় যে আপনার প্রোগ্রামটি এমন একটি অবস্থায় আছে যা এটি হ্যান্ডেল করতে পারে না এবং অবৈধ বা ভুল মান নিয়ে এগিয়ে যাওয়ার চেষ্টা করার পরিবর্তে আপনাকে প্রসেসটি বন্ধ করতে বলে। Result
enum Rust-এর টাইপ সিস্টেম ব্যবহার করে নির্দেশ করে যে অপারেশনগুলি এমনভাবে ব্যর্থ হতে পারে যা থেকে আপনার কোড পুনরুদ্ধার করতে পারে। আপনি Result
ব্যবহার করে আপনার কোড কলকারী কোডকে বলতে পারেন যে তাকেও সম্ভাব্য সফলতা বা ব্যর্থতা হ্যান্ডেল করতে হবে। উপযুক্ত পরিস্থিতিতে panic!
এবং Result
ব্যবহার করা আপনার কোডকে অনিবার্য সমস্যার মুখে আরও নির্ভরযোগ্য করে তুলবে।
এখন যেহেতু আপনি দেখেছেন যে স্ট্যান্ডার্ড লাইব্রেরি Option
এবং Result
enum-এর সাথে জেনেরিকগুলি কীভাবে দরকারী উপায়ে ব্যবহার করে, আমরা আলোচনা করব জেনেরিকগুলি কীভাবে কাজ করে এবং আপনি কীভাবে সেগুলি আপনার কোডে ব্যবহার করতে পারেন।
জেনেরিক টাইপ, ট্রেইট এবং লাইফটাইম
প্রত্যেক প্রোগ্রামিং ল্যাঙ্গুয়েজেই ধারণার পুনরাবৃত্তি (duplication of concepts) কার্যকরভাবে পরিচালনা করার জন্য বিভিন্ন টুল থাকে। Rust-এ, এরকম একটি টুল হলো generics: যা concrete type বা অন্যান্য properties-এর জন্য ব্যবহৃত abstract stand-ins। কোড কম্পাইল এবং রান করার সময় generics-এর জায়গায় কী থাকবে তা না জেনেই আমরা তাদের আচরণ বা অন্য generics-এর সাথে তাদের সম্পর্ক প্রকাশ করতে পারি।
ফাংশনগুলো কোনো concrete type যেমন i32
বা String
-এর পরিবর্তে কোনো generic type-এর প্যারামিটার নিতে পারে, ঠিক যেমনভাবে তারা অজানা মানসহ প্যারামিটার নিয়ে একাধিক concrete value-এর উপর একই কোড চালায়। সত্যি বলতে, আমরা ইতোমধ্যে চ্যাপ্টার ৬-এ Option<T>
, চ্যাপ্টার ৮-এ Vec<T>
এবং HashMap<K, V>
, এবং চ্যাপ্টার ৯-এ Result<T, E>
-এর সাথে generics ব্যবহার করেছি। এই চ্যাপ্টারে, আপনি generics ব্যবহার করে কীভাবে নিজের টাইপ, ফাংশন এবং মেথড ডিফাইন করবেন তা শিখবেন!
প্রথমে আমরা কোডের পুনরাবৃত্তি কমাতে কীভাবে একটি ফাংশন এক্সট্র্যাক্ট করা যায় তা পর্যালোচনা করব। এরপর আমরা একই কৌশল ব্যবহার করে দুটি ফাংশন থেকে একটি জেনেরিক ফাংশন তৈরি করব, যেখানে ফাংশন দুটির মধ্যে শুধুমাত্র তাদের প্যারামিটারের টাইপ ভিন্ন থাকবে। আমরা struct এবং enum ডেফিনিশনে কীভাবে জেনেরিক টাইপ ব্যবহার করতে হয় তাও ব্যাখ্যা করব।
এরপর আপনি traits ব্যবহার করে কীভাবে জেনেরিক উপায়ে আচরণ (behavior) ডিফাইন করতে হয় তা শিখবেন। আপনি generic type-এর সাথে traits যুক্ত করে একটি generic type-কে সীমাবদ্ধ করতে পারেন, যাতে এটি যেকোনো টাইপের পরিবর্তে শুধুমাত্র নির্দিষ্ট আচরণযুক্ত টাইপ গ্রহণ করে।
সবশেষে, আমরা lifetimes নিয়ে আলোচনা করব: এটি এক ধরনের generics যা কম্পাইলারকে রেফারেন্সগুলো একে অপরের সাথে কীভাবে সম্পর্কিত সে সম্পর্কে তথ্য দেয়। Lifetimes ব্যবহার করে আমরা কম্পাইলারকে ধার করা মান (borrowed values) সম্পর্কে যথেষ্ট তথ্য দিতে পারি, যাতে এটি নিশ্চিত করতে পারে যে রেফারেন্সগুলো আমাদের সাহায্য ছাড়াই যতটা সম্ভব তার চেয়ে বেশি পরিস্থিতিতে ভ্যালিড থাকবে।
ফাংশন এক্সট্র্যাক্ট করে কোডের পুনরাবৃত্তি দূর করা
Generics আমাদের কোডের পুনরাবৃত্তি দূর করার জন্য নির্দিষ্ট টাইপের পরিবর্তে একটি প্লেসহোল্ডার ব্যবহার করার সুযোগ দেয় যা একাধিক টাইপকে উপস্থাপন করে। Generics সিনট্যাক্সে যাওয়ার আগে, চলুন প্রথমে দেখি কীভাবে জেনেরিক টাইপ ব্যবহার না করে কোডের পুনরাবৃত্তি দূর করা যায়। এর জন্য আমরা একটি ফাংশন এক্সট্র্যাক্ট করব যা নির্দিষ্ট মানের পরিবর্তে এমন একটি প্লেসহোল্ডার ব্যবহার করবে যা একাধিক মানকে উপস্থাপন করে। তারপর আমরা একই কৌশল প্রয়োগ করে একটি জেনেরিক ফাংশন এক্সট্র্যাক্ট করব! ডুপ্লিকেট কোড চিনে তাকে কীভাবে একটি ফাংশনে এক্সট্র্যাক্ট করা যায় তা দেখলে, আপনি ডুপ্লিকেট কোড চেনা শুরু করবেন যেখানে generics ব্যবহার করা যেতে পারে।
আমরা লিস্টিং ১০-১ এর একটি ছোট প্রোগ্রাম দিয়ে শুরু করব যা একটি তালিকা থেকে সবচেয়ে বড় সংখ্যাটি খুঁজে বের করে।
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); assert_eq!(*largest, 100); }
আমরা number_list
ভ্যারিয়েবলে পূর্ণসংখ্যার একটি তালিকা রাখি এবং তালিকার প্রথম সংখ্যার একটি রেফারেন্স largest
নামের একটি ভ্যারিয়েবলে রাখি। তারপর আমরা তালিকার সমস্ত সংখ্যার উপর দিয়ে ইটারেট করি, এবং যদি বর্তমান সংখ্যাটি largest
-এ থাকা সংখ্যার চেয়ে বড় হয়, আমরা সেই ভ্যারিয়েবলের রেফারেন্সটি প্রতিস্থাপন করি। কিন্তু, যদি বর্তমান সংখ্যাটি এখন পর্যন্ত দেখা সবচেয়ে বড় সংখ্যার চেয়ে ছোট বা সমান হয়, ভ্যারিয়েবলটি পরিবর্তন হয় না এবং কোডটি তালিকার পরবর্তী সংখ্যায় চলে যায়। তালিকার সমস্ত সংখ্যা বিবেচনা করার পর, largest
-কে সবচেয়ে বড় সংখ্যাটিকে রেফার করা উচিত, যা এই ক্ষেত্রে ১০০।
এখন আমাদের দুটি ভিন্ন সংখ্যার তালিকা থেকে সবচেয়ে বড় সংখ্যাটি খুঁজে বের করার দায়িত্ব দেওয়া হয়েছে। এটি করার জন্য, আমরা লিস্টিং ১০-১ এর কোডটি ডুপ্লিকেট করতে পারি এবং প্রোগ্রামের দুটি ভিন্ন জায়গায় একই লজিক ব্যবহার করতে পারি, যেমনটি লিস্টিং ১০-২ এ দেখানো হয়েছে।
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); }
যদিও এই কোডটি কাজ করে, কোড ডুপ্লিকেট করা ক্লান্তিকর এবং ভুল হওয়ার সম্ভাবনা থাকে। আমরা যখন কোড পরিবর্তন করতে চাই, তখন একাধিক জায়গায় এটি আপডেট করার কথাও মনে রাখতে হয়।
এই পুনরাবৃত্তি দূর করার জন্য, আমরা একটি অ্যাবস্ট্র্যাকশন তৈরি করব একটি ফাংশন ডিফাইন করে যা প্যারামিটার হিসাবে পাস করা যেকোনো পূর্ণসংখ্যার তালিকার উপর কাজ করে। এই সমাধানটি আমাদের কোডকে আরও পরিষ্কার করে এবং একটি তালিকা থেকে সবচেয়ে বড় সংখ্যা খুঁজে বের করার ধারণাটিকে অ্যাবস্ট্র্যাক্টভাবে প্রকাশ করতে দেয়।
লিস্টিং ১০-৩ এ, আমরা সবচেয়ে বড় সংখ্যা খোঁজার কোডটিকে largest
নামের একটি ফাংশনে এক্সট্র্যাক্ট করেছি। তারপর আমরা লিস্টিং ১০-২ এর দুটি তালিকা থেকে সবচেয়ে বড় সংখ্যাটি খুঁজে বের করার জন্য ফাংশনটিকে কল করি। আমরা ভবিষ্যতে আমাদের কাছে থাকা যেকোনো i32
মানের তালিকার জন্যও ফাংশনটি ব্যবহার করতে পারতাম।
fn largest(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 6000); }
largest
ফাংশনটির list
নামে একটি প্যারামিটার রয়েছে, যা আমরা ফাংশনে পাস করতে পারি এমন যেকোনো i32
মানের concrete slice-কে প্রতিনিধিত্ব করে। ফলস্বরূপ, যখন আমরা ফাংশনটি কল করি, কোডটি আমাদের পাস করা নির্দিষ্ট মানগুলোর উপর চলে।
সংক্ষেপে, লিস্টিং ১০-২ থেকে লিস্টিং ১০-৩ এ কোড পরিবর্তন করার জন্য আমরা যে পদক্ষেপগুলো নিয়েছি তা হলো:
- ডুপ্লিকেট কোড শনাক্ত করুন।
- ডুপ্লিকেট কোডটি ফাংশনের বডিতে এক্সট্র্যাক্ট করুন, এবং ফাংশন সিগনেচারে সেই কোডের ইনপুট এবং রিটার্ন ভ্যালু উল্লেখ করুন।
- ডুপ্লিকেট কোডের দুটি ইনস্ট্যান্সকে ফাংশন কল করার জন্য আপডেট করুন।
এরপর, আমরা কোডের পুনরাবৃত্তি কমাতে generics-এর সাথে এই একই পদক্ষেপগুলো ব্যবহার করব। ঠিক যেমন ফাংশন বডি নির্দিষ্ট মানের পরিবর্তে একটি অ্যাবস্ট্র্যাক্ট list
-এর উপর কাজ করতে পারে, তেমনি generics কোডকে অ্যাবস্ট্র্যাক্ট টাইপের উপর কাজ করার অনুমতি দেয়।
উদাহরণস্বরূপ, ধরুন আমাদের দুটি ফাংশন ছিল: একটি যা i32
মানের একটি স্লাইস থেকে সবচেয়ে বড় আইটেম খুঁজে বের করে এবং অন্যটি যা char
মানের একটি স্লাইস থেকে সবচেয়ে বড় আইটেম খুঁজে বের করে। আমরা কীভাবে সেই পুনরাবৃত্তি দূর করব? চলুন খুঁজে বের করা যাক
জেনেরিক ডেটা টাইপ
আমরা ফাংশন সিগনেচার বা struct-এর মতো আইটেমগুলোর জন্য ডেফিনিশন তৈরি করতে generics ব্যবহার করি, যা আমরা পরে বিভিন্ন concrete ডেটা টাইপের সাথে ব্যবহার করতে পারি। চলুন প্রথমে দেখি কিভাবে generics ব্যবহার করে ফাংশন, struct, enum, এবং মেথড ডিফাইন করা যায়। তারপর আমরা আলোচনা করব generics কীভাবে কোডের পারফরম্যান্সকে প্রভাবিত করে।
ফাংশন ডেফিনিশনে
যখন আমরা generics ব্যবহার করে এমন একটি ফাংশন ডিফাইন করি, তখন আমরা ফাংশনের সিগনেচারে generics গুলোকে রাখি, যেখানে আমরা সাধারণত প্যারামিটার এবং রিটার্ন ভ্যালুর ডেটা টাইপ নির্দিষ্ট করি। এভাবে কোড লিখলে আমাদের কোড আরও বেশি ফ্লেক্সিবল হয়, কোডের পুনরাবৃত্তি রোধ করে এবং ফাংশন ব্যবহারকারীদের জন্য আরও বেশি কার্যকারিতা প্রদান করে।
আমাদের largest
ফাংশনটি নিয়ে কাজ করা যাক। লিস্টিং ১০-৪ এ দুটি ফাংশন দেখানো হয়েছে যারা উভয়েই একটি স্লাইসের মধ্যে সবচেয়ে বড় মান খুঁজে বের করে। এরপর আমরা এদেরকে generics ব্যবহার করে একটি একক ফাংশনে একত্রিত করব।
fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {result}"); assert_eq!(*result, 'y'); }``` </Listing> `largest_i32` ফাংশনটি আমরা লিস্টিং ১০-৩ এ এক্সট্র্যাক্ট করেছিলাম, যা একটি স্লাইস থেকে সবচেয়ে বড় `i32` খুঁজে বের করে। `largest_char` ফাংশনটি একটি স্লাইস থেকে সবচেয়ে বড় `char` খুঁজে বের করে। দুটি ফাংশনের বডি একই কোড ধারণ করে, তাই আসুন একটি জেনেরিক টাইপ প্যারামিটার ব্যবহার করে একটি একক ফাংশন তৈরি করে এই পুনরাবৃত্তি দূর করি। একটি নতুন একক ফাংশনে টাইপগুলোকে প্যারামিটারাইজ করার জন্য, আমাদের টাইপ প্যারামিটারের একটি নাম দিতে হবে, ঠিক যেমন আমরা একটি ফাংশনের ভ্যালু প্যারামিটারের জন্য নাম দিই। আপনি টাইপ প্যারামিটারের নাম হিসেবে যেকোনো আইডেন্টিফায়ার ব্যবহার করতে পারেন। কিন্তু আমরা `T` ব্যবহার করব কারণ, প্রথা অনুযায়ী, Rust-এ টাইপ প্যারামিটারের নাম ছোট হয়, প্রায়শই কেবল একটি অক্ষর, এবং Rust-এর টাইপ-নামকরণের প্রথা হলো CamelCase। _type_-এর সংক্ষিপ্ত রূপ হিসেবে `T` বেশিরভাগ Rust প্রোগ্রামারদের প্রথম পছন্দ। যখন আমরা ফাংশনের বডিতে একটি প্যারামিটার ব্যবহার করি, তখন আমাদের সিগনেচারে প্যারামিটারের নামটি ডিক্লেয়ার করতে হয় যাতে কম্পাইলার জানে সেই নামের অর্থ কী। একইভাবে, যখন আমরা একটি ফাংশন সিগনেচারে একটি টাইপ প্যারামিটারের নাম ব্যবহার করি, তখন ব্যবহারের আগে আমাদের টাইপ প্যারামিটারের নামটি ডিক্লেয়ার করতে হয়। জেনেরিক `largest` ফাংশনটি ডিফাইন করতে, আমরা ফাংশনের নাম এবং প্যারামিটার তালিকার মধ্যে অ্যাঙ্গেল ব্র্যাকেট `<>`-এর ভিতরে টাইপের নাম ডিক্লেয়ার করি, এভাবে: ```rust,ignore fn largest<T>(list: &[T]) -> &T {
এই ডেফিনিশনটিকে আমরা এভাবে পড়ি: largest
ফাংশনটি কোনো একটি টাইপ T
-এর উপর জেনেরিক। এই ফাংশনের list
নামে একটি প্যারামিটার আছে, যা T
টাইপের ভ্যালুগুলোর একটি স্লাইস। largest
ফাংশনটি একই টাইপ T
-এর একটি ভ্যালুর রেফারেন্স রিটার্ন করবে।
লিস্টিং ১০-৫ এ জেনেরিক ডেটা টাইপ ব্যবহার করে সম্মিলিত largest
ফাংশনের ডেফিনিশন দেখানো হয়েছে। লিস্টিংটি আরও দেখায় যে আমরা কীভাবে i32
মানের স্লাইস বা char
মানের স্লাইস দিয়ে ফাংশনটি কল করতে পারি। মনে রাখবেন যে এই কোডটি এখনও কম্পাইল হবে না।
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
যদি আমরা এই কোডটি এখনই কম্পাইল করি, আমরা এই এররটি পাব:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
হেল্প টেক্সট-এ std::cmp::PartialOrd
-এর কথা উল্লেখ করা হয়েছে, যা একটি trait, এবং আমরা পরবর্তী সেকশনে traits নিয়ে কথা বলব। আপাতত, জেনে রাখুন যে এই এররটি বলছে যে largest
ফাংশনের বডি T
-এর সম্ভাব্য সকল টাইপের জন্য কাজ করবে না। যেহেতু আমরা বডিতে T
টাইপের মান তুলনা করতে চাই, তাই আমরা কেবল সেই টাইপগুলো ব্যবহার করতে পারি যাদের মান வரிசை অনুযায়ী সাজানো (ordered) যায়। তুলনা সক্রিয় করার জন্য, স্ট্যান্ডার্ড লাইব্রেরিতে std::cmp::PartialOrd
trait রয়েছে যা আপনি টাইপগুলিতে ইমপ্লিমেন্ট করতে পারেন (এই trait সম্পর্কে আরও জানতে Appendix C দেখুন)। লিস্টিং ১০-৫ ঠিক করার জন্য, আমরা হেল্প টেক্সট-এর পরামর্শ অনুসরণ করতে পারি এবং T
-এর জন্য বৈধ টাইপগুলোকে কেবল তাদের মধ্যে সীমাবদ্ধ রাখতে পারি যারা PartialOrd
ইমপ্লিমেন্ট করে। এরপর লিস্টিংটি কম্পাইল হবে, কারণ স্ট্যান্ডার্ড লাইব্রেরি i32
এবং char
উভয়ের উপরেই PartialOrd
ইমপ্লিমেন্ট করে।
Struct ডেফিনিশনে
আমরা <>
সিনট্যাক্স ব্যবহার করে এক বা একাধিক ফিল্ডে জেনেরিক টাইপ প্যারামিটার ব্যবহার করার জন্য struct ডিফাইন করতে পারি। লিস্টিং ১০-৬ একটি Point<T>
struct ডিফাইন করে যা যেকোনো টাইপের x
এবং y
কো-অর্ডিনেট ভ্যালু ধারণ করে।
struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
struct ডেফিনিশনে generics ব্যবহারের সিনট্যাক্স ফাংশন ডেফিনিশনে ব্যবহারের মতোই। প্রথমে আমরা struct-এর নামের ঠিক পরে অ্যাঙ্গেল ব্র্যাকেটের মধ্যে টাইপ প্যারামিটারের নাম ডিক্লেয়ার করি। তারপর আমরা struct ডেফিনিশনের মধ্যে জেনেরিক টাইপ ব্যবহার করি যেখানে আমরা অন্যথায় concrete ডেটা টাইপ নির্দিষ্ট করতাম।
মনে রাখবেন যে আমরা Point<T>
ডিফাইন করতে কেবল একটি জেনেরিক টাইপ ব্যবহার করেছি, তাই এই ডেফিনিশনটি বলে যে Point<T>
struct-টি কোনো একটি টাইপ T
-এর উপর জেনেরিক, এবং x
ও y
ফিল্ড দুটি উভয়ই সেই একই টাইপের, টাইপটি যা-ই হোক না কেন। যদি আমরা ভিন্ন টাইপের মান দিয়ে একটি Point<T>
-এর ইনস্ট্যান্স তৈরি করি, যেমন লিস্টিং ১০-৭-এ, আমাদের কোড কম্পাইল হবে না।
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
এই উদাহরণে, যখন আমরা x
-কে ইন্টিজার ভ্যালু 5
অ্যাসাইন করি, তখন আমরা কম্পাইলারকে জানাই যে Point<T>
-এর এই ইনস্ট্যান্সের জন্য জেনেরিক টাইপ T
একটি ইন্টিজার হবে। তারপর যখন আমরা y
-এর জন্য 4.0
নির্দিষ্ট করি, যা আমরা x
-এর মতো একই টাইপের হিসেবে ডিফাইন করেছি, তখন আমরা একটি টাইপ মিসম্যাচ এরর পাব, যেমন:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
x
এবং y
উভয়ই জেনেরিক কিন্তু ভিন্ন টাইপের হতে পারে এমন একটি Point
struct ডিফাইন করতে, আমরা একাধিক জেনেরিক টাইপ প্যারামিটার ব্যবহার করতে পারি। উদাহরণস্বরূপ, লিস্টিং ১০-৮-এ, আমরা Point
-এর ডেফিনিশন পরিবর্তন করে T
এবং U
দুটি টাইপের উপর জেনেরিক করেছি, যেখানে x
-এর টাইপ T
এবং y
-এর টাইপ U
।
struct Point<T, U> { x: T, y: U, } fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 }; }
এখন দেখানো Point
-এর সমস্ত ইনস্ট্যান্স অনুমোদিত! আপনি একটি ডেফিনিশনে যত খুশি জেনেরিক টাইপ প্যারামিটার ব্যবহার করতে পারেন, তবে অল্প কয়েকটির বেশি ব্যবহার করলে আপনার কোড পড়া কঠিন হয়ে যায়। যদি আপনার কোডে অনেক জেনেরিক টাইপের প্রয়োজন হয়, তবে এটি ইঙ্গিত দিতে পারে যে আপনার কোডকে ছোট ছোট অংশে পুনর্গঠন করা প্রয়োজন।
Enum ডেফিনিশনে
যেমনটি আমরা struct-এর সাথে করেছি, আমরা enum-কেও তাদের ভ্যারিয়েন্টে জেনেরিক ডেটা টাইপ ধারণ করার জন্য ডিফাইন করতে পারি। আসুন আমরা স্ট্যান্ডার্ড লাইব্রেরির দেওয়া Option<T>
enum-টি আবার দেখি, যা আমরা চ্যাপ্টার ৬-এ ব্যবহার করেছি:
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
এই ডেফিনিশনটি এখন আপনার কাছে আরও বেশি অর্থবহ মনে হওয়া উচিত। जैसा कि आप देख सकते हैं, Option<T>
enum টি T
টাইপের উপর জেনেরিক এবং এর দুটি ভ্যারিয়েন্ট রয়েছে: Some
, যা T
টাইপের একটি মান ধারণ করে, এবং None
ভ্যারিয়েন্ট যা কোনো মান ধারণ করে না। Option<T>
enum ব্যবহার করে, আমরা একটি ঐচ্ছিক মানের অ্যাবস্ট্রাক্ট ধারণা প্রকাশ করতে পারি, এবং যেহেতু Option<T>
জেনেরিক, তাই ঐচ্ছিক মানের টাইপ যা-ই হোক না কেন, আমরা এই অ্যাবস্ট্রাকশনটি ব্যবহার করতে পারি।
Enum একাধিক জেনেরিক টাইপও ব্যবহার করতে পারে। Result
enum-এর ডেফিনিশন, যা আমরা চ্যাপ্টার ৯-এ ব্যবহার করেছি, এর একটি উদাহরণ:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
Result
enum-টি T
এবং E
দুটি টাইপের উপর জেনেরিক, এবং এর দুটি ভ্যারিয়েন্ট রয়েছে: Ok
, যা T
টাইপের একটি মান ধারণ করে, এবং Err
, যা E
টাইপের একটি মান ধারণ করে। এই ডেফিনিশনটি Result
enum ব্যবহার করা সুবিধাজনক করে তোলে যেখানেই আমাদের এমন কোনো অপারেশন থাকে যা সফল হতে পারে (T
টাইপের কোনো মান রিটার্ন করে) বা ব্যর্থ হতে পারে (E
টাইপের কোনো এরর রিটার্ন করে)। প্রকৃতপক্ষে, এটিই আমরা লিস্টিং ৯-৩-এ একটি ফাইল খোলার জন্য ব্যবহার করেছিলাম, যেখানে ফাইলটি সফলভাবে খোলা হলে T
-কে std::fs::File
টাইপ দিয়ে পূরণ করা হয়েছিল এবং ফাইল খুলতে সমস্যা হলে E
-কে std::io::Error
টাইপ দিয়ে পূরণ করা হয়েছিল।
যখন আপনি আপনার কোডে এমন পরিস্থিতি শনাক্ত করেন যেখানে একাধিক struct বা enum ডেফিনিশন রয়েছে যা কেবল তাদের ধারণ করা মানের টাইপের দিক থেকে ভিন্ন, তখন আপনি জেনেরিক টাইপ ব্যবহার করে পুনরাবৃত্তি এড়াতে পারেন।
মেথড ডেফিনিশনে
আমরা struct এবং enum-এর উপর মেথড ইমপ্লিমেন্ট করতে পারি (যেমনটি আমরা চ্যাপ্টার ৫-এ করেছি) এবং তাদের ডেফিনিশনেও জেনেরিক টাইপ ব্যবহার করতে পারি। লিস্টিং ১০-৯-এ আমরা লিস্টিং ১০-৬-এ ডিফাইন করা Point<T>
struct-টি দেখাচ্ছি, যার উপর x
নামের একটি মেথড ইমপ্লিমেন্ট করা হয়েছে।
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
এখানে, আমরা Point<T>
-এর উপর x
নামের একটি মেথড ডিফাইন করেছি যা x
ফিল্ডের ডেটার একটি রেফারেন্স রিটার্ন করে।
লক্ষ্য করুন যে, impl
-এর ঠিক পরেই আমাদের T
ডিক্লেয়ার করতে হবে যাতে আমরা Point<T>
টাইপের উপর মেথড ইমপ্লিমেন্ট করছি তা নির্দিষ্ট করতে T
ব্যবহার করতে পারি। impl
-এর পরে T
-কে একটি জেনেরিক টাইপ হিসেবে ডিক্লেয়ার করার মাধ্যমে, Rust শনাক্ত করতে পারে যে Point
-এর অ্যাঙ্গেল ব্র্যাকেটের মধ্যে থাকা টাইপটি একটি জেনেরিক টাইপ, কোনো concrete টাইপ নয়। আমরা struct ডেফিনিশনে ডিক্লেয়ার করা জেনেরিক প্যারামিটারের চেয়ে এই জেনেরিক প্যারামিটারের জন্য একটি ভিন্ন নাম বেছে নিতে পারতাম, কিন্তু একই নাম ব্যবহার করাই প্রচলিত। যদি আপনি একটি জেনেরিক টাইপ ডিক্লেয়ার করে এমন একটি impl
-এর মধ্যে একটি মেথড লেখেন, তবে সেই মেথডটি টাইপের যেকোনো ইনস্ট্যান্সের উপর ডিফাইন করা হবে, জেনেরিক টাইপের পরিবর্তে যে কোনো concrete টাইপই আসুক না কেন।
আমরা টাইপের উপর মেথড ডিফাইন করার সময় জেনেরিক টাইপের উপর সীমাবদ্ধতাও নির্দিষ্ট করতে পারি। উদাহরণস্বরূপ, আমরা যেকোনো জেনেরিক টাইপের Point<T>
ইনস্ট্যান্সের পরিবর্তে শুধুমাত্র Point<f32>
ইনস্ট্যান্সের উপর মেথড ইমপ্লিমেন্ট করতে পারি। লিস্টিং ১০-১০-এ আমরা concrete টাইপ f32
ব্যবহার করি, যার মানে আমরা impl
-এর পরে কোনো টাইপ ডিক্লেয়ার করি না।
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
এই কোডের মানে হলো Point<f32>
টাইপের একটি distance_from_origin
মেথড থাকবে; Point<T>
-এর অন্যান্য ইনস্ট্যান্স যেখানে T
f32
টাইপের নয়, তাদের এই মেথডটি ডিফাইন করা থাকবে না। মেথডটি আমাদের পয়েন্টটি (0.0, 0.0) কো-অর্ডিনেটের পয়েন্ট থেকে কত দূরে তা পরিমাপ করে এবং গাণিতিক অপারেশন ব্যবহার করে যা শুধুমাত্র ফ্লোটিং-পয়েন্ট টাইপের জন্য উপলব্ধ।
একটি struct ডেফিনিশনের জেনেরিক টাইপ প্যারামিটার সবসময় সেই একই struct-এর মেথড সিগনেচারে ব্যবহার করা প্যারামিটারের মতো হয় না। লিস্টিং ১০-১১ উদাহরণটিকে আরও স্পষ্ট করার জন্য Point
struct-এর জন্য X1
এবং Y1
এবং mixup
মেথড সিগনেচারের জন্য X2
Y2
জেনেরিক টাইপ ব্যবহার করে। মেথডটি self
Point
(যার টাইপ X1
) থেকে x
ভ্যালু এবং পাস করা Point
(যার টাইপ Y2
) থেকে y
ভ্যালু নিয়ে একটি নতুন Point
ইনস্ট্যান্স তৈরি করে।
struct Point<X1, Y1> { x: X1, y: Y1, } impl<X1, Y1> Point<X1, Y1> { fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c' }; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
main
ফাংশনে, আমরা একটি Point
ডিফাইন করেছি যার x
-এর জন্য একটি i32
(মান 5
) এবং y
-এর জন্য একটি f64
(মান 10.4
) রয়েছে। p2
ভ্যারিয়েবলটি একটি Point
struct যার x
-এর জন্য একটি স্ট্রিং স্লাইস (মান "Hello"
) এবং y
-এর জন্য একটি char
(মান c
) রয়েছে। p1
-এর উপর p2
আর্গুমেন্ট দিয়ে mixup
কল করলে আমরা p3
পাই, যার x
-এর জন্য একটি i32
থাকবে কারণ x
p1
থেকে এসেছে। p3
ভ্যারিয়েবলের y
-এর জন্য একটি char
থাকবে কারণ y
p2
থেকে এসেছে। println!
ম্যাক্রো কলটি p3.x = 5, p3.y = c
প্রিন্ট করবে।
এই উদাহরণের উদ্দেশ্য হলো এমন একটি পরিস্থিতি প্রদর্শন করা যেখানে কিছু জেনেরিক প্যারামিটার impl
-এর সাথে এবং কিছু মেথড ডেফিনিশনের সাথে ডিক্লেয়ার করা হয়। এখানে, জেনেরিক প্যারামিটার X1
এবং Y1
impl
-এর পরে ডিক্লেয়ার করা হয়েছে কারণ তারা struct ডেফিনিশনের সাথে যায়। জেনেরিক প্যারামিটার X2
এবং Y2
fn mixup
-এর পরে ডিক্লেয়ার করা হয়েছে কারণ তারা কেবল মেথডের জন্য প্রাসঙ্গিক।
Generics ব্যবহার করা কোডের পারফরম্যান্স
আপনি হয়তো ভাবছেন যে জেনেরিক টাইপ প্যারামিটার ব্যবহার করার সময় কোনো রানটাইম খরচ আছে কিনা। সুখবর হলো যে জেনেরিক টাইপ ব্যবহার করলে আপনার প্রোগ্রামটি concrete টাইপ ব্যবহার করার চেয়ে কোনো ধীর গতিতে চলবে না।
Rust কম্পাইল টাইমে generics ব্যবহার করা কোডের মনোমর্ফাইজেশন (monomorphization) সম্পাদন করে এটি অর্জন করে। Monomorphization হলো কম্পাইল টাইমে ব্যবহৃত concrete টাইপগুলো দিয়ে জেনেরিক কোডকে নির্দিষ্ট কোডে পরিণত করার প্রক্রিয়া। এই প্রক্রিয়ায়, কম্পাইলার আমরা লিস্টিং ১০-৫-এ জেনেরিক ফাংশন তৈরি করার জন্য যে পদক্ষেপগুলো ব্যবহার করেছি তার বিপরীত কাজ করে: কম্পাইলার সেই সমস্ত জায়গা দেখে যেখানে জেনেরিক কোড কল করা হয়েছে এবং যে concrete টাইপ দিয়ে জেনেরিক কোড কল করা হয়েছে তার জন্য কোড তৈরি করে।
আসুন দেখি এটি কীভাবে কাজ করে স্ট্যান্ডার্ড লাইব্রেরির জেনেরিক Option<T>
enum ব্যবহার করে:
#![allow(unused)] fn main() { let integer = Some(5); let float = Some(5.0); }
যখন Rust এই কোডটি কম্পাইল করে, তখন এটি মনোমর্ফাইজেশন সম্পাদন করে। সেই প্রক্রিয়ার সময়, কম্পাইলার Option<T>
ইনস্ট্যান্সে ব্যবহৃত মানগুলো পড়ে এবং দুই ধরণের Option<T>
শনাক্ত করে: একটি i32
এবং অন্যটি f64
। ফলস্বরূপ, এটি Option<T>
-এর জেনেরিক ডেফিনিশনকে i32
এবং f64
-এর জন্য বিশেষায়িত দুটি ডেফিনিশনে প্রসারিত করে, যার ফলে জেনেরিক ডেফিনিশনটিকে নির্দিষ্টগুলো দিয়ে প্রতিস্থাপন করে।
কোডের মনোমর্ফাইজড সংস্করণটি নিচের মতো দেখতে (কম্পাইলার এখানে উদাহরণের জন্য আমরা যে নামগুলো ব্যবহার করছি তার চেয়ে ভিন্ন নাম ব্যবহার করে):
enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }
জেনেরিক Option<T>
কম্পাইলার দ্বারা তৈরি করা নির্দিষ্ট ডেফিনিশন দিয়ে প্রতিস্থাপিত হয়। কারণ Rust জেনেরিক কোডকে এমন কোডে কম্পাইল করে যা প্রতিটি ইনস্ট্যান্সে টাইপ নির্দিষ্ট করে, তাই generics ব্যবহারের জন্য আমাদের কোনো রানটাইম খরচ দিতে হয় না। কোডটি যখন চলে, তখন এটি ঠিক তেমনই পারফর্ম করে যেমনটি আমরা প্রতিটি ডেফিনিশন হাতে হাতে ডুপ্লিকেট করলে করত। মনোমর্ফাইজেশন প্রক্রিয়াটি রানটাইমে Rust-এর generics-কে অত্যন্ত কার্যকরী করে তোলে।
ট্রেইট (Traits): সাধারণ আচরণ ডিফাইন করা
একটি trait কোনো নির্দিষ্ট টাইপের কার্যকারিতা (functionality) ডিফাইন করে যা অন্যান্য টাইপের সাথে শেয়ার করা যায়। আমরা অ্যাবস্ট্রাক্ট উপায়ে সাধারণ আচরণ (shared behavior) ডিফাইন করতে traits ব্যবহার করতে পারি। আমরা trait bounds ব্যবহার করে নির্দিষ্ট করতে পারি যে একটি জেনেরিক টাইপ যেকোনো টাইপের হতে পারে, যতক্ষণ পর্যন্ত তার একটি নির্দিষ্ট আচরণ থাকে।
দ্রষ্টব্য: Traits অন্যান্য ভাষার interfaces নামক একটি ফিচারের মতো, যদিও কিছু পার্থক্য রয়েছে।
একটি ট্রেইট ডিফাইন করা
একটি টাইপের আচরণ হলো সেইসব মেথড যা আমরা সেই টাইপের উপর কল করতে পারি। বিভিন্ন টাইপ একই আচরণ শেয়ার করে যদি আমরা সেই সব টাইপের উপর একই মেথড কল করতে পারি। Trait ডেফিনিশন হলো মেথড সিগনেচারগুলোকে একসাথে গ্রুপ করার একটি উপায়, যা কোনো উদ্দেশ্য পূরণের জন্য প্রয়োজনীয় আচরণের একটি সেট ডিফাইন করে।
উদাহরণস্বরূপ, ধরা যাক আমাদের একাধিক struct আছে যা বিভিন্ন ধরনের এবং পরিমাণের টেক্সট ধারণ করে: একটি NewsArticle
struct যা একটি নির্দিষ্ট অবস্থানে ফাইল করা সংবাদ ধরে রাখে এবং একটি SocialPost
যা সর্বোচ্চ ২৮০ অক্ষর ধারণ করতে পারে, সাথে মেটাডেটা যা নির্দেশ করে এটি একটি নতুন পোস্ট, একটি রিপোস্ট, বা অন্য কোনো পোস্টের উত্তর ছিল কিনা।
আমরা aggregator
নামে একটি মিডিয়া অ্যাগ্রিগেটর লাইব্রেরি ক্রেট তৈরি করতে চাই যা NewsArticle
বা SocialPost
ইনস্ট্যান্সে সংরক্ষিত ডেটার সারাংশ প্রদর্শন করতে পারে। এটি করার জন্য, আমাদের প্রতিটি টাইপ থেকে একটি সারাংশ প্রয়োজন, এবং আমরা একটি ইনস্ট্যান্সের উপর summarize
মেথড কল করে সেই সারাংশটি অনুরোধ করব। লিস্টিং ১০-১২ একটি পাবলিক Summary
trait-এর ডেফিনিশন দেখায় যা এই আচরণটি প্রকাশ করে।
pub trait Summary {
fn summarize(&self) -> String;
}
এখানে, আমরা trait
কিওয়ার্ড ব্যবহার করে একটি trait ডিক্লেয়ার করি এবং তারপর trait-এর নাম, যা এই ক্ষেত্রে Summary
। আমরা trait-টিকে pub
হিসেবেও ডিক্লেয়ার করি যাতে এই ক্রেটের উপর নির্ভরশীল ক্রেটগুলোও এই trait ব্যবহার করতে পারে, যেমনটি আমরা কয়েকটি উদাহরণে দেখব। কার্লি ব্র্যাকেটের ভিতরে, আমরা মেথড সিগনেচারগুলো ডিক্লেয়ার করি যা এই trait ইমপ্লিমেন্ট করা টাইপগুলোর আচরণ বর্ণনা করে, যা এই ক্ষেত্রে fn summarize(&self) -> String
।
মেথড সিগনেচারের পরে, কার্লি ব্র্যাকেটের মধ্যে একটি ইমপ্লিমেন্টেশন প্রদান করার পরিবর্তে, আমরা একটি সেমিকোলন ব্যবহার করি। এই trait ইমপ্লিমেন্ট করা প্রতিটি টাইপকে অবশ্যই মেথডের বডির জন্য নিজস্ব কাস্টম আচরণ প্রদান করতে হবে। কম্পাইলার নিশ্চিত করবে যে Summary
trait যুক্ত যেকোনো টাইপের summarize
মেথডটি ঠিক এই সিগনেচার দিয়ে ডিফাইন করা থাকবে।
একটি trait-এর বডিতে একাধিক মেথড থাকতে পারে: মেথড সিগনেচারগুলো প্রতি লাইনে একটি করে তালিকাভুক্ত করা হয় এবং প্রতিটি লাইন একটি সেমিকোলন দিয়ে শেষ হয়।
একটি টাইপের উপর ট্রেইট ইমপ্লিমেন্ট করা
এখন যেহেতু আমরা Summary
trait-এর মেথডগুলোর কাঙ্ক্ষিত সিগনেচার ডিফাইন করেছি, আমরা এটিকে আমাদের মিডিয়া অ্যাগ্রিগেটরের টাইপগুলোতে ইমপ্লিমেন্ট করতে পারি। লিস্টিং ১০-১৩ NewsArticle
struct-এর উপর Summary
trait-এর একটি ইমপ্লিমেন্টেশন দেখায় যা summarize
-এর রিটার্ন ভ্যালু তৈরি করতে হেডলাইন, লেখক এবং অবস্থান ব্যবহার করে। SocialPost
struct-এর জন্য, আমরা summarize
-কে ইউজারের নাম এবং পোস্টের সম্পূর্ণ টেক্সট হিসেবে ডিফাইন করি, এই ধরে নিয়ে যে পোস্টের বিষয়বস্তু ইতিমধ্যে ২৮০ অক্ষরের মধ্যে সীমাবদ্ধ।
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
একটি টাইপের উপর একটি trait ইমপ্লিমেন্ট করা সাধারণ মেথড ইমপ্লিমেন্ট করার মতোই। পার্থক্য হলো impl
-এর পরে, আমরা যে trait-টি ইমপ্লিমেন্ট করতে চাই তার নাম লিখি, তারপর for
কিওয়ার্ড ব্যবহার করি এবং তারপর যে টাইপের জন্য trait-টি ইমপ্লিমেন্ট করতে চাই তার নাম নির্দিষ্ট করি। impl
ব্লকের মধ্যে, আমরা trait ডেফিনিশনে সংজ্ঞায়িত মেথড সিগনেচারগুলো রাখি। প্রতিটি সিগনেচারের পরে সেমিকোলন যোগ করার পরিবর্তে, আমরা কার্লি ব্র্যাকেট ব্যবহার করি এবং নির্দিষ্ট টাইপের জন্য trait-এর মেথডগুলোর যে নির্দিষ্ট আচরণ আমরা চাই তা দিয়ে মেথড বডি পূরণ করি।
এখন যেহেতু লাইব্রেরিটি NewsArticle
এবং SocialPost
-এর উপর Summary
trait ইমপ্লিমেন্ট করেছে, ক্রেটের ব্যবহারকারীরা NewsArticle
এবং SocialPost
-এর ইনস্ট্যান্সের উপর trait মেথডগুলো কল করতে পারবে ঠিক যেভাবে আমরা সাধারণ মেথড কল করি। একমাত্র পার্থক্য হলো ব্যবহারকারীকে অবশ্যই trait এবং টাইপ উভয়কেই স্কোপে আনতে হবে। এখানে একটি উদাহরণ দেওয়া হলো যে কীভাবে একটি বাইনারি ক্রেট আমাদের aggregator
লাইব্রেরি ক্রেট ব্যবহার করতে পারে:
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
এই কোডটি প্রিন্ট করে 1 new post: horse_ebooks: of course, as you probably already know, people
।
aggregator
ক্রেটের উপর নির্ভরশীল অন্যান্য ক্রেটগুলোও তাদের নিজস্ব টাইপের উপর Summary
ইমপ্লিমেন্ট করার জন্য Summary
trait-টিকে স্কোপে আনতে পারে। একটি সীমাবদ্ধতা মনে রাখতে হবে যে আমরা একটি টাইপের উপর একটি trait শুধুমাত্র তখনই ইমপ্লিমেন্ট করতে পারি যদি trait অথবা টাইপ, বা উভয়ই, আমাদের ক্রেটের জন্য লোকাল (local) হয়। উদাহরণস্বরূপ, আমরা আমাদের aggregator
ক্রেটের কার্যকারিতার অংশ হিসেবে SocialPost
-এর মতো একটি কাস্টম টাইপের উপর Display
-এর মতো স্ট্যান্ডার্ড লাইব্রেরি trait ইমপ্লিমেন্ট করতে পারি কারণ SocialPost
টাইপটি আমাদের aggregator
ক্রেটের জন্য লোকাল। আমরা আমাদের aggregator
ক্রেটে Vec<T>
-এর উপর Summary
ইমপ্লিমেন্ট করতে পারি কারণ Summary
trait-টি আমাদের aggregator
ক্রেটের জন্য লোকাল।
কিন্তু আমরা এক্সটার্নাল টাইপের উপর এক্সটার্নাল trait ইমপ্লিমেন্ট করতে পারি না। উদাহরণস্বরূপ, আমরা আমাদের aggregator
ক্রেটের মধ্যে Vec<T>
-এর উপর Display
trait ইমপ্লিমেন্ট করতে পারি না কারণ Display
এবং Vec<T>
উভয়ই স্ট্যান্ডার্ড লাইব্রেরিতে ডিফাইন করা এবং আমাদের aggregator
ক্রেটের জন্য লোকাল নয়। এই সীমাবদ্ধতাটি coherence নামক একটি বৈশিষ্ট্যের অংশ, এবং আরও নির্দিষ্টভাবে orphan rule নামে পরিচিত, কারণ প্যারেন্ট টাইপ উপস্থিত নেই। এই নিয়মটি নিশ্চিত করে যে অন্য লোকের কোড আপনার কোড ভাঙতে পারবে না এবং বিপরীতক্রমেও। এই নিয়ম ছাড়া, দুটি ক্রেট একই টাইপের জন্য একই trait ইমপ্লিমেন্ট করতে পারত, এবং Rust জানত না কোন ইমপ্লিমেন্টেশনটি ব্যবহার করতে হবে।
ডিফল্ট ইমপ্লিমেন্টেশন (Default Implementations)
কখনও কখনও একটি trait-এর কিছু বা সমস্ত মেথডের জন্য ডিফল্ট আচরণ থাকা দরকারী, প্রতিটি টাইপের উপর সমস্ত মেথডের জন্য ইমপ্লিমেন্টেশন দাবি করার পরিবর্তে। তারপর, যখন আমরা কোনো নির্দিষ্ট টাইপের উপর trait-টি ইমপ্লিমেন্ট করি, তখন আমরা প্রতিটি মেথডের ডিফল্ট আচরণ রাখতে বা ওভাররাইড করতে পারি।
লিস্টিং ১০-১৪-এ, আমরা Summary
trait-এর summarize
মেথডের জন্য একটি ডিফল্ট স্ট্রিং নির্দিষ্ট করি, শুধুমাত্র মেথড সিগনেচার ডিফাইন করার পরিবর্তে, যেমনটি আমরা লিস্টিং ১০-১২-এ করেছিলাম।
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
NewsArticle
-এর ইনস্ট্যান্সগুলোর সারাংশ তৈরি করতে ডিফল্ট ইমপ্লিমেন্টেশন ব্যবহার করার জন্য, আমরা impl Summary for NewsArticle {}
দিয়ে একটি খালি impl
ব্লক নির্দিষ্ট করি।
যদিও আমরা আর সরাসরি NewsArticle
-এ summarize
মেথড ডিফাইন করছি না, আমরা একটি ডিফল্ট ইমপ্লিমেন্টেশন সরবরাহ করেছি এবং নির্দিষ্ট করেছি যে NewsArticle
Summary
trait ইমপ্লিমেন্ট করে। ফলস্বরূপ, আমরা এখনও একটি NewsArticle
-এর ইনস্ট্যান্সে summarize
মেথড কল করতে পারি, এভাবে:
use aggregator::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
এই কোডটি New article available! (Read more...)
প্রিন্ট করে।
একটি ডিফল্ট ইমপ্লিমেন্টেশন তৈরি করার জন্য লিস্টিং ১০-১৩-এর SocialPost
-এ Summary
-এর ইমপ্লিমেন্টেশন সম্পর্কে আমাদের কিছু পরিবর্তন করতে হবে না। কারণটি হলো একটি ডিফল্ট ইমপ্লিমেন্টেশন ওভাররাইড করার সিনট্যাক্স এবং যে trait মেথডের ডিফল্ট ইমপ্লিমেন্টেশন নেই তা ইমপ্লিমেন্ট করার সিনট্যাক্স একই।
ডিফল্ট ইমপ্লিমেন্টেশনগুলো একই trait-এর অন্যান্য মেথড কল করতে পারে, এমনকি যদি সেই অন্যান্য মেথডগুলোর ডিফল্ট ইমপ্লিমেন্টেশন না থাকে। এইভাবে, একটি trait অনেক দরকারী কার্যকারিতা সরবরাহ করতে পারে এবং ইমপ্লিমেন্টরদের শুধুমাত্র একটি ছোট অংশ নির্দিষ্ট করতে হয়। উদাহরণস্বরূপ, আমরা Summary
trait-কে এমনভাবে ডিফাইন করতে পারি যাতে একটি summarize_author
মেথড থাকে যার ইমপ্লিমেন্টেশন আবশ্যক, এবং তারপর একটি summarize
মেথড ডিফাইন করতে পারি যার একটি ডিফল্ট ইমপ্লিমেন্টেশন থাকে যা summarize_author
মেথডকে কল করে:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
Summary
-এর এই সংস্করণটি ব্যবহার করতে, আমাদের কেবল একটি টাইপের উপর trait ইমপ্লিমেন্ট করার সময় summarize_author
ডিফাইন করতে হবে:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
আমরা summarize_author
ডিফাইন করার পরে, আমরা SocialPost
struct-এর ইনস্ট্যান্সগুলিতে summarize
কল করতে পারি, এবং summarize
-এর ডিফল্ট ইমপ্লিমেন্টেশনটি আমাদের সরবরাহ করা summarize_author
-এর ডেফিনিশনকে কল করবে। যেহেতু আমরা summarize_author
ইমপ্লিমেন্ট করেছি, Summary
trait আমাদের summarize
মেথডের আচরণ দিয়েছে কোনো অতিরিক্ত কোড লেখা ছাড়াই। এটি দেখতে এরকম:
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
এই কোডটি প্রিন্ট করে 1 new post: (Read more from @horse_ebooks...)
।
মনে রাখবেন যে একই মেথডের একটি ওভাররাইডিং ইমপ্লিমেন্টেশন থেকে ডিফল্ট ইমপ্লিমেন্টেশন কল করা সম্ভব নয়।
প্যারামিটার হিসাবে ট্রেইট (Traits as Parameters)
এখন আপনি জানেন কিভাবে trait ডিফাইন এবং ইমপ্লিমেন্ট করতে হয়, আমরা এখন দেখব কিভাবে trait ব্যবহার করে এমন ফাংশন ডিফাইন করা যায় যা বিভিন্ন ধরনের টাইপ গ্রহণ করতে পারে। আমরা লিস্টিং ১০-১৩-এ NewsArticle
এবং SocialPost
টাইপের উপর ইমপ্লিমেন্ট করা Summary
trait ব্যবহার করে একটি notify
ফাংশন ডিফাইন করব যা তার item
প্যারামিটারে summarize
মেথড কল করে, যা এমন কোনো টাইপের যা Summary
trait ইমপ্লিমেন্ট করে। এটি করার জন্য, আমরা impl Trait
সিনট্যাক্স ব্যবহার করি, এভাবে:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
item
প্যারামিটারের জন্য একটি concrete টাইপের পরিবর্তে, আমরা impl
কিওয়ার্ড এবং trait-এর নাম নির্দিষ্ট করি। এই প্যারামিটারটি নির্দিষ্ট trait ইমপ্লিমেন্ট করে এমন যেকোনো টাইপ গ্রহণ করে। notify
-এর বডিতে, আমরা item
-এর উপর Summary
trait থেকে আসা যেকোনো মেথড কল করতে পারি, যেমন summarize
। আমরা notify
কল করতে পারি এবং যেকোনো NewsArticle
বা SocialPost
-এর ইনস্ট্যান্স পাস করতে পারি। যে কোড String
বা i32
-এর মতো অন্য কোনো টাইপ দিয়ে ফাংশনটি কল করবে, তা কম্পাইল হবে না কারণ সেই টাইপগুলো Summary
ইমপ্লিমেন্ট করে না।
ট্রেইট বাউন্ড সিনট্যাক্স (Trait Bound Syntax)
impl Trait
সিনট্যাক্স সহজ ক্ষেত্রে কাজ করে তবে এটি আসলে একটি দীর্ঘ ফর্মের জন্য সিনট্যাক্স সুগার যা trait bound নামে পরিচিত; এটি দেখতে এইরকম:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
এই দীর্ঘ ফর্মটি পূর্ববর্তী বিভাগের উদাহরণের সমতুল্য তবে আরও ভার্বোস। আমরা জেনেরিক টাইপ প্যারামিটারের ডিক্লেয়ারেশনের সাথে একটি কোলনের পরে এবং অ্যাঙ্গেল ব্র্যাকেটের ভিতরে trait bounds রাখি।
impl Trait
সিনট্যাক্স সুবিধাজনক এবং সহজ ক্ষেত্রে কোডকে আরও সংক্ষিপ্ত করে তোলে, যেখানে সম্পূর্ণ trait bound সিনট্যাক্স অন্যান্য ক্ষেত্রে আরও জটিলতা প্রকাশ করতে পারে। উদাহরণস্বরূপ, আমাদের দুটি প্যারামিটার থাকতে পারে যা Summary
ইমপ্লিমেন্ট করে। impl Trait
সিনট্যাক্স দিয়ে এটি করতে হলে দেখতে এমন হবে:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
impl Trait
ব্যবহার করা উপযুক্ত যদি আমরা এই ফাংশনটিকে item1
এবং item2
-কে ভিন্ন টাইপের হতে দিতে চাই (যতক্ষণ উভয় টাইপ Summary
ইমপ্লিমেন্ট করে)। যদি আমরা উভয় প্যারামিটারকে একই টাইপের হতে বাধ্য করতে চাই, তবে আমাদের অবশ্যই একটি trait bound ব্যবহার করতে হবে, যেমন:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
item1
এবং item2
প্যারামিটারের টাইপ হিসাবে নির্দিষ্ট করা জেনেরিক টাইপ T
ফাংশনটিকে এমনভাবে সীমাবদ্ধ করে যে item1
এবং item2
-এর জন্য আর্গুমেন্ট হিসাবে পাস করা মানের concrete টাইপ অবশ্যই একই হতে হবে।
+
সিনট্যাক্স দিয়ে একাধিক ট্রেইট বাউন্ড নির্দিষ্ট করা
আমরা একাধিক trait bound-ও নির্দিষ্ট করতে পারি। ধরা যাক আমরা চাই notify
item
-এ summarize
-এর পাশাপাশি ডিসপ্লে ফরম্যাটিংও ব্যবহার করুক: আমরা notify
ডেফিনিশনে নির্দিষ্ট করি যে item
অবশ্যই Display
এবং Summary
উভয়ই ইমপ্লিমেন্ট করবে। আমরা +
সিনট্যাক্স ব্যবহার করে এটি করতে পারি:
pub fn notify(item: &(impl Summary + Display)) {
+
সিনট্যাক্স জেনেরিক টাইপের উপর trait bounds-এর সাথেও বৈধ:
pub fn notify<T: Summary + Display>(item: &T) {
দুটি trait bounds নির্দিষ্ট করার সাথে, notify
-এর বডি summarize
কল করতে পারে এবং item
ফরম্যাট করতে {}
ব্যবহার করতে পারে।
where
ক্লজ দিয়ে পরিষ্কার ট্রেইট বাউন্ড
অতিরিক্ত trait bounds ব্যবহার করার কিছু অসুবিধা আছে। প্রতিটি জেনেরিকের নিজস্ব trait bounds থাকে, তাই একাধিক জেনেরিক টাইপ প্যারামিটারযুক্ত ফাংশনগুলোতে ফাংশনের নাম এবং তার প্যারামিটার তালিকার মধ্যে প্রচুর trait bound তথ্য থাকতে পারে, যা ফাংশন সিগনেচার পড়া কঠিন করে তোলে। এই কারণে, Rust-এর ফাংশন সিগনেচারের পরে একটি where
ক্লজের ভিতরে trait bounds নির্দিষ্ট করার জন্য বিকল্প সিনট্যাক্স রয়েছে। সুতরাং, এটি লেখার পরিবর্তে:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
আমরা একটি where
ক্লজ ব্যবহার করতে পারি, এভাবে:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
এই ফাংশনের সিগনেচারটি কম জটীল: ফাংশনের নাম, প্যারামিটার তালিকা এবং রিটার্ন টাইপ কাছাকাছি রয়েছে, অনেকটা trait bounds ছাড়া একটি ফাংশনের মতো।
ট্রেইট ইমপ্লিমেন্ট করে এমন টাইপ রিটার্ন করা
আমরা রিটার্ন পজিশনে impl Trait
সিনট্যাক্স ব্যবহার করে এমন একটি টাইপের মান রিটার্ন করতে পারি যা একটি trait ইমপ্লিমেন্ট করে, যেমনটি এখানে দেখানো হয়েছে:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
রিটার্ন টাইপের জন্য impl Summary
ব্যবহার করে, আমরা নির্দিষ্ট করি যে returns_summarizable
ফাংশনটি Summary
trait ইমপ্লিমেন্ট করে এমন কোনো টাইপ রিটার্ন করে, concrete টাইপের নাম উল্লেখ না করেই। এই ক্ষেত্রে, returns_summarizable
একটি SocialPost
রিটার্ন করে, কিন্তু এই ফাংশন কল করা কোডকে এটি জানার প্রয়োজন নেই।
শুধুমাত্র যে trait ইমপ্লিমেন্ট করে তা দ্বারা একটি রিটার্ন টাইপ নির্দিষ্ট করার ক্ষমতাটি ক্লোজার এবং ইটারেটরের প্রসঙ্গে বিশেষভাবে কার্যকর, যা আমরা চ্যাপ্টার ১৩-এ আলোচনা করব। ক্লোজার এবং ইটারেটর এমন টাইপ তৈরি করে যা কেবল কম্পাইলার জানে বা যে টাইপগুলো নির্দিষ্ট করা খুব দীর্ঘ। impl Trait
সিনট্যাক্স আপনাকে সংক্ষিপ্তভাবে নির্দিষ্ট করতে দেয় যে একটি ফাংশন Iterator
trait ইমপ্লিমেন্ট করে এমন কোনো টাইপ রিটার্ন করে, একটি খুব দীর্ঘ টাইপ লেখার প্রয়োজন ছাড়াই।
তবে, আপনি কেবল impl Trait
ব্যবহার করতে পারেন যদি আপনি একটি একক টাইপ রিটার্ন করেন। উদাহরণস্বরূপ, এই কোডটি যা impl Summary
হিসাবে রিটার্ন টাইপ নির্দিষ্ট করে একটি NewsArticle
বা একটি SocialPost
রিটার্ন করে, কাজ করবে না:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}
কম্পাইলারে impl Trait
সিনট্যাক্স কীভাবে ইমপ্লিমেন্ট করা হয়েছে তার সীমাবদ্ধতার কারণে একটি NewsArticle
বা একটি SocialPost
রিটার্ন করা অনুমোদিত নয়। আমরা চ্যাপ্টার ১৮-এর “ভিন্ন ধরনের মানের জন্য ট্রেইট অবজেক্ট ব্যবহার করা” বিভাগে এই আচরণ সহ একটি ফাংশন কীভাবে লিখতে হয় তা আলোচনা করব।
ট্রেইট বাউন্ড ব্যবহার করে শর্তসাপেক্ষে মেথড ইমপ্লিমেন্ট করা
জেনেরিক টাইপ প্যারামিটার ব্যবহার করে একটি impl
ব্লকের সাথে একটি trait bound ব্যবহার করে, আমরা নির্দিষ্ট trait ইমপ্লিমেন্ট করে এমন টাইপগুলোর জন্য শর্তসাপেক্ষে মেথড ইমপ্লিমেন্ট করতে পারি। উদাহরণস্বরূপ, লিস্টিং ১০-১৫-এর Pair<T>
টাইপটি সর্বদা Pair<T>
-এর একটি নতুন ইনস্ট্যান্স রিটার্ন করার জন্য new
ফাংশন ইমপ্লিমেন্ট করে (চ্যাপ্টার ৫-এর “মেথড ডিফাইন করা” বিভাগ থেকে মনে করুন যে Self
হলো impl
ব্লকের টাইপের একটি টাইপ অ্যালিয়াস, যা এই ক্ষেত্রে Pair<T>
)। কিন্তু পরবর্তী impl
ব্লকে, Pair<T>
কেবল তখনই cmp_display
মেথডটি ইমপ্লিমেন্ট করে যদি তার অভ্যন্তরীণ টাইপ T
PartialOrd
trait ইমপ্লিমেন্ট করে যা তুলনা সক্ষম করে এবং Display
trait ইমপ্লিমেন্ট করে যা প্রিন্টিং সক্ষম করে।
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
আমরা trait bounds সন্তুষ্ট করে এমন যেকোনো টাইপের জন্য একটি trait শর্তসাপেক্ষে ইমপ্লিমেন্ট করতে পারি। এই ধরনের ইমপ্লিমেন্টেশনকে blanket implementations বলা হয় এবং এটি Rust স্ট্যান্ডার্ড লাইব্রেরিতে ব্যাপকভাবে ব্যবহৃত হয়। উদাহরণস্বরূপ, স্ট্যান্ডার্ড লাইব্রেরি Display
trait ইমপ্লিমেন্ট করে এমন যেকোনো টাইপের উপর ToString
trait ইমপ্লিমেন্ট করে। স্ট্যান্ডার্ড লাইব্রেরিতে impl
ব্লকটি এই কোডের মতো দেখতে:
impl<T: Display> ToString for T {
// --snip--
}
যেহেতু স্ট্যান্ডার্ড লাইব্রেরিতে এই blanket implementation রয়েছে, আমরা Display
trait ইমপ্লিমেন্ট করে এমন যেকোনো টাইপের উপর ToString
trait দ্বারা সংজ্ঞায়িত to_string
মেথড কল করতে পারি। উদাহরণস্বরূপ, আমরা পূর্ণসংখ্যাকে তাদের সংশ্লিষ্ট String
মানে পরিণত করতে পারি কারণ পূর্ণসংখ্যা Display
ইমপ্লিমেন্ট করে:
#![allow(unused)] fn main() { let s = 3.to_string(); }
Blanket implementation গুলি trait-এর ডকুমেন্টেশনে “Implementors” বিভাগে উপস্থিত থাকে।
Traits এবং trait bounds আমাদের জেনেরিক টাইপ প্যারামিটার ব্যবহার করে কোড লিখতে দেয় যা পুনরাবৃত্তি কমায় কিন্তু কম্পাইলারকে নির্দিষ্ট করে দেয় যে আমরা জেনেরিক টাইপের নির্দিষ্ট আচরণ চাই। কম্পাইলার তখন trait bound তথ্য ব্যবহার করে পরীক্ষা করতে পারে যে আমাদের কোডের সাথে ব্যবহৃত সমস্ত concrete টাইপ সঠিক আচরণ প্রদান করে। ডাইনামিক্যালি টাইপড ল্যাঙ্গুয়েজে, আমরা যদি এমন একটি টাইপের উপর একটি মেথড কল করি যা মেথডটি ডিফাইন করে না, তবে আমরা রানটাইমে একটি এরর পেতাম। কিন্তু Rust এই এররগুলিকে কম্পাইল টাইমে নিয়ে আসে যাতে আমরা আমাদের কোড রান করতে সক্ষম হওয়ার আগেই সমস্যাগুলি সমাধান করতে বাধ্য হই। উপরন্তু, আমাদের রানটাইমে আচরণের জন্য পরীক্ষা করার কোড লিখতে হবে না কারণ আমরা ইতিমধ্যে কম্পাইল টাইমে পরীক্ষা করে ফেলেছি। এটি জেনেরিক্সের নমনীয়তা ত্যাগ না করেই পারফরম্যান্স উন্নত করে।
লাইফটাইম দিয়ে রেফারেন্স ভ্যালিডেইট করা
লাইফটাইম (Lifetimes) হলো আরেক ধরনের জেনেরিক যা আমরা ইতিমধ্যে ব্যবহার করে আসছি। একটি টাইপের কাঙ্ক্ষিত আচরণ আছে কিনা তা নিশ্চিত করার পরিবর্তে, লাইফটাইম নিশ্চিত করে যে রেফারেন্সগুলো যতক্ষণ আমাদের প্রয়োজন ততক্ষণ ভ্যালিড থাকবে।
চ্যাপ্টার ৪-এর "রেফারেন্স এবং বরোয়িং" (References and Borrowing) বিভাগে আমরা একটি বিষয় আলোচনা করিনি, তা হলো Rust-এর প্রতিটি রেফারেন্সের একটি লাইফটাইম থাকে, যা হলো সেই স্কোপ যার জন্য রেফারেন্সটি ভ্যালিড। বেশিরভাগ সময়, লাইফটাইমগুলো উহ্য (implicit) এবং অনুমিত (inferred) থাকে, ঠিক যেমন বেশিরভাগ সময় টাইপগুলো অনুমিত থাকে। আমাদের কেবল তখনই টাইপ অ্যানোটেট করতে হয় যখন একাধিক টাইপ সম্ভব হয়। একইভাবে, আমাদের তখনই লাইফটাইম অ্যানোটেট করতে হয় যখন রেফারেন্সগুলোর লাইফটাইম কয়েকটি ভিন্ন উপায়ে সম্পর্কিত হতে পারে। Rust আমাদের এই সম্পর্কগুলো জেনেরিক লাইফটাইম প্যারামিটার ব্যবহার করে অ্যানোটেট করতে বলে, যাতে রানটাইমে ব্যবহৃত আসল রেফারেন্সগুলো অবশ্যই ভ্যালিড থাকে।
লাইফটাইম অ্যানোটেট করা এমন একটি ধারণা যা অন্য বেশিরভাগ প্রোগ্রামিং ল্যাঙ্গুয়েজে নেই, তাই এটি অপরিচিত মনে হতে পারে। যদিও আমরা এই অধ্যায়ে লাইফটাইম সম্পূর্ণরূপে কভার করব না, আমরা সাধারণ উপায়গুলো আলোচনা করব যেখানে আপনি লাইফটাইম সিনট্যাক্সের সম্মুখীন হতে পারেন যাতে আপনি ধারণাটির সাথে পরিচিত হতে পারেন।
লাইফটাইম দিয়ে ড্যাংলিং রেফারেন্স প্রতিরোধ করা
লাইফটাইমের মূল উদ্দেশ্য হলো ড্যাংলিং রেফারেন্স (dangling references) প্রতিরোধ করা, যা একটি প্রোগ্রামকে এমন ডেটা রেফারেন্স করতে বাধ্য করে যা তার উদ্দিষ্ট ডেটা নয়। লিস্টিং ১০-১৬-এর প্রোগ্রামটি বিবেচনা করুন, যার একটি বাইরের স্কোপ এবং একটি ভেতরের স্কোপ রয়েছে।
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
দ্রষ্টব্য: লিস্টিং ১০-১৬, ১০-১৭, এবং ১০-২৩ এর উদাহরণগুলোতে ভ্যারিয়েবল ডিক্লেয়ার করা হয়েছে কোনো প্রাথমিক মান না দিয়েই, তাই ভ্যারিয়েবলের নামটি বাইরের স্কোপে বিদ্যমান থাকে। প্রথম নজরে, এটি Rust-এর কোনো null ভ্যালু না থাকার সাথে সাংঘর্ষিক মনে হতে পারে। তবে, যদি আমরা কোনো ভ্যারিয়েবলকে মান দেওয়ার আগে ব্যবহার করার চেষ্টা করি, তাহলে আমরা একটি কম্পাইল-টাইম এরর পাব, যা দেখায় যে Rust সত্যিই null ভ্যালু অনুমোদন করে না।
বাইরের স্কোপটি r
নামে একটি ভ্যারিয়েবল ডিক্লেয়ার করে যার কোনো প্রাথমিক মান নেই, এবং ভেতরের স্কোপটি x
নামে একটি ভ্যারিয়েবল ডিক্লেয়ার করে যার প্রাথমিক মান 5
। ভেতরের স্কোপের মধ্যে, আমরা r
-এর মানকে x
-এর একটি রেফারেন্স হিসাবে সেট করার চেষ্টা করি। তারপর ভেতরের স্কোপ শেষ হয়ে যায়, এবং আমরা r
-এর মান প্রিন্ট করার চেষ্টা করি। এই কোডটি কম্পাইল হবে না কারণ r
যে মানটিকে রেফার করছে তা আমরা ব্যবহার করার চেষ্টা করার আগেই স্কোপের বাইরে চলে গেছে। এখানে এরর মেসেজটি হলো:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| --- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
এরর মেসেজটি বলছে যে ভ্যারিয়েবল x
"যথেষ্ট দীর্ঘজীবী নয়" (does not live long enough)। কারণটি হলো লাইন ৭-এ ভেতরের স্কোপ শেষ হলে x
স্কোপের বাইরে চলে যাবে। কিন্তু r
বাইরের স্কোপের জন্য এখনও ভ্যালিড; যেহেতু এর স্কোপটি বড়, আমরা বলি যে এটি "দীর্ঘজীবী" (lives longer)। যদি Rust এই কোডটি কাজ করার অনুমতি দিত, r
এমন মেমরি রেফারেন্স করত যা x
স্কোপের বাইরে যাওয়ার সময় ডিঅ্যালোকেট হয়ে গিয়েছিল, এবং r
দিয়ে আমরা যা করার চেষ্টা করতাম তা সঠিকভাবে কাজ করত না। তাহলে Rust কীভাবে নির্ধারণ করে যে এই কোডটি অবৈধ? এটি একটি borrow checker ব্যবহার করে।
দ্য বরো চেকার (The Borrow Checker)
Rust কম্পাইলারের একটি borrow checker আছে যা স্কোপগুলো তুলনা করে নির্ধারণ করে যে সমস্ত borrow ভ্যালিড কিনা। লিস্টিং ১০-১৭ লিস্টিং ১০-১৬-এর মতো একই কোড দেখায় কিন্তু ভ্যারিয়েবলগুলোর লাইফটাইম দেখানো অ্যানোটেশনসহ।
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
এখানে, আমরা r
-এর লাইফটাইমকে 'a
এবং x
-এর লাইফটাইমকে 'b
দিয়ে অ্যানোটেট করেছি। जैसा कि आप देख सकते हैं, ভেতরের 'b
ব্লকটি বাইরের 'a
লাইফটাইম ব্লকের চেয়ে অনেক ছোট। কম্পাইল টাইমে, Rust দুটি লাইফটাইমের আকার তুলনা করে এবং দেখে যে r
-এর লাইফটাইম 'a
কিন্তু এটি 'b
লাইফটাইমযুক্ত মেমরিকে রেফার করে। প্রোগ্রামটি প্রত্যাখ্যান করা হয়েছে কারণ 'b
'a
-এর চেয়ে ছোট: রেফারেন্সের বিষয়বস্তুটি রেফারেন্সের মতো দীর্ঘজীবী নয়।
লিস্টিং ১০-১৮ কোডটি ঠিক করে যাতে এটিতে কোনো ড্যাংলিং রেফারেন্স না থাকে এবং এটি কোনো এরর ছাড়াই কম্পাইল হয়।
fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {r}"); // | | // --+ | } // ----------+
এখানে, x
-এর লাইফটাইম 'b
, যা এই ক্ষেত্রে 'a
-এর চেয়ে বড়। এর মানে হলো r
, x
-কে রেফারেন্স করতে পারে কারণ Rust জানে যে r
-এর রেফারেন্সটি সবসময় ভ্যালিড থাকবে যতক্ষণ x
ভ্যালিড থাকে।
এখন যেহেতু আপনি জানেন রেফারেন্সের লাইফটাইম কোথায় থাকে এবং Rust কীভাবে লাইফটাইম বিশ্লেষণ করে রেফারেন্সগুলো সবসময় ভ্যালিড থাকবে তা নিশ্চিত করে, চলুন ফাংশনের প্রেক্ষাপটে প্যারামিটার এবং রিটার্ন ভ্যালুর জেনেরিক লাইফটাইম অন্বেষণ করি।
ফাংশনে জেনেরিক লাইফটাইম
আমরা একটি ফাংশন লিখব যা দুটি স্ট্রিং স্লাইসের মধ্যে দীর্ঘতরটি রিটার্ন করে। এই ফাংশনটি দুটি স্ট্রিং স্লাইস নেবে এবং একটি একক স্ট্রিং স্লাইস রিটার্ন করবে। আমরা longest
ফাংশনটি ইমপ্লিমেন্ট করার পরে, লিস্টিং ১০-১৯-এর কোডটি The longest string is abcd
প্রিন্ট করা উচিত।
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
লক্ষ্য করুন যে আমরা চাই ফাংশনটি স্ট্রিং স্লাইস (যা রেফারেন্স) নিক, স্ট্রিং নয়, কারণ আমরা চাই না যে longest
ফাংশন তার প্যারামিটারগুলোর মালিকানা (ownership) নিয়ে নিক। লিস্টিং ১০-১৯-এ আমরা যে প্যারামিটারগুলো ব্যবহার করি সেগুলো কেন আমরা চাই সে সম্পর্কে আরও আলোচনার জন্য চ্যাপ্টার ৪-এর "প্যারামিটার হিসাবে স্ট্রিং স্লাইস" (String Slices as Parameters) দেখুন।
যদি আমরা লিস্টিং ১০-২০-তে দেখানো হিসাবে longest
ফাংশনটি ইমপ্লিমেন্ট করার চেষ্টা করি, তবে এটি কম্পাইল হবে না।
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
পরিবর্তে, আমরা নিম্নলিখিত এররটি পাই যা লাইফটাইম সম্পর্কে কথা বলে:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
হেল্প টেক্সট প্রকাশ করে যে রিটার্ন টাইপের উপর একটি জেনেরিক লাইফটাইম প্যারামিটার প্রয়োজন কারণ Rust বলতে পারে না যে রিটার্ন করা রেফারেন্সটি x
-কে নাকি y
-কে রেফার করে। আসলে, আমরাও জানি না, কারণ এই ফাংশনের বডির if
ব্লকটি x
-এর একটি রেফারেন্স রিটার্ন করে এবং else
ব্লকটি y
-এর একটি রেফারেন্স রিটার্ন করে!
যখন আমরা এই ফাংশনটি ডিফাইন করছি, তখন আমরা জানি না কোন কংক্রিট (concrete) ভ্যালু এই ফাংশনে পাস করা হবে, তাই আমরা জানি না if
কেস না else
কেস এক্সিকিউট হবে। আমরা এটাও জানি না যে পাস করা রেফারেন্সগুলোর কংক্রিট লাইফটাইম কী হবে, তাই আমরা লিস্টিং ১০-১৭ এবং ১০-১৮ এর মতো স্কোপগুলো দেখে নির্ধারণ করতে পারি না যে আমাদের রিটার্ন করা রেফারেন্সটি সবসময় ভ্যালিড থাকবে কিনা। বরো চেকারও এটি নির্ধারণ করতে পারে না, কারণ এটি জানে না x
এবং y
-এর লাইফটাইম রিটার্ন ভ্যালুর লাইফটাইমের সাথে কীভাবে সম্পর্কিত। এই এররটি ঠিক করার জন্য, আমরা জেনেরিক লাইফটাইম প্যারামিটার যোগ করব যা রেফারেন্সগুলোর মধ্যে সম্পর্ক ডিফাইন করবে যাতে বরো চেকার তার বিশ্লেষণ করতে পারে।
লাইফটাইম অ্যানোটেশন সিনট্যাক্স
লাইফটাইম অ্যানোটেশন কোনো রেফারেন্স কতদিন বেঁচে থাকবে তা পরিবর্তন করে না। বরং, তারা লাইফটাইমকে প্রভাবিত না করে একাধিক রেফারেন্সের লাইফটাইমের সম্পর্ক বর্ণনা করে। ঠিক যেমন ফাংশনগুলো যেকোনো টাইপ গ্রহণ করতে পারে যখন সিগনেচার একটি জেনেরিক টাইপ প্যারামিটার নির্দিষ্ট করে, ফাংশনগুলো একটি জেনেরিক লাইফটাইম প্যারামিটার নির্দিষ্ট করে যেকোনো লাইফটাইমসহ রেফারেন্স গ্রহণ করতে পারে।
লাইফটাইম অ্যানোটেশনের একটি কিছুটা অস্বাভাবিক সিনট্যাক্স আছে: লাইফটাইম প্যারামিটারের নাম অবশ্যই একটি অ্যাপস্ট্রফি ('
) দিয়ে শুরু হতে হবে এবং সাধারণত সবগুলো ছোট হাতের এবং খুব ছোট হয়, জেনেরিক টাইপের মতো। বেশিরভাগ লোক প্রথম লাইফটাইম অ্যানোটেশনের জন্য 'a
নামটি ব্যবহার করে। আমরা রেফারেন্সের &
-এর পরে লাইফটাইম প্যারামিটার অ্যানোটেশন রাখি, অ্যানোটেশনটিকে রেফারেন্সের টাইপ থেকে আলাদা করার জন্য একটি স্পেস ব্যবহার করে।
এখানে কিছু উদাহরণ দেওয়া হলো: একটি i32
-এর রেফারেন্স যাতে কোনো লাইফটাইম প্যারামিটার নেই, একটি i32
-এর রেফারেন্স যার 'a
নামের একটি লাইফটাইম প্যারামিটার আছে, এবং একটি i32
-এর মিউটেবল রেফারেন্স যারও 'a
লাইফটাইম আছে।
&i32 // একটি রেফারেন্স
&'a i32 // একটি সুস্পষ্ট লাইফটাইমসহ রেফারেন্স
&'a mut i32 // একটি সুস্পষ্ট লাইফটাইমসহ মিউটেবল রেফারেন্স
একটি লাইফটাইম অ্যানোটেশনের নিজের কোনো বিশেষ অর্থ নেই কারণ অ্যানোটেশনগুলো Rust-কে জানাতে চায় যে একাধিক রেফারেন্সের জেনেরিক লাইফটাইম প্যারামিটারগুলো একে অপরের সাথে কীভাবে সম্পর্কিত। আসুন longest
ফাংশনের প্রেক্ষাপটে দেখি লাইফটাইম অ্যানোটেশনগুলো একে অপরের সাথে কীভাবে সম্পর্কিত।
ফাংশন সিগনেচারে লাইফটাইম অ্যানোটেশন
ফাংশন সিগনেচারে লাইফটাইম অ্যানোটেশন ব্যবহার করার জন্য, আমাদের ফাংশনের নাম এবং প্যারামিটার তালিকার মধ্যে অ্যাঙ্গেল ব্র্যাকেটের ভিতরে জেনেরিক লাইফটাইম প্যারামিটার ডিক্লেয়ার করতে হবে, ঠিক যেমন আমরা জেনেরিক টাইপ প্যারামিটারের সাথে করেছিলাম।
আমরা চাই সিগনেচারটি নিম্নলিখিত সীমাবদ্ধতা প্রকাশ করুক: রিটার্ন করা রেফারেন্সটি ততক্ষণ ভ্যালিড থাকবে যতক্ষণ উভয় প্যারামিটার ভ্যালিড থাকবে। এটি প্যারামিটার এবং রিটার্ন ভ্যালুর লাইফটাইমের মধ্যেকার সম্পর্ক। আমরা লাইফটাইমটির নাম দেব 'a
এবং তারপর প্রতিটি রেফারেন্সে এটি যোগ করব, যেমনটি লিস্টিং ১০-২১-এ দেখানো হয়েছে।
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
এই কোডটি কম্পাইল হওয়া উচিত এবং আমরা যখন লিস্টিং ১০-১৯-এর main
ফাংশনের সাথে এটি ব্যবহার করব তখন কাঙ্ক্ষিত ফলাফল তৈরি করা উচিত।
ফাংশন সিগনেচারটি এখন Rust-কে বলছে যে কোনো একটি লাইফটাইম 'a
-এর জন্য, ফাংশনটি দুটি প্যারামিটার নেয়, উভয়ই স্ট্রিং স্লাইস যা অন্তত লাইফটাইম 'a
পর্যন্ত বেঁচে থাকে। ফাংশন সিগনেচারটি Rust-কে আরও বলছে যে ফাংশন থেকে রিটার্ন করা স্ট্রিং স্লাইসটি অন্তত লাইফটাইম 'a
পর্যন্ত বেঁচে থাকবে। বাস্তবে, এর মানে হলো longest
ফাংশন দ্বারা রিটার্ন করা রেফারেন্সের লাইফটাইম ফাংশন আর্গুমেন্ট দ্বারা নির্দেশিত মানগুলির লাইফটাইমের মধ্যে যেটি ছোট তার সমান। এই সম্পর্কগুলোই আমরা চাই Rust এই কোড বিশ্লেষণ করার সময় ব্যবহার করুক।
মনে রাখবেন, যখন আমরা এই ফাংশন সিগনেচারে লাইফটাইম প্যারামিটারগুলো নির্দিষ্ট করি, তখন আমরা পাস করা বা রিটার্ন করা কোনো মানের লাইফটাইম পরিবর্তন করছি না। বরং, আমরা নির্দিষ্ট করছি যে বরো চেকার এমন কোনো মান প্রত্যাখ্যান করবে যা এই সীমাবদ্ধতাগুলো মেনে চলে না। লক্ষ্য করুন যে longest
ফাংশনটিকে x
এবং y
ঠিক কতদিন বেঁচে থাকবে তা জানার প্রয়োজন নেই, কেবল এটি জানতে হবে যে 'a
-এর জন্য এমন কোনো স্কোপ প্রতিস্থাপন করা যেতে পারে যা এই সিগনেচারটি সন্তুষ্ট করবে।
ফাংশনে লাইফটাইম অ্যানোটেট করার সময়, অ্যানোটেশনগুলো ফাংশন সিগনেচারে যায়, ফাংশন বডিতে নয়। লাইফটাইম অ্যানোটেশনগুলো ফাংশনের চুক্তির অংশ হয়ে যায়, ঠিক যেমন সিগনেচারের টাইপগুলো। ফাংশন সিগনেচারে লাইফটাইম চুক্তি থাকার মানে হলো Rust কম্পাইলার যে বিশ্লেষণ করে তা সহজ হতে পারে। যদি কোনো ফাংশন যেভাবে অ্যানোটেট করা হয়েছে বা যেভাবে কল করা হয়েছে তাতে কোনো সমস্যা থাকে, তাহলে কম্পাইলার এররগুলো আমাদের কোডের অংশ এবং সীমাবদ্ধতাগুলোকে আরও স্পষ্টভাবে নির্দেশ করতে পারে। যদি, পরিবর্তে, Rust কম্পাইলার আমরা লাইফটাইমের সম্পর্কগুলো কেমন চেয়েছিলাম সে সম্পর্কে আরও বেশি অনুমান করত, তাহলে কম্পাইলার হয়তো সমস্যার কারণ থেকে অনেক দূরে আমাদের কোডের একটি ব্যবহারের দিকেই কেবল নির্দেশ করতে পারত।
যখন আমরা longest
-এ কংক্রিট রেফারেন্স পাস করি, তখন 'a
-এর জন্য প্রতিস্থাপিত কংক্রিট লাইফটাইম হলো x
-এর স্কোপের সেই অংশ যা y
-এর স্কোপের সাথে ওভারল্যাপ করে। অন্য কথায়, জেনেরিক লাইফটাইম 'a
সেই কংক্রিট লাইফটাইম পাবে যা x
এবং y
-এর লাইফটাইমের মধ্যে ছোটটির সমান। যেহেতু আমরা রিটার্ন করা রেফারেন্সটিকে একই লাইফটাইম প্যারামিটার 'a
দিয়ে অ্যানোটেট করেছি, তাই রিটার্ন করা রেফারেন্সটিও x
এবং y
-এর লাইফটাইমের মধ্যে ছোটটির দৈর্ঘ্যের জন্য ভ্যালিড থাকবে।
আসুন দেখি কীভাবে লাইফটাইম অ্যানোটেশনগুলো longest
ফাংশনটিকে সীমাবদ্ধ করে, ভিন্ন কংক্রিট লাইফটাইমযুক্ত রেফারেন্স পাস করে। লিস্টিং ১০-২২ একটি সহজ উদাহরণ।
fn main() { let string1 = String::from("long string is long"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("The longest string is {result}"); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
এই উদাহরণে, string1
বাইরের স্কোপের শেষ পর্যন্ত ভ্যালিড, string2
ভেতরের স্কোপের শেষ পর্যন্ত ভ্যালিড, এবং result
এমন কিছুকে রেফারেন্স করে যা ভেতরের স্কোপের শেষ পর্যন্ত ভ্যালিড। এই কোডটি চালান এবং আপনি দেখবেন যে বরো চেকার অনুমোদন করে; এটি কম্পাইল হবে এবং The longest string is long string is long
প্রিন্ট করবে।
এরপরে, আসুন একটি উদাহরণ চেষ্টা করি যা দেখায় যে result
-এর রেফারেন্সের লাইফটাইম অবশ্যই দুটি আর্গুমেন্টের ছোট লাইফটাইম হতে হবে। আমরা result
ভ্যারিয়েবলের ডিক্লেয়ারেশন ভেতরের স্কোপের বাইরে নিয়ে যাব কিন্তু result
ভ্যারিয়েবলে মানের অ্যাসাইনমেন্ট string2
-এর সাথে স্কোপের ভিতরেই রাখব। তারপর আমরা println!
যা result
ব্যবহার করে তা ভেতরের স্কোপের বাইরে, ভেতরের স্কোপ শেষ হওয়ার পরে নিয়ে যাব। লিস্টিং ১০-২৩-এর কোডটি কম্পাইল হবে না।
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
যখন আমরা এই কোডটি কম্পাইল করার চেষ্টা করি, তখন আমরা এই এররটি পাই:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| -------- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
এররটি দেখায় যে println!
স্টেটমেন্টের জন্য result
ভ্যালিড হতে হলে, string2
-কে বাইরের স্কোপের শেষ পর্যন্ত ভ্যালিড থাকতে হতো। Rust এটি জানে কারণ আমরা ফাংশন প্যারামিটার এবং রিটার্ন ভ্যালুর লাইফটাইম একই লাইফটাইম প্যারামিটার 'a
ব্যবহার করে অ্যানোটেট করেছি।
মানুষ হিসেবে, আমরা এই কোডটি দেখে বুঝতে পারি যে string1
string2
-এর চেয়ে দীর্ঘ, এবং তাই result
string1
-এর একটি রেফারেন্স ধারণ করবে। যেহেতু string1
এখনও স্কোপের বাইরে যায়নি, তাই string1
-এর একটি রেফারেন্স println!
স্টেটমেন্টের জন্য এখনও ভ্যালিড থাকবে। তবে, কম্পাইলার এই ক্ষেত্রে দেখতে পারে না যে রেফারেন্সটি ভ্যালিড। আমরা Rust-কে বলেছি যে longest
ফাংশন দ্বারা রিটার্ন করা রেফারেন্সের লাইফটাইম পাস করা রেফারেন্সগুলোর লাইফটাইমের মধ্যে যেটি ছোট তার সমান। তাই, বরো চেকার লিস্টিং ১০-২৩-এর কোডটিকে সম্ভবত একটি অবৈধ রেফারেন্স থাকার কারণে অনুমোদন করে না।
longest
ফাংশনে পাস করা রেফারেন্সগুলোর মান এবং লাইফটাইম এবং রিটার্ন করা রেফারেন্সটি কীভাবে ব্যবহৃত হয় তা পরিবর্তন করে আরও পরীক্ষা ডিজাইন করার চেষ্টা করুন। আপনার পরীক্ষাগুলো বরো চেকার পাস করবে কিনা সে সম্পর্কে অনুমান করুন কম্পাইল করার আগে; তারপর পরীক্ষা করে দেখুন আপনি সঠিক ছিলেন কিনা!
লাইফটাইমের দৃষ্টিকোণ থেকে চিন্তা করা
আপনার ফাংশন কী করছে তার উপর নির্ভর করে আপনাকে কীভাবে লাইফটাইম প্যারামিটার নির্দিষ্ট করতে হবে। উদাহরণস্বরূপ, যদি আমরা longest
ফাংশনের ইমপ্লিমেন্টেশন পরিবর্তন করে সবসময় দীর্ঘতম স্ট্রিং স্লাইসের পরিবর্তে প্রথম প্যারামিটারটি রিটার্ন করতাম, তাহলে আমাদের y
প্যারামিটারে একটি লাইফটাইম নির্দিষ্ট করার প্রয়োজন হতো না। নিম্নলিখিত কোডটি কম্পাইল হবে:
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {result}"); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
আমরা x
প্যারামিটার এবং রিটার্ন টাইপের জন্য একটি লাইফটাইম প্যারামিটার 'a
নির্দিষ্ট করেছি, কিন্তু y
প্যারামিটারের জন্য নয়, কারণ y
-এর লাইফটাইমের সাথে x
-এর লাইফটাইম বা রিটার্ন ভ্যালুর কোনো সম্পর্ক নেই।
একটি ফাংশন থেকে একটি রেফারেন্স রিটার্ন করার সময়, রিটার্ন টাইপের জন্য লাইফটাইম প্যারামিটারটি প্যারামিটারগুলোর মধ্যে একটির লাইফটাইম প্যারামিটারের সাথে মিলতে হবে। যদি রিটার্ন করা রেফারেন্সটি প্যারামিটারগুলোর মধ্যে একটিকে রেফার না করে, তবে এটি অবশ্যই এই ফাংশনের মধ্যে তৈরি একটি মানকে রেফার করবে। তবে, এটি একটি ড্যাংলিং রেফারেন্স হবে কারণ মানটি ফাংশনের শেষে স্কোপের বাইরে চলে যাবে। longest
ফাংশনের এই প্রচেষ্টাটি বিবেচনা করুন যা কম্পাইল হবে না:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
এখানে, যদিও আমরা রিটার্ন টাইপের জন্য একটি লাইফটাইম প্যারামিটার 'a
নির্দিষ্ট করেছি, এই ইমপ্লিমেন্টেশনটি কম্পাইল করতে ব্যর্থ হবে কারণ রিটার্ন ভ্যালুর লাইফটাইম প্যারামিটারগুলোর লাইফটাইমের সাথে মোটেও সম্পর্কিত নয়। এখানে আমরা যে এরর মেসেজটি পাই:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
সমস্যাটি হলো result
longest
ফাংশনের শেষে স্কোপের বাইরে চলে যায় এবং পরিষ্কার হয়ে যায়। আমরা ফাংশন থেকে result
-এর একটি রেফারেন্সও রিটার্ন করার চেষ্টা করছি। এমন কোনো উপায় নেই যে আমরা লাইফটাইম প্যারামিটার নির্দিষ্ট করতে পারি যা ড্যাংলিং রেফারেন্স পরিবর্তন করবে, এবং Rust আমাদের একটি ড্যাংলিং রেফারেন্স তৈরি করতে দেবে না। এই ক্ষেত্রে, সেরা সমাধান হবে একটি ওনড (owned) ডেটা টাইপ রিটার্ন করা, রেফারেন্সের পরিবর্তে, যাতে কলিং ফাংশনটি মান পরিষ্কার করার জন্য দায়ী থাকে।
শেষ পর্যন্ত, লাইফটাইম সিনট্যাক্স বিভিন্ন প্যারামিটার এবং ফাংশনের রিটার্ন ভ্যালুর লাইফটাইম সংযোগ করার বিষয়। একবার সেগুলো সংযুক্ত হয়ে গেলে, Rust-এর কাছে মেমরি-সেফ অপারেশন অনুমোদন করার এবং ড্যাংলিং পয়েন্টার তৈরি বা অন্যথায় মেমরি সেফটি লঙ্ঘনকারী অপারেশনগুলো নিষিদ্ধ করার জন্য যথেষ্ট তথ্য থাকে।
স্ট্রাকট ডেফিনিশনে লাইফটাইম অ্যানোটেশন
এখন পর্যন্ত, আমরা যে struct গুলো ডিফাইন করেছি সেগুলো সবই owned টাইপ ধারণ করে। আমরা রেফারেন্স ধারণ করার জন্য struct ডিফাইন করতে পারি, কিন্তু সেক্ষেত্রে আমাদের struct-এর ডেফিনিশনে প্রতিটি রেফারেন্সের উপর একটি লাইফটাইম অ্যানোটেশন যোগ করতে হবে। লিস্টিং ১০-২৪-এ ImportantExcerpt
নামে একটি struct আছে যা একটি স্ট্রিং স্লাইস ধারণ করে।
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
এই struct-টির part
নামে একটি মাত্র ফিল্ড আছে যা একটি স্ট্রিং স্লাইস ধারণ করে, যা একটি রেফারেন্স। জেনেরিক ডেটা টাইপের মতো, আমরা struct-এর নামের পরে অ্যাঙ্গেল ব্র্যাকেটের মধ্যে জেনেরিক লাইফটাইম প্যারামিটারের নাম ডিক্লেয়ার করি যাতে আমরা struct ডেফিনিশনের বডিতে লাইফটাইম প্যারামিটার ব্যবহার করতে পারি। এই অ্যানোটেশনের মানে হলো ImportantExcerpt
-এর একটি ইনস্ট্যান্স তার part
ফিল্ডে থাকা রেফারেন্সের চেয়ে বেশিদিন বাঁচতে পারে না।
এখানকার main
ফাংশনটি ImportantExcerpt
struct-এর একটি ইনস্ট্যান্স তৈরি করে যা novel
ভ্যারিয়েবলের মালিকানাধীন String
-এর প্রথম বাক্যের একটি রেফারেন্স ধারণ করে। ImportantExcerpt
ইনস্ট্যান্স তৈরি হওয়ার আগে novel
-এর ডেটা বিদ্যমান থাকে। উপরন্তু, ImportantExcerpt
স্কোপের বাইরে যাওয়ার পরেও novel
স্কোপের বাইরে যায় না, তাই ImportantExcerpt
ইনস্ট্যান্সের রেফারেন্সটি ভ্যালিড।
লাইফটাইম এলিশন (Lifetime Elision)
আপনি শিখেছেন যে প্রতিটি রেফারেন্সের একটি লাইফটাইম আছে এবং আপনাকে রেফারেন্স ব্যবহার করে এমন ফাংশন বা struct-এর জন্য লাইফটাইম প্যারামিটার নির্দিষ্ট করতে হবে। তবে, আমাদের লিস্টিং ৪-৯-এ একটি ফাংশন ছিল, যা আবার লিস্টিং ১০-২৫-এ দেখানো হয়েছে, যা লাইফটাইম অ্যানোটেশন ছাড়াই কম্পাইল হয়েছে।
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
এই ফাংশনটি লাইফটাইম অ্যানোটেশন ছাড়াই কম্পাইল হওয়ার কারণটি ঐতিহাসিক: Rust-এর প্রাথমিক সংস্করণগুলোতে (১.০-এর আগে), এই কোডটি কম্পাইল হতো না কারণ প্রতিটি রেফারেন্সের জন্য একটি সুস্পষ্ট লাইফটাইম প্রয়োজন ছিল। সেই সময়ে, ফাংশন সিগনেচারটি এভাবে লেখা হতো:
fn first_word<'a>(s: &'a str) -> &'a str {
অনেক Rust কোড লেখার পর, Rust টিম দেখতে পেল যে Rust প্রোগ্রামাররা নির্দিষ্ট পরিস্থিতিতে বারবার একই লাইফটাইম অ্যানোটেশন লিখছে। এই পরিস্থিতিগুলো অনুমানযোগ্য ছিল এবং কয়েকটি ডিটারমিনিস্টিক প্যাটার্ন অনুসরণ করত। ডেভেলপাররা এই প্যাটার্নগুলো কম্পাইলারের কোডে প্রোগ্রাম করে দিয়েছে যাতে বরো চেকার এই পরিস্থিতিতে লাইফটাইমগুলো অনুমান করতে পারে এবং সুস্পষ্ট অ্যানোটেশনের প্রয়োজন না হয়।
Rust ইতিহাসের এই অংশটি প্রাসঙ্গিক কারণ ভবিষ্যতে আরও ডিটারমিনিস্টিক প্যাটার্ন ortaya আসতে পারে এবং কম্পাইলারে যোগ করা হতে পারে। ভবিষ্যতে, আরও কম লাইফটাইম অ্যানোটেশনের প্রয়োজন হতে পারে।
Rust-এর রেফারেন্স বিশ্লেষণের মধ্যে প্রোগ্রাম করা প্যাটার্নগুলোকে লাইফটাইম এলিশন রুলস (lifetime elision rules) বলা হয়। এগুলো প্রোগ্রামারদের অনুসরণ করার নিয়ম নয়; এগুলো এমন কিছু বিশেষ কেসের সেট যা কম্পাইলার বিবেচনা করবে, এবং যদি আপনার কোড এই কেসগুলোর সাথে মিলে যায়, তাহলে আপনাকে সুস্পষ্টভাবে লাইফটাইম লেখার প্রয়োজন নেই।
এলিশন রুলস সম্পূর্ণ অনুমান প্রদান করে না। যদি Rust রুলস প্রয়োগ করার পরেও রেফারেন্সগুলোর লাইফটাইম সম্পর্কে অস্পষ্টতা থাকে, কম্পাইলার বাকি রেফারেন্সগুলোর লাইফটাইম কী হওয়া উচিত তা অনুমান করবে না। অনুমান করার পরিবর্তে, কম্পাইলার আপনাকে একটি এরর দেবে যা আপনি লাইফটাইম অ্যানোটেশন যোগ করে সমাধান করতে পারেন।
ফাংশন বা মেথড প্যারামিটারে লাইফটাইমকে ইনপুট লাইফটাইম (input lifetimes) বলা হয়, এবং রিটার্ন ভ্যালুতে লাইফটাইমকে আউটপুট লাইফটাইম (output lifetimes) বলা হয়।
কম্পাইলার যখন সুস্পষ্ট অ্যানোটেশন না থাকে তখন রেফারেন্সের লাইফটাইম বের করার জন্য তিনটি নিয়ম ব্যবহার করে। প্রথম নিয়মটি ইনপুট লাইফটাইমের জন্য প্রযোজ্য, এবং দ্বিতীয় ও তৃতীয় নিয়ম আউটপুট লাইফটাইমের জন্য প্রযোজ্য। যদি কম্পাইলার তিনটি নিয়ম শেষে পৌঁছানোর পরেও এমন রেফারেন্স থাকে যার লাইফটাইম বের করতে পারে না, কম্পাইলার একটি এরর দিয়ে থেমে যাবে। এই নিয়মগুলো fn
ডেফিনিশন এবং impl
ব্লকের জন্য প্রযোজ্য।
প্রথম নিয়মটি হলো কম্পাইলার প্রতিটি প্যারামিটার যা একটি রেফারেন্স, তাকে একটি লাইফটাইম প্যারামিটার বরাদ্দ করে। অন্য কথায়, একটি প্যারামিটারযুক্ত একটি ফাংশন একটি লাইফটাইম প্যারামিটার পায়: fn foo<'a>(x: &'a i32)
; দুটি প্যারামিটারযুক্ত একটি ফাংশন দুটি পৃথক লাইফটাইম প্যারামিটার পায়: fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
; এবং এভাবেই চলতে থাকে।
দ্বিতীয় নিয়মটি হলো, যদি ঠিক একটি ইনপুট লাইফটাইম প্যারামিটার থাকে, তবে সেই লাইফটাইমটি সমস্ত আউটপুট লাইফটাইম প্যারামিটারে বরাদ্দ করা হয়: fn foo<'a>(x: &'a i32) -> &'a i32
।
তৃতীয় নিয়মটি হলো, যদি একাধিক ইনপুট লাইফটাইম প্যারামিটার থাকে, কিন্তু তাদের মধ্যে একটি &self
বা &mut self
হয় কারণ এটি একটি মেথড, তবে self
-এর লাইফটাইম সমস্ত আউটপুট লাইফটাইম প্যারামিটারে বরাদ্দ করা হয়। এই তৃতীয় নিয়মটি মেথডগুলোকে পড়া এবং লেখা অনেক সুন্দর করে তোলে কারণ কম প্রতীকের প্রয়োজন হয়।
চলুন আমরা কম্পাইলারের মতো ভান করি। আমরা লিস্টিং ১০-২৫-এর first_word
ফাংশনের সিগনেচারে রেফারেন্সের লাইফটাইম বের করার জন্য এই নিয়মগুলো প্রয়োগ করব। সিগনেচারটি রেফারেন্সের সাথে কোনো লাইফটাইম যুক্ত না করে শুরু হয়:
fn first_word(s: &str) -> &str {
তারপর কম্পাইলার প্রথম নিয়মটি প্রয়োগ করে, যা নির্দিষ্ট করে যে প্রতিটি প্যারামিটার তার নিজস্ব লাইফটাইম পায়। আমরা এটিকে যথারীতি 'a
বলব, তাই এখন সিগনেচারটি হলো:
fn first_word<'a>(s: &'a str) -> &str {
দ্বিতীয় নিয়মটি প্রযোজ্য কারণ ঠিক একটি ইনপুট লাইফটাইম আছে। দ্বিতীয় নিয়মটি নির্দিষ্ট করে যে একটি ইনপুট প্যারামিটারের লাইফটাইম আউটপুট লাইফটাইমে বরাদ্দ করা হয়, তাই সিগনেচারটি এখন হলো:
fn first_word<'a>(s: &'a str) -> &'a str {
এখন এই ফাংশন সিগনেচারের সমস্ত রেফারেন্সের লাইফটাইম আছে, এবং কম্পাইলার প্রোগ্রামারকে এই ফাংশন সিগনেচারে লাইফটাইম অ্যানোটেট করার প্রয়োজন ছাড়াই তার বিশ্লেষণ চালিয়ে যেতে পারে।
আসুন আরেকটি উদাহরণ দেখি, এবার longest
ফাংশনটি ব্যবহার করে যা আমরা লিস্টিং ১০-২০-এ কাজ শুরু করার সময় কোনো লাইফটাইম প্যারামিটার ছিল না:
fn longest(x: &str, y: &str) -> &str {
আসুন প্রথম নিয়মটি প্রয়োগ করি: প্রতিটি প্যারামিটার তার নিজস্ব লাইফটাইম পায়। এবার আমাদের একটির পরিবর্তে দুটি প্যারামিটার আছে, তাই আমাদের দুটি লাইফটাইম আছে:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
আপনি দেখতে পাচ্ছেন যে দ্বিতীয় নিয়মটি প্রযোজ্য নয় কারণ একাধিক ইনপুট লাইফটাইম আছে। তৃতীয় নিয়মটিও প্রযোজ্য নয়, কারণ longest
একটি ফাংশন, মেথড নয়, তাই কোনো প্যারামিটার self
নয়। তিনটি নিয়ম কাজ করার পরেও, আমরা এখনও বের করতে পারিনি যে রিটার্ন টাইপের লাইফটাইম কী। এই কারণেই আমরা লিস্টিং ১০-২০-এর কোড কম্পাইল করার চেষ্টা করার সময় একটি এরর পেয়েছিলাম: কম্পাইলার লাইফটাইম এলিশন রুলস কাজ করেছে কিন্তু এখনও সিগনেচারের সমস্ত রেফারেন্সের লাইফটাইম বের করতে পারেনি।
যেহেতু তৃতীয় নিয়মটি সত্যিই কেবল মেথড সিগনেচারে প্রযোজ্য, আমরা পরবর্তী অংশে সেই প্রেক্ষাপটে লাইফটাইম দেখব কেন তৃতীয় নিয়মের মানে হলো আমাদের মেথড সিগনেচারে খুব কমই লাইফটাইম অ্যানোটেট করতে হয়।
মেথড ডেফিনিশনে লাইফটাইম অ্যানোটেশন
যখন আমরা লাইফটাইমসহ একটি struct-এ মেথড ইমপ্লিমেন্ট করি, তখন আমরা জেনেরিক টাইপ প্যারামিটারের মতো একই সিনট্যাক্স ব্যবহার করি, যেমনটি লিস্টিং ১০-১১-এ দেখানো হয়েছে। আমরা কোথায় লাইফটাইম প্যারামিটার ডিক্লেয়ার এবং ব্যবহার করি তা নির্ভর করে সেগুলো struct ফিল্ড বা মেথড প্যারামিটার এবং রিটার্ন ভ্যালুর সাথে সম্পর্কিত কিনা তার উপর।
struct ফিল্ডের জন্য লাইফটাইম নাম সবসময় impl
কিওয়ার্ডের পরে ডিক্লেয়ার করতে হবে এবং তারপর struct-এর নামের পরে ব্যবহার করতে হবে কারণ সেই লাইফটাইমগুলো struct-এর টাইপের অংশ।
impl
ব্লকের ভিতরে মেথড সিগনেচারে, রেফারেন্সগুলো struct-এর ফিল্ডে থাকা রেফারেন্সের লাইফটাইমের সাথে আবদ্ধ হতে পারে, অথবা সেগুলো স্বাধীন হতে পারে। উপরন্তু, লাইফটাইম এলিশন রুলস প্রায়শই মেথড সিগনেচারে লাইফটাইম অ্যানোটেশনের প্রয়োজন হয় না। আসুন আমরা লিস্টিং ১০-২৪-এ সংজ্ঞায়িত ImportantExcerpt
নামক struct ব্যবহার করে কিছু উদাহরণ দেখি।
প্রথমে আমরা level
নামের একটি মেথড ব্যবহার করব যার একমাত্র প্যারামিটার হলো self
-এর একটি রেফারেন্স এবং যার রিটার্ন ভ্যালু একটি i32
, যা কোনো কিছুর রেফারেন্স নয়:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
impl
-এর পরে লাইফটাইম প্যারামিটার ডিক্লেয়ারেশন এবং টাইপের নামের পরে এর ব্যবহার প্রয়োজন, কিন্তু প্রথম এলিশন রুলের কারণে আমাদের self
-এর রেফারেন্সের লাইফটাইম অ্যানোটেট করার প্রয়োজন নেই।
এখানে একটি উদাহরণ যেখানে তৃতীয় লাইফটাইম এলিশন রুল প্রযোজ্য:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {announcement}"); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().unwrap(); let i = ImportantExcerpt { part: first_sentence, }; }
দুটি ইনপুট লাইফটাইম আছে, তাই Rust প্রথম লাইফটাইম এলিশন রুল প্রয়োগ করে এবং &self
এবং announcement
উভয়কেই তাদের নিজস্ব লাইফটাইম দেয়। তারপর, যেহেতু প্যারামিটারগুলোর মধ্যে একটি &self
, রিটার্ন টাইপ &self
-এর লাইফটাইম পায়, এবং সমস্ত লাইফটাইম হিসাব করা হয়েছে।
স্ট্যাটিক লাইফটাইম (The Static Lifetime)
একটি বিশেষ লাইফটাইম যা আমাদের আলোচনা করতে হবে তা হলো 'static
, যা বোঝায় যে প্রভাবিত রেফারেন্সটি প্রোগ্রামের পুরো সময়কাল ধরে বেঁচে থাকতে পারে। সমস্ত স্ট্রিং লিটারেলের 'static
লাইফটাইম থাকে, যা আমরা নিম্নরূপ অ্যানোটেট করতে পারি:
#![allow(unused)] fn main() { let s: &'static str = "I have a static lifetime."; }
এই স্ট্রিংয়ের টেক্সট সরাসরি প্রোগ্রামের বাইনারিতে সংরক্ষণ করা হয়, যা সর্বদা উপলব্ধ। তাই, সমস্ত স্ট্রিং লিটারেলের লাইফটাইম হলো 'static
।
আপনি এরর মেসেজে 'static
লাইফটাইম ব্যবহার করার পরামর্শ দেখতে পারেন। কিন্তু একটি রেফারেন্সের জন্য 'static
লাইফটাইম নির্দিষ্ট করার আগে, ভাবুন যে আপনার রেফারেন্সটি আসলে আপনার প্রোগ্রামের পুরো লাইফটাইম বেঁচে থাকে কিনা, এবং আপনি তা চান কিনা। বেশিরভাগ সময়, 'static
লাইফটাইম প্রস্তাবকারী একটি এরর মেসেজ একটি ড্যাংলিং রেফারেন্স তৈরি করার চেষ্টা বা উপলব্ধ লাইফটাইমের অমিলের ফলে হয়। এই ধরনের ক্ষেত্রে, সমাধান হলো সেই সমস্যাগুলো ঠিক করা, 'static
লাইফটাইম নির্দিষ্ট করা নয়।
জেনেরিক টাইপ প্যারামিটার, ট্রেইট বাউন্ড এবং লাইফটাইম একসাথে
আসুন সংক্ষেপে জেনেরিক টাইপ প্যারামিটার, ট্রেইট বাউন্ড এবং লাইফটাইম একসাথে একটি ফাংশনে নির্দিষ্ট করার সিনট্যাক্স দেখি!
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {result}"); } use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { println!("Announcement! {ann}"); if x.len() > y.len() { x } else { y } }
এটি লিস্টিং ১০-২১-এর longest
ফাংশন যা দুটি স্ট্রিং স্লাইসের মধ্যে দীর্ঘতরটি রিটার্ন করে। কিন্তু এখন এর ann
নামে একটি অতিরিক্ত প্যারামিটার আছে যা জেনেরিক টাইপ T
-এর, যা where
ক্লজ দ্বারা নির্দিষ্ট করা Display
trait ইমপ্লিমেন্ট করে এমন যেকোনো টাইপ দ্বারা পূরণ করা যেতে পারে। এই অতিরিক্ত প্যারামিটারটি {}
ব্যবহার করে প্রিন্ট করা হবে, যার কারণে Display
trait bound প্রয়োজন। যেহেতু লাইফটাইম একটি ধরনের জেনেরিক, তাই লাইফটাইম প্যারামিটার 'a
এবং জেনেরিক টাইপ প্যারামিটার T
-এর ডিক্লেয়ারেশন ফাংশনের নামের পরে অ্যাঙ্গেল ব্র্যাকেটের ভিতরে একই তালিকায় যায়।
সারাংশ
আমরা এই অধ্যায়ে অনেক কিছু কভার করেছি! এখন যেহেতু আপনি জেনেরিক টাইপ প্যারামিটার, ট্রেইট এবং ট্রেইট বাউন্ড এবং জেনেরিক লাইফটাইম প্যারামিটার সম্পর্কে জানেন, আপনি পুনরাবৃত্তি ছাড়াই কোড লিখতে প্রস্তুত যা বিভিন্ন পরিস্থিতিতে কাজ করে। জেনেরিক টাইপ প্যারামিটার আপনাকে বিভিন্ন টাইপের উপর কোড প্রয়োগ করতে দেয়। ট্রেইট এবং ট্রেইট বাউন্ড নিশ্চিত করে যে যদিও টাইপগুলো জেনেরিক, তাদের সেই আচরণ থাকবে যা কোডের প্রয়োজন। আপনি শিখেছেন কীভাবে লাইফটাইম অ্যানোটেশন ব্যবহার করে নিশ্চিত করতে হয় যে এই ফ্লেক্সিবল কোডে কোনো ড্যাংলিং রেফারেন্স থাকবে না। এবং এই সমস্ত বিশ্লেষণ কম্পাইল টাইমে ঘটে, যা রানটাইম পারফরম্যান্সকে প্রভাবিত করে না!
বিশ্বাস করুন বা না করুন, আমরা এই অধ্যায়ে যে বিষয়গুলো আলোচনা করেছি সে সম্পর্কে আরও অনেক কিছু শেখার আছে: চ্যাপ্টার ১৮ ট্রেইট অবজেক্ট নিয়ে আলোচনা করে, যা ট্রেইট ব্যবহার করার আরেকটি উপায়। লাইফটাইম অ্যানোটেশন জড়িত আরও জটিল পরিস্থিতিও রয়েছে যা আপনার কেবল খুব উন্নত পরিস্থিতিতে প্রয়োজন হবে; সেগুলোর জন্য, আপনার Rust Reference পড়া উচিত। কিন্তু এর পরে, আপনি শিখবেন কীভাবে Rust-এ টেস্ট লিখতে হয় যাতে আপনি নিশ্চিত করতে পারেন যে আপনার কোড যেভাবে কাজ করা উচিত সেভাবে কাজ করছে।
স্বয়ংক্রিয় টেস্ট লেখা
১৯৭২ সালে Edsger W. Dijkstra তার “The Humble Programmer” প্রবন্ধে বলেছিলেন যে, “প্রোগ্রাম টেস্টিং বাগ (bug) এর উপস্থিতি দেখানোর জন্য খুব কার্যকর একটি উপায় হতে পারে, কিন্তু এটি বাগের অনুপস্থিতি দেখানোর জন্য একেবারেই যথেষ্ট নয়।” এর মানে এই নয় যে আমরা যতটা সম্ভব টেস্ট করার চেষ্টা করব না!
আমাদের প্রোগ্রামের নির্ভুলতা (correctness) বলতে বোঝায়, আমাদের কোড ঠিক সেই কাজটি কতটা ভালোভাবে করে যা আমরা করতে চেয়েছিলাম। Rust ডিজাইন করার সময় প্রোগ্রামের নির্ভুলতার উপর অনেক বেশি গুরুত্ব দেওয়া হয়েছে, কিন্তু নির্ভুলতা একটি জটিল বিষয় এবং এটি প্রমাণ করা সহজ নয়। Rust-এর type system এই গুরুদায়িত্বের একটি বড় অংশ পালন করে, কিন্তু type system সবকিছু ধরতে পারে না। একারণে, Rust-এ স্বয়ংক্রিয় সফটওয়্যার টেস্ট (automated software test) লেখার জন্য সাপোর্ট অন্তর্ভুক্ত করা হয়েছে।
ধরা যাক, আমরা add_two
নামে একটি function লিখলাম যা তার প্যারামিটারে (parameter) আসা যেকোনো সংখ্যার সাথে ২ যোগ করে। এই function-টির সিগনেচার একটি integer প্যারামিটার হিসেবে গ্রহণ করে এবং একটি integer ফলাফল হিসেবে রিটার্ন করে। যখন আমরা সেই function-টি ইমপ্লিমেন্ট (implement) এবং কম্পাইল (compile) করি, তখন Rust সমস্ত type checking এবং borrow checking সম্পন্ন করে, যা আপনারা এর মধ্যেই শিখেছেন। এটি নিশ্চিত করে যে আমরা যেন এই function-এ String
ভ্যালু বা কোনো অবৈধ reference
পাস না করি। কিন্তু Rust এটা পরীক্ষা করতে পারে না যে এই function-টি ঠিক সেটাই করবে যা আমরা করতে চেয়েছিলাম, অর্থাৎ প্যারামিটারের সাথে ২ যোগ করবে, ১০ যোগ বা ৫০ বিয়োগ নয়! এখানেই টেস্টের প্রয়োজনীয়তা আসে।
আমরা এমন টেস্ট লিখতে পারি যা assert করে, উদাহরণস্বরূপ, যখন আমরা add_two
ফাংশনে 3
পাস করব, তখন রিটার্ন ভ্যালু হবে 5
। আমরা যখনই আমাদের কোডে কোনো পরিবর্তন আনি, তখন এই টেস্টগুলো চালাতে পারি। এটি নিশ্চিত করে যে বিদ্যমান সঠিক আচরণে (correct behavior) কোনো পরিবর্তন আসেনি।
টেস্টিং একটি জটিল দক্ষতা: যদিও এক অধ্যায়ে ভালো টেস্ট লেখার সমস্ত খুঁটিনাটি আলোচনা করা সম্ভব নয়, এই অধ্যায়ে আমরা Rust-এর টেস্টিং ব্যবস্থার (testing facilities) কার্যপ্রণালী নিয়ে আলোচনা করব। আমরা টেস্ট লেখার জন্য উপলব্ধ অ্যানোটেশন (annotations) এবং ম্যাক্রো (macros), টেস্ট চালানোর জন্য ডিফল্ট আচরণ ও অপশন এবং কীভাবে টেস্টগুলোকে ইউনিট টেস্ট (unit tests) এবং ইন্টিগ্রেশন টেস্টে (integration tests) সাজানো যায় তা নিয়ে কথা বলব।
টেস্ট কিভাবে লিখতে হয়
টেস্ট হলো Rust ফাংশন যা ভেরিফাই (verify) করে যে নন-টেস্ট কোড প্রত্যাশিত পদ্ধতিতে কাজ করছে কিনা। টেস্ট ফাংশনের বডি (body) সাধারণত এই তিনটি কাজ করে:
- প্রয়োজনীয় ডেটা বা স্টেট সেট আপ করা।
- আপনি যে কোডটি টেস্ট করতে চান তা রান করা।
- ফলাফল আপনার প্রত্যাশা অনুযায়ী কিনা তা অ্যাসার্ট (assert) করা।
আসুন, Rust এর সেই ফিচারগুলো দেখি যা বিশেষভাবে টেস্ট লেখার জন্য এই কাজগুলো করতে সাহায্য করে। এর মধ্যে রয়েছে test
অ্যাট্রিবিউট, কয়েকটি ম্যাক্রো এবং should_panic
অ্যাট্রিবিউট।
একটি টেস্ট ফাংশনের গঠন
সহজ ভাষায়, Rust-এ একটি টেস্ট হলো এমন একটি ফাংশন যা test
অ্যাট্রিবিউট দিয়ে অ্যানোটেট (annotated) করা থাকে। অ্যাট্রিবিউট হলো Rust কোডের বিভিন্ন অংশ সম্পর্কে মেটাডেটা; এর একটি উদাহরণ হলো derive
অ্যাট্রিবিউট যা আমরা পঞ্চম অধ্যায়ে struct-এর সাথে ব্যবহার করেছি। একটি সাধারণ ফাংশনকে টেস্ট ফাংশনে রূপান্তর করতে, fn
এর আগের লাইনে #[test]
যোগ করুন। যখন আপনি cargo test
কমান্ড দিয়ে আপনার টেস্টগুলো চালান, তখন Rust একটি টেস্ট রানার বাইনারি (test runner binary) তৈরি করে যা এই অ্যানোটেট করা ফাংশনগুলো চালায় এবং প্রতিটি টেস্ট ফাংশন পাস করেছে না ফেইল করেছে তার রিপোর্ট দেয়।
যখনই আমরা Cargo দিয়ে একটি নতুন লাইব্রেরি প্রজেক্ট তৈরি করি, তখন আমাদের জন্য স্বয়ংক্রিয়ভাবে একটি টেস্ট মডিউল এবং তার ভেতরে একটি টেস্ট ফাংশন তৈরি হয়ে যায়। এই মডিউলটি আপনাকে টেস্ট লেখার জন্য একটি টেমপ্লেট দেয়, যাতে প্রতিবার নতুন প্রজেক্ট শুরু করার সময় আপনাকে সঠিক গঠন এবং সিনট্যাক্স খুঁজতে না হয়। আপনি যত খুশি অতিরিক্ত টেস্ট ফাংশন এবং টেস্ট মডিউল যোগ করতে পারেন!
কোনো কোড টেস্ট করার আগে, আমরা টেমপ্লেট টেস্টটি নিয়ে পরীক্ষা করে দেখব টেস্টগুলো কীভাবে কাজ করে। তারপর আমরা কিছু বাস্তবসম্মত টেস্ট লিখব যা আমাদের লেখা কোডকে কল করবে এবং তার আচরণ সঠিক কিনা তা অ্যাসার্ট করবে।
আসুন adder
নামে একটি নতুন লাইব্রেরি প্রজেক্ট তৈরি করি যা দুটি সংখ্যা যোগ করবে:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
আপনার adder
লাইব্রেরির src/lib.rs ফাইলের কন্টেন্ট তালিকা ১১-১ এর মতো দেখতে হওয়া উচিত।
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
ফাইলটি একটি উদাহরণ add
ফাংশন দিয়ে শুরু হয়, যাতে আমাদের টেস্ট করার জন্য কিছু একটা থাকে।
আপাতত, আসুন আমরা শুধু it_works
ফাংশনটির উপর মনোযোগ দিই। #[test]
অ্যানোটেশনটি লক্ষ্য করুন: এই অ্যাট্রিবিউটটি নির্দেশ করে যে এটি একটি টেস্ট ফাংশন, তাই টেস্ট রানার জানে যে এই ফাংশনটিকে একটি টেস্ট হিসাবে গণ্য করতে হবে। আমাদের tests
মডিউলে নন-টেস্ট ফাংশনও থাকতে পারে যা সাধারণ পরিস্থিতি সেট আপ করতে বা সাধারণ অপারেশন করতে সাহায্য করে, তাই আমাদের সবসময় নির্দিষ্ট করে দিতে হবে কোন ফাংশনগুলো টেস্ট।
উদাহরণ ফাংশন বডি assert_eq!
ম্যাক্রো ব্যবহার করে অ্যাসার্ট করে যে result
(যেখানে ২ এবং ২ দিয়ে add
কল করার ফলাফল রয়েছে) এর মান ৪ এর সমান। এই অ্যাসার্শনটি একটি সাধারণ টেস্টের ফরম্যাটের উদাহরণ হিসাবে কাজ করে। চলুন এটি রান করে দেখি যে এই টেস্টটি পাস করে কিনা।
cargo test
কমান্ডটি আমাদের প্রজেক্টের সমস্ত টেস্ট চালায়, যা তালিকা ১১-২-এ দেখানো হয়েছে।
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Cargo টেস্টটি কম্পাইল এবং রান করেছে। আমরা running 1 test
লাইনটি দেখতে পাচ্ছি। পরবর্তী লাইনটি জেনারেট হওয়া টেস্ট ফাংশনের নাম দেখায়, যা হলো tests::it_works
এবং সেই টেস্টটি চালানোর ফলাফল হলো ok
। সামগ্রিক সারাংশ test result: ok.
এর মানে হলো সমস্ত টেস্ট পাস করেছে এবং 1 passed; 0 failed
অংশটি পাস বা ফেইল করা টেস্টের সংখ্যা দেখায়।
একটি টেস্টকে ইগনোর (ignored) হিসেবে চিহ্নিত করা সম্ভব যাতে এটি একটি নির্দিষ্ট ক্ষেত্রে রান না হয়; আমরা এই অধ্যায়ের পরে "Ignoring Some Tests Unless Specifically Requested" বিভাগে এটি আলোচনা করব। যেহেতু আমরা এখানে তা করিনি, তাই সারাংশে 0 ignored
দেখাচ্ছে। আমরা cargo test
কমান্ডে একটি আর্গুমেন্ট পাস করে শুধুমাত্র সেইসব টেস্ট চালাতে পারি যাদের নাম একটি স্ট্রিং এর সাথে মেলে; একে ফিল্টারিং বলা হয় এবং আমরা এটি "Running a Subset of Tests by Name” বিভাগে আলোচনা করব। এখানে আমরা কোনো টেস্ট ফিল্টার করিনি, তাই সারাংশের শেষে 0 filtered out
দেখাচ্ছে।
0 measured
পরিসংখ্যানটি বেঞ্চমার্ক টেস্টের জন্য যা পারফরম্যান্স পরিমাপ করে। বেঞ্চমার্ক টেস্ট, এই লেখা পর্যন্ত, শুধুমাত্র নাইটলি রাস্ট-এ (nightly Rust) উপলব্ধ। বেঞ্চমার্ক টেস্ট সম্পর্কে আরও জানতে the documentation about benchmark tests দেখুন।
টেস্ট আউটপুটের পরবর্তী অংশ যা Doc-tests adder
দিয়ে শুরু হয়েছে, তা যেকোনো ডকুমেন্টেশন টেস্টের ফলাফলের জন্য। আমাদের এখনো কোনো ডকুমেন্টেশন টেস্ট নেই, কিন্তু Rust আমাদের API ডকুমেন্টেশনে থাকা যেকোনো কোড উদাহরণ কম্পাইল করতে পারে। এই ফিচারটি আপনার ডকুমেন্টেশন এবং আপনার কোডকে সিঙ্কে রাখতে সাহায্য করে! আমরা ১৪ অধ্যায়ের “Documentation Comments as Tests” বিভাগে কীভাবে ডকুমেন্টেশন টেস্ট লিখতে হয় তা আলোচনা করব। আপাতত, আমরা Doc-tests
আউটপুটটি উপেক্ষা করব।
আসুন আমাদের প্রয়োজন অনুযায়ী টেস্টটি কাস্টমাইজ করা শুরু করি। প্রথমে, it_works
ফাংশনের নাম পরিবর্তন করে অন্য কোনো নাম দিন, যেমন exploration
, এইভাবে:
Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
তারপর আবার cargo test
চালান। আউটপুট এখন it_works
এর পরিবর্তে exploration
দেখাবে:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
এখন আমরা আরেকটি টেস্ট যোগ করব, কিন্তু এবার আমরা এমন একটি টেস্ট তৈরি করব যা ফেইল করবে! টেস্ট তখন ফেইল করে যখন টেস্ট ফাংশনের কোনো কিছু প্যানিক (panic) করে। প্রতিটি টেস্ট একটি নতুন থ্রেডে (thread) চালানো হয়, এবং যখন প্রধান থ্রেড দেখে যে একটি টেস্ট থ্রেড মারা গেছে, তখন টেস্টটিকে ফেইল হিসেবে চিহ্নিত করা হয়। নবম অধ্যায়ে আমরা আলোচনা করেছি যে প্যানিক করার সবচেয়ে সহজ উপায় হলো panic!
ম্যাক্রো কল করা। নতুন টেস্টটি another
নামে একটি ফাংশন হিসাবে প্রবেশ করান, যাতে আপনার src/lib.rs ফাইলটি তালিকা ১১-৩ এর মতো দেখায়।
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
cargo test
ব্যবহার করে আবার টেস্টগুলো চালান। আউটপুটটি তালিকা ১১-৪ এর মতো হওয়া উচিত, যা দেখায় যে আমাদের exploration
টেস্ট পাস করেছে এবং another
ফেইল করেছে।
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
ok
এর পরিবর্তে, test tests::another
লাইনটি FAILED
দেখাচ্ছে। স্বতন্ত্র ফলাফলের এবং সারাংশের মধ্যে দুটি নতুন বিভাগ উপস্থিত হয়েছে: প্রথমটি প্রতিটি টেস্ট ফেইলের বিস্তারিত কারণ প্রদর্শন করে। এক্ষেত্রে, আমরা বিস্তারিতভাবে জানতে পারি যে tests::another
ফেইল করেছে কারণ এটি src/lib.rs ফাইলের ১৭ নম্বর লাইনে Make this test fail
বার্তা দিয়ে প্যানিক করেছে। পরবর্তী বিভাগটি শুধুমাত্র সমস্ত ফেইল করা টেস্টের নাম তালিকাভুক্ত করে, যা অনেক টেস্ট এবং অনেক বিস্তারিত ফেইলিং টেস্ট আউটপুট থাকলে কার্যকর। আমরা একটি ফেইল করা টেস্টের নাম ব্যবহার করে শুধুমাত্র সেই টেস্টটি চালাতে পারি যাতে এটি ডিবাগ করা সহজ হয়; আমরা “Controlling How Tests Are Run” বিভাগে টেস্ট চালানোর উপায় সম্পর্কে আরও কথা বলব।
সারাংশ লাইনটি শেষে প্রদর্শিত হয়: সামগ্রিকভাবে, আমাদের টেস্ট ফলাফল FAILED
। আমাদের একটি টেস্ট পাস করেছে এবং একটি ফেইল করেছে।
এখন যেহেতু আপনি বিভিন্ন পরিস্থিতিতে টেস্টের ফলাফল কেমন দেখায় তা দেখেছেন, আসুন panic!
ছাড়া অন্য কিছু ম্যাক্রো দেখি যা টেস্টে কার্যকর।
assert!
ম্যাক্রো দিয়ে ফলাফল পরীক্ষা করা
assert!
ম্যাক্রো, যা স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা হয়, তখন খুব দরকারি যখন আপনি নিশ্চিত করতে চান যে টেস্টের কোনো একটি শর্ত true
হিসেবে মূল্যায়ন হয়। আমরা assert!
ম্যাক্রোকে একটি আর্গুমেন্ট দিই যা একটি বুলিয়ানে (Boolean) পরিণত হয়। যদি মান true
হয়, কিছুই হয় না এবং টেস্ট পাস করে। যদি মান false
হয়, assert!
ম্যাক্রো panic!
কল করে টেস্টটিকে ফেইল করায়। assert!
ম্যাক্রো ব্যবহার করে আমরা পরীক্ষা করতে পারি যে আমাদের কোডটি আমাদের উদ্দেশ্য অনুযায়ী কাজ করছে কিনা।
অধ্যায় ৫, তালিকা ৫-১৫-তে, আমরা একটি Rectangle
struct এবং একটি can_hold
মেথড ব্যবহার করেছিলাম, যা এখানে তালিকা ১১-৫-এ পুনরাবৃত্তি করা হলো। চলুন এই কোডটি src/lib.rs ফাইলে রাখি, তারপর assert!
ম্যাক্রো ব্যবহার করে এর জন্য কিছু টেস্ট লিখি।
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
can_hold
মেথডটি একটি বুলিয়ান রিটার্ন করে, যার মানে এটি assert!
ম্যাক্রোর জন্য একটি উপযুক্ত ব্যবহারক্ষেত্র। তালিকা ১১-৬-এ, আমরা একটি টেস্ট লিখছি যা can_hold
মেথডটি ব্যবহার করে। এতে আমরা ৮ প্রস্থ এবং ৭ উচ্চতার একটি Rectangle
ইনস্ট্যান্স তৈরি করি এবং অ্যাসার্ট করি যে এটি ৫ প্রস্থ এবং ১ উচ্চতার আরেকটি Rectangle
ইনস্ট্যান্সকে ধারণ করতে পারে।
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
tests
মডিউলের ভিতরে use super::*;
লাইনটি লক্ষ্য করুন। tests
মডিউলটি একটি সাধারণ মডিউল যা আমরা ৭ অধ্যায়ের "Paths for Referring to an Item in the Module Tree" বিভাগে আলোচনা করা সাধারণ ভিজিবিলিটি নিয়ম অনুসরণ করে। যেহেতু tests
মডিউলটি একটি অভ্যন্তরীণ মডিউল, তাই আমাদের বাইরের মডিউলের কোডটি ভেতরের মডিউলের স্কোপে আনতে হবে। আমরা এখানে একটি গ্লব (glob) ব্যবহার করি, তাই বাইরের মডিউলে আমরা যা কিছু ডিফাইন করি তা এই tests
মডিউলের জন্য উপলব্ধ থাকে।
আমরা আমাদের টেস্টের নাম দিয়েছি larger_can_hold_smaller
এবং আমাদের প্রয়োজনীয় দুটি Rectangle
ইনস্ট্যান্স তৈরি করেছি। তারপর আমরা assert!
ম্যাক্রো কল করে তাতে larger.can_hold(&smaller)
এর ফলাফল পাস করেছি। এই এক্সপ্রেশনটির true
রিটার্ন করার কথা, তাই আমাদের টেস্ট পাস করা উচিত। চলুন দেখি!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
এটি পাস করেছে! আসুন আরেকটি টেস্ট যোগ করি, এবার অ্যাসার্ট করব যে একটি ছোট আয়তক্ষেত্র একটি বড় আয়তক্ষেত্রকে ধারণ করতে পারে না:
Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --snip--
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
যেহেতু এই ক্ষেত্রে can_hold
ফাংশনের সঠিক ফলাফল false
, তাই আমাদের assert!
ম্যাক্রোতে পাস করার আগে সেই ফলাফলটিকে নেগেট (negate) করতে হবে। ফলস্বরূপ, যদি can_hold
false
রিটার্ন করে তাহলে আমাদের টেস্ট পাস করবে:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
দুটি টেস্টই পাস করেছে! এখন দেখা যাক আমাদের কোডে একটি বাগ প্রবেশ করালে টেস্টের ফলাফলে কী ঘটে। আমরা can_hold
মেথডের ইমপ্লিমেন্টেশনে প্রস্থ তুলনা করার সময় গ্রেটার-দ্যান চিহ্নের পরিবর্তে একটি লেস-দ্যান চিহ্ন দিয়ে পরিবর্তন করব:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
এখন টেস্ট চালালে নিম্নলিখিত ফলাফল পাওয়া যাবে:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib````
আমাদের টেস্ট বাগটি ধরে ফেলেছে! যেহেতু `larger.width` হলো `8` এবং `smaller.width` হলো `5`, `can_hold`-এ প্রস্থের তুলনা এখন `false` রিটার্ন করছে: ৮, ৫-এর থেকে ছোট নয়।
### `assert_eq!` এবং `assert_ne!` ম্যাক্রো দিয়ে সমতা পরীক্ষা করা
ফাংশনালিটি যাচাই করার একটি সাধারণ উপায় হলো, টেস্ট করা কোডের ফলাফল এবং আপনার প্রত্যাশিত মানের মধ্যে সমতা পরীক্ষা করা। আপনি `assert!` ম্যাক্রো এবং `==` অপারেটর ব্যবহার করে এটি করতে পারেন। যাইহোক, এটি এত সাধারণ একটি পরীক্ষা যে স্ট্যান্ডার্ড লাইব্রেরি এই কাজটি আরও সুবিধাজনকভাবে করার জন্য `assert_eq!` এবং `assert_ne!`—নামে একজোড়া ম্যাক্রো সরবরাহ করে। এই ম্যাক্রোগুলো দুটি আর্গুমেন্টকে যথাক্রমে সমতা বা অসমতার জন্য তুলনা করে। যদি অ্যাসার্শন ফেইল করে, তবে তারা দুটি মানই প্রিন্ট করবে, যা টেস্টটি _কেন_ ফেইল করেছে তা দেখতে সহজ করে তোলে; বিপরীতভাবে, `assert!` ম্যাক্রো শুধুমাত্র নির্দেশ করে যে এটি `==` এক্সপ্রেশনের জন্য একটি `false` মান পেয়েছে, কিন্তু যে মানগুলোর কারণে `false` হয়েছে তা প্রিন্ট করে না।
তালিকা ১১-৭-এ, আমরা `add_two` নামে একটি ফাংশন লিখছি যা এর প্যারামিটারের সাথে `2` যোগ করে, তারপর আমরা `assert_eq!` ম্যাক্রো ব্যবহার করে এই ফাংশনটি টেস্ট করি।
<Listing number="11-7" file-name="src/lib.rs" caption="`assert_eq!` ম্যাক্রো ব্যবহার করে `add_two` ফাংশন টেস্ট করা">
```rust,noplayground
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
চলুন পরীক্ষা করি এটি পাস করে কিনা!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
আমরা result
নামে একটি ভ্যারিয়েবল তৈরি করেছি যা add_two(2)
কল করার ফলাফল ধারণ করে। তারপর আমরা result
এবং 4
কে assert_eq!
ম্যাক্রোর আর্গুমেন্ট হিসেবে পাস করেছি। এই টেস্টের আউটপুট লাইনটি হলো test tests::it_adds_two ... ok
, এবং ok
টেক্সটটি নির্দেশ করে যে আমাদের টেস্ট পাস করেছে!
আসুন আমাদের কোডে একটি বাগ প্রবেশ করাই এবং দেখি assert_eq!
ফেইল করলে কেমন দেখায়। add_two
ফাংশনের ইমপ্লিমেন্টেশন পরিবর্তন করে 3
যোগ করার ব্যবস্থা করি:
pub fn add_two(a: u64) -> u64 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
আবার টেস্ট চালান:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
আমাদের টেস্ট বাগটি ধরে ফেলেছে! tests::it_adds_two
টেস্টটি ফেইল করেছে, এবং মেসেজটি আমাদের বলছে যে যে অ্যাসার্শনটি ফেইল করেছে তা হলো left == right
এবং left
ও right
এর মান কী। এই মেসেজটি আমাদের ডিবাগিং শুরু করতে সাহায্য করে: left
আর্গুমেন্ট, যেখানে add_two(2)
কল করার ফলাফল ছিল, সেটি ছিল 5
কিন্তু right
আর্গুমেন্ট ছিল 4
। আপনি কল্পনা করতে পারেন যে যখন আমাদের অনেকগুলো টেস্ট থাকবে তখন এটি বিশেষভাবে সহায়ক হবে।
উল্লেখ্য যে কিছু ভাষা এবং টেস্ট ফ্রেমওয়ার্কে, সমতা অ্যাসার্শন ফাংশনের প্যারামিটারগুলোকে expected
এবং actual
বলা হয় এবং আমরা কোন ক্রমে আর্গুমেন্টগুলো নির্দিষ্ট করি তা গুরুত্বপূর্ণ। যাইহোক, Rust-এ এগুলোকে left
এবং right
বলা হয় এবং আমরা প্রত্যাশিত মান এবং কোডের উৎপাদিত মানের ক্রম নির্দিষ্ট করার ক্ষেত্রে কোনো বাধ্যবাধকতা নেই। আমরা এই টেস্টের অ্যাসার্শনটি assert_eq!(4, result)
হিসেবেও লিখতে পারতাম, যা একই ফেইলার মেসেজ দিত যা assertion `left == right` failed
প্রদর্শন করে।
assert_ne!
ম্যাক্রো পাস করবে যদি আমরা দেওয়া দুটি মান সমান না হয় এবং ফেইল করবে যদি তারা সমান হয়। এই ম্যাক্রোটি সেইসব ক্ষেত্রে সবচেয়ে কার্যকর যখন আমরা নিশ্চিত নই যে একটি মান কী হবে, কিন্তু আমরা জানি যে মানটি নিশ্চিতভাবে কী হওয়া উচিত নয়। উদাহরণস্বরূপ, যদি আমরা এমন একটি ফাংশন টেস্ট করি যা তার ইনপুটকে কোনোভাবে পরিবর্তন করার গ্যারান্টি দেয়, কিন্তু ইনপুটটি কোন উপায়ে পরিবর্তিত হবে তা সপ্তাহের কোন দিনে আমরা টেস্ট চালাচ্ছি তার উপর নির্ভর করে, তবে সবচেয়ে ভালো অ্যাসার্শন হতে পারে যে ফাংশনের আউটপুট ইনপুটের সমান নয়।
ভিতরে ভিতরে, assert_eq!
এবং assert_ne!
ম্যাক্রোগুলো যথাক্রমে ==
এবং !=
অপারেটর ব্যবহার করে। যখন অ্যাসার্শন ফেইল করে, তখন এই ম্যাক্রোগুলো ডিবাগ ফরম্যাটিং ব্যবহার করে তাদের আর্গুমেন্ট প্রিন্ট করে, যার মানে হলো তুলনা করা মানগুলোকে অবশ্যই PartialEq
এবং Debug
ট্রেইট ইমপ্লিমেন্ট করতে হবে। সমস্ত প্রিমিটিভ টাইপ এবং স্ট্যান্ডার্ড লাইব্রেরির বেশিরভাগ টাইপ এই ট্রেইটগুলো ইমপ্লিমেন্ট করে। আপনার নিজের ডিফাইন করা struct এবং enum-এর জন্য, সেই টাইপগুলোর সমতা অ্যাসার্ট করতে আপনাকে PartialEq
ইমপ্লিমেন্ট করতে হবে। অ্যাসার্শন ফেইল করলে মানগুলো প্রিন্ট করার জন্য আপনাকে Debug
ইমপ্লিমেন্ট করতে হবে। যেহেতু উভয়ই ডিরাইভেবল ট্রেইট (derivable traits), যেমনটি অধ্যায় ৫ এর তালিকা ৫-১২ তে উল্লেখ করা হয়েছে, এটি সাধারণত আপনার struct বা enum ডেফিনিশনে #[derive(PartialEq, Debug)]
অ্যানোটেশন যোগ করার মতোই সহজ। এই এবং অন্যান্য ডিরাইভেবল ট্রেইট সম্পর্কে আরও বিস্তারিত জানতে পরিশিষ্ট C, “Derivable Traits,” দেখুন।
কাস্টম ফেইলার মেসেজ যোগ করা
আপনি assert!
, assert_eq!
, এবং assert_ne!
ম্যাক্রোগুলোতে ঐচ্ছিক আর্গুমেন্ট হিসেবে ফেইলার মেসেজের সাথে প্রিন্ট করার জন্য একটি কাস্টম মেসেজও যোগ করতে পারেন। প্রয়োজনীয় আর্গুমেন্টের পরে নির্দিষ্ট করা যেকোনো আর্গুমেন্ট format!
ম্যাক্রোতে (অধ্যায় ৮-এর “Concatenation with the +
Operator or the format!
Macro” বিভাগে আলোচিত) পাস করা হয়, তাই আপনি একটি ফরম্যাট স্ট্রিং পাস করতে পারেন যাতে {}
প্লেসহোল্ডার এবং সেই প্লেসহোল্ডারে যাওয়ার জন্য মান থাকে। কাস্টম মেসেজ একটি অ্যাসার্শনের অর্থ নথিভুক্ত করার জন্য কার্যকর; যখন একটি টেস্ট ফেইল করে, তখন কোডের সমস্যাটি কী সে সম্পর্কে আপনার একটি ভালো ধারণা থাকবে।
উদাহরণস্বরূপ, ধরা যাক আমাদের এমন একটি ফাংশন আছে যা নাম ধরে লোকেদের সম্ভাষণ জানায় এবং আমরা টেস্ট করতে চাই যে আমরা ফাংশনে যে নামটি পাস করছি তা আউটপুটে প্রদর্শিত হচ্ছে কিনা:
Filename: src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hello {name}!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
এই প্রোগ্রামের প্রয়োজনীয়তা এখনও ঠিক হয়নি, এবং আমরা প্রায় নিশ্চিত যে সম্ভাষণের শুরুতে থাকা Hello
টেক্সট পরিবর্তন হবে। আমরা সিদ্ধান্ত নিয়েছি যে প্রয়োজনীয়তা পরিবর্তিত হলে আমাদের টেস্ট আপডেট করতে হবে না, তাই greeting
ফাংশন থেকে ফিরে আসা মানের সাথে হুবহু সমতা পরীক্ষা করার পরিবর্তে, আমরা কেবল অ্যাসার্ট করব যে আউটপুট ইনপুট প্যারামিটারের টেক্সট ধারণ করে।
এখন এই কোডে একটি বাগ প্রবেশ করাই greeting
ফাংশন পরিবর্তন করে name
বাদ দিয়ে, যাতে ডিফল্ট টেস্ট ফেইলার কেমন দেখায় তা দেখতে পারি:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
এই টেস্টটি চালালে নিম্নলিখিত ফলাফল পাওয়া যায়:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
এই ফলাফলটি কেবল নির্দেশ করে যে অ্যাসার্শনটি ফেইল করেছে এবং অ্যাসার্শনটি কোন লাইনে আছে। একটি আরও দরকারী ফেইলার মেসেজ greeting
ফাংশন থেকে প্রাপ্ত মানটি প্রিন্ট করত। আসুন আমরা একটি কাস্টম ফেইলার মেসেজ যোগ করি যা একটি ফরম্যাট স্ট্রিং এবং greeting
ফাংশন থেকে প্রাপ্ত আসল মান দিয়ে পূরণ করা একটি প্লেসহোল্ডার নিয়ে গঠিত:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
}
এখন যখন আমরা টেস্টটি চালাব, আমরা একটি আরও তথ্যপূর্ণ ত্রুটি বার্তা পাব:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
আমরা টেস্ট আউটপুটে আসলে যে মানটি পেয়েছি তা দেখতে পাচ্ছি, যা আমাদের প্রত্যাশার পরিবর্তে কী ঘটেছে তা ডিবাগ করতে সাহায্য করবে।
should_panic
দিয়ে প্যানিক পরীক্ষা করা
রিটার্ন ভ্যালু পরীক্ষা করার পাশাপাশি, আমাদের কোড প্রত্যাশা অনুযায়ী ত্রুটির শর্তগুলো পরিচালনা করছে কিনা তা পরীক্ষা করাও গুরুত্বপূর্ণ। উদাহরণস্বরূপ, Guess
টাইপটি বিবেচনা করুন যা আমরা অধ্যায় ৯, তালিকা ৯-১৩-এ তৈরি করেছি। Guess
ব্যবহারকারী অন্যান্য কোড এই গ্যারান্টির উপর নির্ভর করে যে Guess
ইনস্ট্যান্সগুলিতে শুধুমাত্র ১ থেকে ১০০ এর মধ্যে মান থাকবে। আমরা এমন একটি টেস্ট লিখতে পারি যা নিশ্চিত করে যে সেই সীমার বাইরের কোনো মান দিয়ে একটি Guess
ইনস্ট্যান্স তৈরি করার চেষ্টা করলে তা প্যানিক করে।
আমরা আমাদের টেস্ট ফাংশনে should_panic
অ্যাট্রিবিউট যোগ করে এটি করি। যদি ফাংশনের ভিতরের কোড প্যানিক করে তবে টেস্টটি পাস করে; যদি ফাংশনের ভিতরের কোড প্যানিক না করে তবে টেস্টটি ফেইল করে।
তালিকা ১১-৮ একটি টেস্ট দেখায় যা পরীক্ষা করে যে Guess::new
এর ত্রুটির শর্তগুলো আমাদের প্রত্যাশা অনুযায়ী ঘটে কিনা।
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
আমরা #[should_panic]
অ্যাট্রিবিউটটি #[test]
অ্যাট্রিবিউটের পরে এবং যে টেস্ট ফাংশনে এটি প্রযোজ্য তার আগে স্থাপন করি। আসুন দেখি এই টেস্টটি পাস করলে ফলাফল কেমন হয়:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
বেশ ভালো দেখাচ্ছে! এখন আমাদের কোডে একটি বাগ প্রবেশ করাই new
ফাংশনের সেই শর্তটি সরিয়ে দিয়ে যা বলে যে মান ১০০ এর বেশি হলে ফাংশনটি প্যানিক করবে:
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
যখন আমরা তালিকা ১১-৮-এর টেস্টটি চালাই, তখন এটি ফেইল করবে:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
এক্ষেত্রে আমরা খুব সহায়ক বার্তা পাই না, কিন্তু যখন আমরা টেস্ট ফাংশনটি দেখি, তখন আমরা দেখতে পাই যে এটি #[should_panic]
দিয়ে অ্যানোটেটেড। আমরা যে ফেইলার পেয়েছি তার মানে হল টেস্ট ফাংশনের কোডটি প্যানিক ঘটায়নি।
should_panic
ব্যবহার করা টেস্টগুলো অসম্পূর্ণ হতে পারে। একটি should_panic
টেস্ট পাস করতে পারত এমনকি যদি টেস্টটি আমাদের প্রত্যাশিত কারণের থেকে ভিন্ন কোনো কারণে প্যানিক করত। should_panic
টেস্টগুলোকে আরও সুনির্দিষ্ট করতে, আমরা should_panic
অ্যাট্রিবিউটে একটি ঐচ্ছিক expected
প্যারামিটার যোগ করতে পারি। টেস্ট হারনেস নিশ্চিত করবে যে ফেইলার মেসেজটিতে প্রদত্ত টেক্সট রয়েছে। উদাহরণস্বরূপ, তালিকা ১১-৯-এ Guess
-এর পরিবর্তিত কোডটি বিবেচনা করুন যেখানে new
ফাংশনটি মানটি খুব ছোট বা খুব বড় হওয়ার উপর নির্ভর করে বিভিন্ন মেসেজ দিয়ে প্যানিক করে।
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
এই টেস্টটি পাস করবে কারণ should_panic
অ্যাট্রিবিউটের expected
প্যারামিটারে আমরা যে মানটি রেখেছি তা Guess::new
ফাংশন যে মেসেজ দিয়ে প্যানিক করে তার একটি সাবস্ট্রিং। আমরা প্রত্যাশিত সম্পূর্ণ প্যানিক মেসেজটিও নির্দিষ্ট করতে পারতাম, যা এক্ষেত্রে Guess value must be less than or equal to 100, got 200
হতো। আপনি কী নির্দিষ্ট করতে চান তা নির্ভর করে প্যানিক মেসেজের কতটা অংশ অনন্য বা ডাইনামিক এবং আপনি আপনার টেস্টকে কতটা সুনির্দিষ্ট করতে চান তার উপর। এক্ষেত্রে, প্যানিক মেসেজের একটি সাবস্ট্রিংই নিশ্চিত করার জন্য যথেষ্ট যে টেস্ট ফাংশনের কোডটি else if value > 100
কেসটি এক্সিকিউট করে।
একটি expected
মেসেজসহ should_panic
টেস্ট ফেইল করলে কী হয় তা দেখতে, আসুন if value < 1
এবং else if value > 100
ব্লকগুলোর বডি অদলবদল করে আমাদের কোডে আবার একটি বাগ প্রবেশ করাই:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
এবার যখন আমরা should_panic
টেস্টটি চালাই, তখন এটি ফেইল করবে:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got 200."`,
expected substring: `"less than or equal to 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
ফেইলার মেসেজটি নির্দেশ করে যে এই টেস্টটি আমাদের প্রত্যাশা অনুযায়ী প্যানিক করেছিল, কিন্তু প্যানিক মেসেজটিতে প্রত্যাশিত স্ট্রিং less than or equal to 100
অন্তর্ভুক্ত ছিল না। এক্ষেত্রে আমরা যে প্যানিক মেসেজটি পেয়েছি তা হল Guess value must be greater than or equal to 1, got 200.
এখন আমরা আমাদের বাগ কোথায় তা খুঁজে বের করা শুরু করতে পারি!
টেস্টে Result<T, E>
ব্যবহার করা
আমাদের এ পর্যন্ত সব টেস্ট ফেইল করলে প্যানিক করে। আমরা এমন টেস্টও লিখতে পারি যা Result<T, E>
ব্যবহার করে! এখানে তালিকা ১১-১ থেকে টেস্টটি Result<T, E>
ব্যবহার করে এবং প্যানিক করার পরিবর্তে একটি Err
রিটার্ন করার জন্য পুনরায় লেখা হয়েছে:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
it_works
ফাংশনটির এখন Result<(), String>
রিটার্ন টাইপ রয়েছে। ফাংশনের বডিতে, assert_eq!
ম্যাক্রো কল করার পরিবর্তে, আমরা টেস্ট পাস করলে Ok(())
এবং টেস্ট ফেইল করলে একটি String
সহ Err
রিটার্ন করি।
টেস্ট এমনভাবে লেখা যাতে তারা একটি Result<T, E>
রিটার্ন করে, তা আপনাকে টেস্টের বডিতে প্রশ্নবোধক চিহ্ন অপারেটর (?) ব্যবহার করতে সক্ষম করে, যা এমন টেস্ট লেখার জন্য একটি সুবিধাজনক উপায় হতে পারে যা তাদের মধ্যে কোনো অপারেশন Err
ভ্যারিয়েন্ট রিটার্ন করলে ফেইল করা উচিত।
Result<T, E>
ব্যবহার করা টেস্টে আপনি #[should_panic]
অ্যানোটেশন ব্যবহার করতে পারবেন না। কোনো অপারেশন একটি Err
ভ্যারিয়েন্ট রিটার্ন করে তা অ্যাসার্ট করতে, Result<T, E>
মানের উপর প্রশ্নবোধক চিহ্ন অপারেটর ব্যবহার করবেন না। পরিবর্তে, assert!(value.is_err())
ব্যবহার করুন।
এখন যেহেতু আপনি টেস্ট লেখার বিভিন্ন উপায় জানেন, আসুন দেখি আমাদের টেস্ট চালানোর সময় কী ঘটছে এবং cargo test
এর সাথে আমরা যে বিভিন্ন অপশন ব্যবহার করতে পারি তা অন্বেষণ করি।
টেস্ট কিভাবে চালানো হয় তা নিয়ন্ত্রণ করা
যেভাবে cargo run
আপনার কোড কম্পাইল করে এবং তার ফলে তৈরি হওয়া বাইনারি চালায়, ঠিক সেভাবেই cargo test
আপনার কোডকে টেস্ট মোডে (test mode) কম্পাইল করে এবং তার ফলে তৈরি হওয়া টেস্ট বাইনারি চালায়। cargo test
দ্বারা উৎপাদিত বাইনারির ডিফল্ট আচরণ হলো সমস্ত টেস্টকে প্যারালালি (in parallel) চালানো এবং টেস্ট চলাকালীন জেনারেট হওয়া আউটপুট ক্যাপচার করা। এটি আউটপুটকে প্রদর্শিত হতে বাধা দেয় এবং টেস্টের ফলাফলের সাথে সম্পর্কিত আউটপুট পড়া সহজ করে তোলে। তবে, আপনি এই ডিফল্ট আচরণ পরিবর্তন করার জন্য কমান্ড লাইন অপশন নির্দিষ্ট করতে পারেন।
কিছু কমান্ড লাইন অপশন cargo test
-এর জন্য এবং কিছু তার ফলে তৈরি হওয়া টেস্ট বাইনারির জন্য। এই দুই ধরনের আর্গুমেন্ট আলাদা করতে, আপনি প্রথমে cargo test
-এর আর্গুমেন্টগুলো তালিকাভুক্ত করুন, তারপর --
বিভাজক (separator) দিন এবং এরপর টেস্ট বাইনারির জন্য আর্গুমেন্টগুলো দিন। cargo test --help
চালালে আপনি cargo test
-এর সাথে ব্যবহারযোগ্য অপশনগুলো দেখতে পাবেন, এবং cargo test -- --help
চালালে আপনি বিভাজকের পরে ব্যবহারযোগ্য অপশনগুলো দেখতে পাবেন। সেই অপশনগুলো the rustc book-এর "Tests" section-এও নথিভুক্ত আছে।
টেস্ট প্যারালালি বা পরপর চালানো
যখন আপনি একাধিক টেস্ট চালান, ডিফল্টভাবে সেগুলো থ্রেড (thread) ব্যবহার করে প্যারালালি চলে, যার মানে হলো সেগুলো দ্রুত শেষ হয় এবং আপনি তাড়াতাড়ি ফিডব্যাক পান। যেহেতু টেস্টগুলো একই সময়ে চলছে, আপনাকে নিশ্চিত করতে হবে যে আপনার টেস্টগুলো একে অপরের উপর বা কোনো শেয়ার্ড স্টেট (shared state), যেমন বর্তমান ওয়ার্কিং ডিরেক্টরি বা এনভায়রনমেন্ট ভেরিয়েবলের উপর নির্ভরশীল নয়।
উদাহরণস্বরূপ, ধরুন আপনার প্রতিটি টেস্ট এমন কিছু কোড চালায় যা ডিস্কে test-output.txt নামে একটি ফাইল তৈরি করে এবং সেই ফাইলে কিছু ডেটা লেখে। তারপর প্রতিটি টেস্ট সেই ফাইলের ডেটা পড়ে এবং অ্যাসার্ট করে যে ফাইলটিতে একটি নির্দিষ্ট মান রয়েছে, যা প্রতিটি টেস্টে ভিন্ন। যেহেতু টেস্টগুলো একই সময়ে চলছে, একটি টেস্ট ফাইল লেখার এবং পড়ার মধ্যবর্তী সময়ে অন্য একটি টেস্ট ফাইলটি ওভাররাইট করে ফেলতে পারে। তখন দ্বিতীয় টেস্টটি ফেইল করবে, কোডটি ভুল হওয়ার কারণে নয়, বরং প্যারালালি চলার সময় টেস্টগুলো একে অপরের সাথে হস্তক্ষেপ করার কারণে। একটি সমাধান হলো নিশ্চিত করা যে প্রতিটি টেস্ট একটি ভিন্ন ফাইলে লেখে; আরেকটি সমাধান হলো টেস্টগুলো একবারে একটি করে চালানো।
আপনি যদি টেস্টগুলো প্যারালালি চালাতে না চান অথবা ব্যবহৃত থ্রেডের সংখ্যার উপর আরও সূক্ষ্ম নিয়ন্ত্রণ চান, তাহলে আপনি --test-threads
ফ্ল্যাগ এবং আপনি যে সংখ্যক থ্রেড ব্যবহার করতে চান তা টেস্ট বাইনারিতে পাঠাতে পারেন। নিচের উদাহরণটি দেখুন:
$ cargo test -- --test-threads=1
আমরা টেস্ট থ্রেডের সংখ্যা 1
সেট করেছি, যা প্রোগ্রামকে কোনো প্যারালালিসম ব্যবহার না করতে বলছে। একটি থ্রেড ব্যবহার করে টেস্ট চালালে প্যারালালি চালানোর চেয়ে বেশি সময় লাগবে, কিন্তু টেস্টগুলো যদি স্টেট শেয়ার করে তবে একে অপরের সাথে হস্তক্ষেপ করবে না।
ফাংশন আউটপুট দেখানো
ডিফল্টভাবে, যদি একটি টেস্ট পাস করে, Rust-এর টেস্ট লাইব্রেরি স্ট্যান্ডার্ড আউটপুটে প্রিন্ট করা যেকোনো কিছু ক্যাপচার করে। উদাহরণস্বরূপ, যদি আমরা একটি টেস্টে println!
কল করি এবং টেস্টটি পাস করে, আমরা টার্মিনালে println!
আউটপুট দেখতে পাব না; আমরা শুধুমাত্র সেই লাইনটি দেখব যা নির্দেশ করে যে টেস্টটি পাস করেছে। যদি একটি টেস্ট ফেইল করে, আমরা স্ট্যান্ডার্ড আউটপুটে প্রিন্ট করা সবকিছু ফেইলার মেসেজের বাকি অংশের সাথে দেখতে পাব।
উদাহরণস্বরূপ, তালিকা ১১-১০-এ একটি সাধারণ ফাংশন রয়েছে যা তার প্যারামিটারের মান প্রিন্ট করে এবং ১০ রিটার্ন করে, সাথে একটি পাস করা টেস্ট এবং একটি ফেইল করা টেস্ট রয়েছে।
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}```
</Listing>
যখন আমরা `cargo test` দিয়ে এই টেস্টগুলো চালাই, আমরা নিম্নলিখিত আউটপুট দেখতে পাব:
```console
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
লক্ষ্য করুন যে এই আউটপুটের কোথাও আমরা I got the value 4
দেখতে পাচ্ছি না, যা পাস করা টেস্টটি চলার সময় প্রিন্ট হয়। সেই আউটপুট ক্যাপচার করা হয়েছে। ফেইল করা টেস্টের আউটপুট, I got the value 8
, টেস্ট সারাংশ আউটপুটের সেই অংশে প্রদর্শিত হয়, যা টেস্ট ফেইলের কারণও দেখায়।
আমরা যদি পাস করা টেস্টগুলোর জন্যও প্রিন্ট করা মান দেখতে চাই, আমরা --show-output
দিয়ে Rust-কে সফল টেস্টের আউটপুটও দেখাতে বলতে পারি:
$ cargo test -- --show-output
যখন আমরা --show-output
ফ্ল্যাগ দিয়ে তালিকা ১১-১০-এর টেস্টগুলো আবার চালাই, আমরা নিম্নলিখিত আউটপুট দেখতে পাই:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
---- tests::this_test_will_pass stdout ----
I got the value 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
নাম অনুসারে টেস্টের একটি অংশ চালানো
কখনও কখনও, একটি সম্পূর্ণ টেস্ট স্যুট (test suite) চালাতে অনেক সময় লাগতে পারে। আপনি যদি একটি নির্দিষ্ট এলাকার কোডে কাজ করেন, তবে আপনি শুধুমাত্র সেই কোড সম্পর্কিত টেস্টগুলো চালাতে চাইতে পারেন। আপনি cargo test
-কে আর্গুমেন্ট হিসেবে যে টেস্ট বা টেস্টগুলোর নাম চালাতে চান তা পাস করে কোন টেস্টগুলো চালাবেন তা বেছে নিতে পারেন।
কিভাবে টেস্টের একটি অংশ চালাতে হয় তা দেখানোর জন্য, আমরা প্রথমে আমাদের add_two
ফাংশনের জন্য তিনটি টেস্ট তৈরি করব, যেমনটি তালিকা ১১-১১-এ দেখানো হয়েছে, এবং সেগুলোর মধ্যে কোনটি চালাব তা বেছে নেব।
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
যদি আমরা কোনো আর্গুমেন্ট পাস না করে টেস্টগুলো চালাই, যেমনটি আমরা আগে দেখেছি, সমস্ত টেস্ট প্যারালালি চলবে:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
একক টেস্ট চালানো
আমরা যেকোনো টেস্ট ফাংশনের নাম cargo test
-কে পাস করে শুধুমাত্র সেই টেস্টটি চালাতে পারি:
$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
শুধুমাত্র one_hundred
নামের টেস্টটি চলেছে; অন্য দুটি টেস্ট সেই নামের সাথে মেলেনি। টেস্ট আউটপুট আমাদের জানায় যে আমাদের আরও টেস্ট ছিল যা চলেনি, শেষে 2 filtered out
প্রদর্শন করে।
আমরা এইভাবে একাধিক টেস্টের নাম নির্দিষ্ট করতে পারি না; cargo test
-কে দেওয়া শুধুমাত্র প্রথম মানটি ব্যবহার করা হবে। কিন্তু একাধিক টেস্ট চালানোর একটি উপায় আছে।
একাধিক টেস্ট চালানোর জন্য ফিল্টারিং
আমরা একটি টেস্ট নামের অংশ নির্দিষ্ট করতে পারি, এবং যে কোনো টেস্টের নাম সেই মানের সাথে মিলবে তা চালানো হবে। উদাহরণস্বরূপ, যেহেতু আমাদের দুটি টেস্টের নামে add
রয়েছে, আমরা cargo test add
চালিয়ে সেই দুটি চালাতে পারি:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
এই কমান্ডটি add
নামে থাকা সমস্ত টেস্ট চালিয়েছে এবং one_hundred
নামের টেস্টটি ফিল্টার করে বাদ দিয়েছে। আরও লক্ষ্য করুন যে একটি টেস্ট যে মডিউলে উপস্থিত থাকে তা টেস্টের নামের অংশ হয়ে যায়, তাই আমরা মডিউলের নামের উপর ফিল্টার করে একটি মডিউলের সমস্ত টেস্ট চালাতে পারি।
নির্দিষ্টভাবে অনুরোধ না করা পর্যন্ত কিছু টেস্ট উপেক্ষা করা
কখনও কখনও কিছু নির্দিষ্ট টেস্ট চালাতে অনেক সময় লাগতে পারে, তাই আপনি cargo test
-এর বেশিরভাগ রানের সময় সেগুলোকে বাদ দিতে চাইতে পারেন। আপনি যে সমস্ত টেস্ট চালাতে চান সেগুলোকে আর্গুমেন্ট হিসেবে তালিকাভুক্ত করার পরিবর্তে, আপনি সময়সাপেক্ষ টেস্টগুলোকে ignore
অ্যাট্রিবিউট ব্যবহার করে অ্যানোটেট করে বাদ দিতে পারেন, যেমনটি এখানে দেখানো হয়েছে:
Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
#[test]
-এর পরে, আমরা যে টেস্টটি বাদ দিতে চাই তার জন্য #[ignore]
লাইনটি যোগ করি। এখন যখন আমরা আমাদের টেস্টগুলো চালাই, it_works
চলে, কিন্তু expensive_test
চলে না:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
expensive_test
ফাংশনটি ignored
হিসাবে তালিকাভুক্ত হয়েছে। যদি আমরা শুধুমাত্র উপেক্ষা করা টেস্টগুলো চালাতে চাই, আমরা cargo test -- --ignored
ব্যবহার করতে পারি:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
কোন টেস্টগুলো চলবে তা নিয়ন্ত্রণ করে, আপনি নিশ্চিত করতে পারেন যে আপনার cargo test
-এর ফলাফল দ্রুত ফিরে আসবে। যখন আপনি এমন একটি পর্যায়ে থাকবেন যেখানে ignored
টেস্টগুলোর ফলাফল পরীক্ষা করা অর্থপূর্ণ এবং আপনার কাছে ফলাফলের জন্য অপেক্ষা করার সময় আছে, তখন আপনি cargo test -- --ignored
চালাতে পারেন। আপনি যদি সমস্ত টেস্ট চালাতে চান, সেগুলি উপেক্ষা করা হোক বা না হোক, আপনি cargo test -- --include-ignored
চালাতে পারেন।
টেস্ট অর্গানাইজেশন
এই অধ্যায়ের শুরুতে যেমনটি উল্লেখ করা হয়েছে, টেস্টিং একটি জটিল বিষয় এবং বিভিন্ন মানুষ বিভিন্ন পরিভাষা ও সংগঠন ব্যবহার করে। Rust কমিউনিটি টেস্টগুলোকে প্রধানত দুটি ভাগে ভাগ করে: ইউনিট টেস্ট (unit tests) এবং ইন্টিগ্রেশন টেস্ট (integration tests)। ইউনিট টেস্ট ছোট এবং বেশি ফোকাসড হয়, যা একবারে একটি মডিউলকে আলাদাভাবে পরীক্ষা করে এবং প্রাইভেট ইন্টারফেসও (private interfaces) পরীক্ষা করতে পারে। ইন্টিগ্রেশন টেস্ট আপনার লাইব্রেরির সম্পূর্ণ বাইরে থাকে এবং আপনার কোডকে অন্য যেকোনো এক্সটার্নাল কোডের মতোই ব্যবহার করে, শুধুমাত্র পাবলিক ইন্টারফেস ব্যবহার করে এবং প্রতিটি টেস্টে একাধিক মডিউল পরীক্ষা করতে পারে।
আপনার লাইব্রেরির অংশগুলো আলাদাভাবে এবং একসঙ্গে প্রত্যাশা অনুযায়ী কাজ করছে কিনা তা নিশ্চিত করার জন্য উভয় প্রকারের টেস্ট লেখাই গুরুত্বপূর্ণ।
ইউনিট টেস্ট
ইউনিট টেস্টের উদ্দেশ্য হলো কোডের প্রতিটি ইউনিটকে বাকি কোড থেকে বিচ্ছিন্নভাবে পরীক্ষা করা, যাতে কোডের কোথায় প্রত্যাশা অনুযায়ী কাজ করছে এবং কোথায় করছে না তা দ্রুত চিহ্নিত করা যায়। আপনি ইউনিট টেস্টগুলোকে src ডিরেক্টরিতে প্রতিটি ফাইলের মধ্যে রাখবেন, যে কোডটি তারা পরীক্ষা করছে তার সাথে। প্রচলিত নিয়ম হলো, টেস্ট ফাংশনগুলো রাখার জন্য প্রতিটি ফাইলে tests
নামে একটি মডিউল তৈরি করা এবং মডিউলটিকে cfg(test)
দিয়ে অ্যানোটেট করা।
tests মডিউল এবং #[cfg(test)]
tests
মডিউলের উপর #[cfg(test)]
অ্যানোটেশনটি Rust-কে বলে যে শুধুমাত্র cargo test
চালানোর সময় টেস্ট কোড কম্পাইল এবং রান করতে হবে, cargo build
চালানোর সময় নয়। এটি কম্পাইলের সময় বাঁচায় যখন আপনি কেবল লাইব্রেরি তৈরি করতে চান এবং ফলে তৈরি হওয়া কম্পাইল্ড আর্টিফ্যাক্টে জায়গা বাঁচায় কারণ টেস্টগুলো অন্তর্ভুক্ত থাকে না। আপনি দেখবেন যে ইন্টিগ্রেশন টেস্টগুলো একটি ভিন্ন ডিরেক্টরিতে যাওয়ায় তাদের #[cfg(test)]
অ্যানোটেশনের প্রয়োজন হয় না। তবে, যেহেতু ইউনিট টেস্টগুলো কোডের সাথে একই ফাইলে থাকে, তাই কম্পাইল্ড ফলাফলে সেগুলো অন্তর্ভুক্ত করা উচিত নয় তা নির্দিষ্ট করতে আপনি #[cfg(test)]
ব্যবহার করবেন।
স্মরণ করুন, এই অধ্যায়ের প্রথম বিভাগে যখন আমরা নতুন adder
প্রজেক্ট তৈরি করেছিলাম, Cargo আমাদের জন্য এই কোডটি তৈরি করেছিল:
Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
স্বয়ংক্রিয়ভাবে জেনারেট হওয়া tests
মডিউলে, cfg
অ্যাট্রিবিউটটি configuration (কনফিগারেশন) বোঝায় এবং Rust-কে বলে যে নিম্নলিখিত আইটেমটি শুধুমাত্র একটি নির্দিষ্ট কনফিগারেশন অপশন দেওয়া থাকলেই অন্তর্ভুক্ত করা উচিত। এক্ষেত্রে, কনফিগারেশন অপশনটি হলো test
, যা Rust দ্বারা টেস্ট কম্পাইল এবং চালানোর জন্য সরবরাহ করা হয়। cfg
অ্যাট্রিবিউট ব্যবহার করে, Cargo আমাদের টেস্ট কোড শুধুমাত্র তখনই কম্পাইল করে যখন আমরা cargo test
দিয়ে সক্রিয়ভাবে টেস্ট চালাই। এর মধ্যে #[test]
দিয়ে অ্যানোটেট করা ফাংশনগুলো ছাড়াও এই মডিউলের মধ্যে থাকা যেকোনো সাহায্যকারী ফাংশন অন্তর্ভুক্ত থাকে।
প্রাইভেট ফাংশন টেস্ট করা
টেস্টিং কমিউনিটিতে প্রাইভেট ফাংশন সরাসরি পরীক্ষা করা উচিত কিনা তা নিয়ে বিতর্ক রয়েছে, এবং অন্যান্য ভাষা প্রাইভেট ফাংশন পরীক্ষা করা কঠিন বা অসম্ভব করে তোলে। আপনি যে টেস্টিং মতাদর্শই অনুসরণ করুন না কেন, Rust-এর প্রাইভেসি নিয়ম আপনাকে প্রাইভেট ফাংশন পরীক্ষা করার অনুমতি দেয়। তালিকা ১১-১২-এর কোডটি বিবেচনা করুন যেখানে internal_adder
নামে একটি প্রাইভেট ফাংশন রয়েছে।
pub fn add_two(a: u64) -> u64 {
internal_adder(a, 2)
}
fn internal_adder(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
লক্ষ্য করুন যে internal_adder
ফাংশনটি pub
হিসাবে চিহ্নিত নয়। টেস্টগুলো কেবল Rust কোড, এবং tests
মডিউলটি কেবল আরেকটি মডিউল। যেমনটি আমরা ["Paths for Referring to an Item in the Module Tree"][paths]-তে আলোচনা করেছি, চাইল্ড মডিউলের আইটেমগুলো তাদের পূর্বপুরুষ মডিউলের আইটেমগুলো ব্যবহার করতে পারে। এই টেস্টে, আমরা use super::*
দিয়ে tests
মডিউলের প্যারেন্টের সমস্ত আইটেমকে স্কোপে নিয়ে আসি এবং তারপর টেস্টটি internal_adder
কল করতে পারে। আপনি যদি মনে করেন যে প্রাইভেট ফাংশন পরীক্ষা করা উচিত নয়, তবে Rust-এ এমন কিছুই নেই যা আপনাকে তা করতে বাধ্য করবে।
ইন্টিগ্রেশন টেস্ট
Rust-এ, ইন্টিগ্রেশন টেস্টগুলো আপনার লাইব্রেরির সম্পূর্ণ বাইরে থাকে। তারা আপনার লাইব্রেরিটি অন্য যেকোনো কোডের মতোই ব্যবহার করে, যার মানে তারা কেবল সেই ফাংশনগুলোকেই কল করতে পারে যা আপনার লাইব্রেরির পাবলিক API-এর অংশ। তাদের উদ্দেশ্য হলো আপনার লাইব্রেরির অনেকগুলো অংশ একসাথে সঠিকভাবে কাজ করছে কিনা তা পরীক্ষা করা। যে কোডের ইউনিটগুলো একা একা সঠিকভাবে কাজ করে, সেগুলো ইন্টিগ্রেট করার সময় সমস্যা হতে পারে, তাই ইন্টিগ্রেটেড কোডের টেস্ট কভারেজও গুরুত্বপূর্ণ। ইন্টিগ্রেশন টেস্ট তৈরি করতে, আপনাকে প্রথমে একটি tests ডিরেক্টরি তৈরি করতে হবে।
tests ডিরেক্টরি
আমরা আমাদের প্রজেক্ট ডিরেক্টরির টপ লেভেলে, src এর পাশে একটি tests ডিরেক্টরি তৈরি করি। Cargo জানে যে এই ডিরেক্টরিতে ইন্টিগ্রেশন টেস্ট ফাইল খুঁজতে হবে। আমরা তখন যত খুশি টেস্ট ফাইল তৈরি করতে পারি, এবং Cargo প্রতিটি ফাইলকে একটি স্বতন্ত্র ক্রেট (crate) হিসাবে কম্পাইল করবে।
চলুন একটি ইন্টিগ্রেশন টেস্ট তৈরি করি। তালিকা ১১-১২-এর কোডটি এখনও src/lib.rs ফাইলে থাকা অবস্থায়, একটি tests ডিরেক্টরি তৈরি করুন এবং tests/integration_test.rs নামে একটি নতুন ফাইল তৈরি করুন। আপনার ডিরেক্টরি কাঠামোটি এমন হওয়া উচিত:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
তালিকা ১১-১৩-এর কোডটি tests/integration_test.rs ফাইলে প্রবেশ করান।
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
tests ডিরেক্টরির প্রতিটি ফাইল একটি পৃথক ক্রেট, তাই আমাদের প্রতিটি টেস্ট ক্রেটের স্কোপে আমাদের লাইব্রেরি আনতে হবে। এই কারণে আমরা কোডের শীর্ষে use adder;
যোগ করি, যা ইউনিট টেস্টে আমাদের প্রয়োজন হয়নি।
আমাদের tests/integration_test.rs-এর কোনো কোডকে #[cfg(test)]
দিয়ে অ্যানোটেট করার প্রয়োজন নেই। Cargo tests ডিরেক্টরিকে বিশেষভাবে বিবেচনা করে এবং এই ডিরেক্টরির ফাইলগুলো শুধুমাত্র cargo test
চালানোর সময় কম্পাইল করে। এখন cargo test
চালান:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
আউটপুটের তিনটি অংশে ইউনিট টেস্ট, ইন্টিগ্রেশন টেস্ট এবং ডক টেস্ট অন্তর্ভুক্ত রয়েছে। উল্লেখ্য, যদি কোনো একটি বিভাগের কোনো টেস্ট ফেইল করে, তাহলে পরবর্তী বিভাগগুলো চালানো হবে না। উদাহরণস্বরূপ, যদি একটি ইউনিট টেস্ট ফেইল করে, তাহলে ইন্টিগ্রেশন এবং ডক টেস্টের জন্য কোনো আউটপুট থাকবে না কারণ সেই টেস্টগুলো শুধুমাত্র তখনই চালানো হবে যদি সমস্ত ইউনিট টেস্ট পাস করে।
ইউনিট টেস্টের জন্য প্রথম বিভাগটি আমরা যা দেখে আসছি তার মতোই: প্রতিটি ইউনিট টেস্টের জন্য একটি লাইন (একটি internal
নামের যা আমরা তালিকা ১১-১২-এ যোগ করেছি) এবং তারপর ইউনিট টেস্টের জন্য একটি সারাংশ লাইন।
ইন্টিগ্রেশন টেস্ট বিভাগটি Running tests/integration_test.rs
লাইন দিয়ে শুরু হয়। এরপর, সেই ইন্টিগ্রেশন টেস্টের প্রতিটি টেস্ট ফাংশনের জন্য একটি লাইন এবং Doc-tests adder
বিভাগ শুরু হওয়ার ঠিক আগে ইন্টিগ্রেশন টেস্টের ফলাফলের জন্য একটি সারাংশ লাইন রয়েছে।
প্রতিটি ইন্টিগ্রেশন টেস্ট ফাইলের নিজস্ব বিভাগ রয়েছে, তাই আমরা যদি tests ডিরেক্টরিতে আরও ফাইল যোগ করি, তাহলে আরও ইন্টিগ্রেশন টেস্ট বিভাগ থাকবে।
আমরা এখনও cargo test
-এর আর্গুমেন্ট হিসাবে টেস্ট ফাংশনের নাম উল্লেখ করে একটি নির্দিষ্ট ইন্টিগ্রেশন টেস্ট ফাংশন চালাতে পারি। একটি নির্দিষ্ট ইন্টিগ্রেশন টেস্ট ফাইলের সমস্ত টেস্ট চালানোর জন্য, cargo test
-এর --test
আর্গুমেন্ট এবং তারপরে ফাইলের নাম ব্যবহার করুন:
$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
এই কমান্ডটি শুধুমাত্র tests/integration_test.rs ফাইলের টেস্টগুলো চালায়।
ইন্টিগ্রেশন টেস্টে সাবমডিউল
আপনি যখন আরও ইন্টিগ্রেশন টেস্ট যোগ করবেন, তখন সেগুলোকে সংগঠিত করতে সাহায্য করার জন্য আপনি tests ডিরেক্টরিতে আরও ফাইল তৈরি করতে চাইতে পারেন; উদাহরণস্বরূপ, আপনি যে কার্যকারিতা পরীক্ষা করছেন তার উপর ভিত্তি করে টেস্ট ফাংশনগুলোকে গ্রুপ করতে পারেন। আগে যেমন উল্লেখ করা হয়েছে, tests ডিরেক্টরির প্রতিটি ফাইল তার নিজস্ব পৃথক ক্রেট হিসাবে কম্পাইল করা হয়, যা পৃথক স্কোপ তৈরি করার জন্য দরকারী যাতে শেষ ব্যবহারকারীরা আপনার ক্রেট কীভাবে ব্যবহার করবে তার আরও কাছাকাছি অনুকরণ করা যায়। যাইহোক, এর মানে হল tests ডিরেক্টরির ফাইলগুলো src-এর ফাইলগুলোর মতো একই আচরণ শেয়ার করে না, যেমনটি আপনি অধ্যায় ৭-এ শিখেছিলেন কিভাবে কোডকে মডিউল এবং ফাইলে বিভক্ত করতে হয়।
tests ডিরেক্টরির ফাইলগুলোর ভিন্ন আচরণ সবচেয়ে বেশি লক্ষণীয় হয় যখন আপনার কাছে একাধিক ইন্টিগ্রেশন টেস্ট ফাইলে ব্যবহার করার জন্য একসেট সাহায্যকারী ফাংশন থাকে এবং আপনি সেগুলোকে একটি সাধারণ মডিউলে বের করে আনার জন্য অধ্যায় ৭-এর ["Separating Modules into Different Files"][separating-modules-into-files] বিভাগের ধাপগুলো অনুসরণ করার চেষ্টা করেন। উদাহরণস্বরূপ, যদি আমরা tests/common.rs তৈরি করি এবং এতে setup
নামে একটি ফাংশন রাখি, আমরা setup
-এ কিছু কোড যোগ করতে পারি যা আমরা একাধিক টেস্ট ফাইলের একাধিক টেস্ট ফাংশন থেকে কল করতে চাই:
Filename: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
যখন আমরা আবার টেস্টগুলো চালাই, আমরা টেস্ট আউটপুটে common.rs ফাইলের জন্য একটি নতুন বিভাগ দেখতে পাব, যদিও এই ফাইলে কোনো টেস্ট ফাংশন নেই এবং আমরা setup
ফাংশনটি কোথাও থেকে কলও করিনি:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
টেস্ট ফলাফলে common
এর জন্য running 0 tests
প্রদর্শিত হওয়াটা আমরা চাইনি। আমরা কেবল অন্য ইন্টিগ্রেশন টেস্ট ফাইলগুলোর সাথে কিছু কোড শেয়ার করতে চেয়েছিলাম। টেস্ট আউটপুটে common
আসা এড়াতে, tests/common.rs তৈরি করার পরিবর্তে, আমরা tests/common/mod.rs তৈরি করব। প্রজেক্ট ডিরেক্টরি এখন এমন দেখাচ্ছে:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
এটি পুরনো নামকরণের নিয়ম যা Rust বোঝে এবং যা আমরা অধ্যায় ৭-এর ["Alternate File Paths"][alt-paths]-এ উল্লেখ করেছি। ফাইলটিকে এইভাবে নামকরণ করা Rust-কে বলে যে common
মডিউলটিকে একটি ইন্টিগ্রেশন টেস্ট ফাইল হিসাবে বিবেচনা না করতে। যখন আমরা setup
ফাংশনের কোডটি tests/common/mod.rs-এ নিয়ে যাই এবং tests/common.rs ফাইলটি মুছে ফেলি, তখন টেস্ট আউটপুটের বিভাগটি আর প্রদর্শিত হবে না। tests ডিরেক্টরির সাবডিরেক্টরির ফাইলগুলো পৃথক ক্রেট হিসাবে কম্পাইল হয় না বা টেস্ট আউটপুটে তাদের বিভাগ থাকে না।
আমরা tests/common/mod.rs তৈরি করার পরে, আমরা এটিকে যেকোনো ইন্টিগ্রেশন টেস্ট ফাইল থেকে একটি মডিউল হিসাবে ব্যবহার করতে পারি। এখানে tests/integration_test.rs-এর it_adds_two
টেস্ট থেকে setup
ফাংশন কল করার একটি উদাহরণ দেওয়া হলো:
Filename: tests/integration_test.rs
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
let result = add_two(2);
assert_eq!(result, 4);
}
লক্ষ্য করুন যে mod common;
ডিক্লারেশনটি আমরা তালিকা ৭-২১-এ দেখানো মডিউল ডিক্লারেশনের মতোই। তারপর, টেস্ট ফাংশনে, আমরা common::setup()
ফাংশন কল করতে পারি।
বাইনারি ক্রেটের জন্য ইন্টিগ্রেশন টেস্ট
যদি আমাদের প্রজেক্টটি একটি বাইনারি ক্রেট হয় যাতে শুধুমাত্র একটি src/main.rs ফাইল থাকে এবং কোনো src/lib.rs ফাইল না থাকে, আমরা tests ডিরেক্টরিতে ইন্টিগ্রেশন টেস্ট তৈরি করতে এবং src/main.rs ফাইলে সংজ্ঞায়িত ফাংশনগুলোকে একটি use
স্টেটমেন্ট দিয়ে স্কোপে আনতে পারি না। শুধুমাত্র লাইব্রেরি ক্রেটগুলো ফাংশন এক্সপোজ করে যা অন্যান্য ক্রেট ব্যবহার করতে পারে; বাইনারি ক্রেটগুলো নিজে থেকে চালানোর জন্য তৈরি।
এটি একটি কারণ যে কারণে Rust প্রজেক্ট যেগুলো একটি বাইনারি সরবরাহ করে, সেগুলোতে একটি সহজবোধ্য src/main.rs ফাইল থাকে যা src/lib.rs ফাইলে থাকা লজিককে কল করে। সেই কাঠামো ব্যবহার করে, ইন্টিগ্রেশন টেস্টগুলো use
দিয়ে লাইব্রেরি ক্রেট পরীক্ষা করতে পারে যাতে গুরুত্বপূর্ণ কার্যকারিতা উপলব্ধ করা যায়। যদি গুরুত্বপূর্ণ কার্যকারিতা কাজ করে, তাহলে src/main.rs ফাইলের অল্প পরিমাণ কোডও কাজ করবে, এবং সেই অল্প পরিমাণ কোড পরীক্ষা করার প্রয়োজন নেই।
সারসংক্ষেপ
Rust-এর টেস্টিং ফিচারগুলো কোড কীভাবে কাজ করা উচিত তা নির্দিষ্ট করার একটি উপায় সরবরাহ করে যাতে আপনি পরিবর্তন করার পরেও এটি আপনার প্রত্যাশা অনুযায়ী কাজ করে তা নিশ্চিত করা যায়। ইউনিট টেস্টগুলো একটি লাইব্রেরির বিভিন্ন অংশকে আলাদাভাবে পরীক্ষা করে এবং প্রাইভেট ইমপ্লিমেন্টেশন ডিটেইলস পরীক্ষা করতে পারে। ইন্টিগ্রেশন টেস্টগুলো পরীক্ষা করে যে লাইব্রেরির অনেকগুলো অংশ একসাথে সঠিকভাবে কাজ করছে কিনা, এবং তারা লাইব্রেরির পাবলিক API ব্যবহার করে কোডটি সেভাবেই পরীক্ষা করে যেভাবে এক্সটার্নাল কোড এটি ব্যবহার করবে। যদিও Rust-এর টাইপ সিস্টেম এবং ওনারশিপ নিয়ম কিছু ধরণের বাগ প্রতিরোধ করতে সাহায্য করে, আপনার কোড কীভাবে আচরণ করবে বলে আশা করা হচ্ছে সে সম্পর্কিত লজিক বাগ কমাতে টেস্টগুলো এখনও গুরুত্বপূর্ণ।
চলুন এই অধ্যায়ে এবং পূর্ববর্তী অধ্যায়গুলোতে শেখা জ্ঞান একত্রিত করে একটি প্রজেক্টে কাজ করা যাক
একটি I/O প্রজেক্ট: একটি কমান্ড লাইন প্রোগ্রাম তৈরি
এই অধ্যায়ে, আমরা এখন পর্যন্ত শেখা বিভিন্ন দক্ষতার পুনরালোচনা করব এবং আরও কিছু standard library-র ফিচার নিয়ে আলোচনা করব। আমরা একটি কমান্ড লাইন টুল তৈরি করব যা ফাইল এবং কমান্ড লাইন input/output-এর সাথে কাজ করে, যার মাধ্যমে আমরা এ পর্যন্ত শেখা Rust-এর কিছু ধারণা অনুশীলন করতে পারব।
Rust-এর স্পিড, সেফটি, single binary আউটপুট এবং ক্রস-প্ল্যাটফর্ম সাপোর্ট এটিকে কমান্ড লাইন টুল তৈরির জন্য একটি আদর্শ ল্যাঙ্গুয়েজ করে তুলেছে। তাই, আমাদের প্রজেক্টের জন্য আমরা ক্লাসিক কমান্ড লাইন সার্চ টুল grep
(globally search a regular expression and print)-এর নিজস্ব একটি সংস্করণ তৈরি করব। সবচেয়ে সহজ ক্ষেত্রে, grep
একটি নির্দিষ্ট ফাইলে একটি নির্দিষ্ট স্ট্রিং খুঁজে বের করে। এটি করার জন্য, grep
আর্গুমেন্ট হিসেবে একটি ফাইলের পাথ এবং একটি স্ট্রিং গ্রহণ করে। এরপর এটি ফাইলটি পড়ে, ফাইলের যে লাইনগুলোতে স্ট্রিং আর্গুমেন্টটি রয়েছে সেগুলো খুঁজে বের করে এবং সেই লাইনগুলো প্রিন্ট করে।
এই প্রজেক্টটি করার সময় আমরা দেখব কীভাবে আমাদের কমান্ড লাইন টুলটিতে অন্যান্য কমান্ড লাইন টুলের মতো টার্মিনালের ফিচারগুলো ব্যবহার করা যায়। আমরা একটি environment variable-এর ভ্যালু রিড করব, যাতে ইউজার আমাদের টুলের আচরণ কনফিগার করতে পারেন। আমরা error message-গুলো standard output (stdout
)-এর পরিবর্তে standard error console stream (stderr
)-এ প্রিন্ট করব। এর ফলে, উদাহরণস্বরূপ, ইউজার সফল আউটপুটকে একটি ফাইলে রিডাইরেক্ট করতে পারবেন এবং একই সাথে স্ক্রিনে error message-গুলোও দেখতে পাবেন।
Rust কমিউনিটির একজন সদস্য, Andrew Gallant, ইতোমধ্যেই grep
-এর একটি সম্পূর্ণ ফিচার সমৃদ্ধ এবং অত্যন্ত দ্রুতগতির সংস্করণ তৈরি করেছেন, যার নাম ripgrep
। তুলনামূলকভাবে, আমাদের সংস্করণটি বেশ সহজ-সরল হবে, কিন্তু এই অধ্যায়টি আপনাকে ripgrep
-এর মতো একটি বাস্তব প্রজেক্ট বোঝার জন্য প্রয়োজনীয় কিছু প্রাথমিক ধারণা দেবে।
আমাদের grep
প্রজেক্টটি আপনার শেখা বেশ কয়েকটি ধারণাকে একত্রিত করবে:
- কোড অর্গানাইজ করা (অধ্যায় ৭)
- ভেক্টর এবং স্ট্রিং ব্যবহার করা (অধ্যায় ৮)
- এরর হ্যান্ডলিং (অধ্যায় ৯)
- প্রয়োজনীয় ক্ষেত্রে ট্রেইট এবং লাইফটাইম ব্যবহার করা (অধ্যায় ১০)
- টেস্ট লেখা (অধ্যায় ১১)
এছাড়াও আমরা সংক্ষিপ্তভাবে ক্লোজার (closures), ইটারেটর (iterators) এবং ট্রেইট অবজেক্ট (trait objects) সম্পর্কে জানব, যেগুলো নিয়ে অধ্যায় ১৩ এবং অধ্যায় ১৮-তে বিস্তারিত আলোচনা করা হবে।
কমান্ড লাইন আর্গুমেন্ট গ্রহণ করা (Accepting Command Line Arguments)
চলুন, বরাবরের মতো cargo new
ব্যবহার করে একটি নতুন প্রজেক্ট তৈরি করি। আমরা আমাদের প্রজেক্টের নাম দেব minigrep
, যাতে আপনার সিস্টেমে থাকা grep
টুল থেকে এটিকে আলাদা করা যায়।
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
আমাদের প্রথম কাজ হলো minigrep
-কে তার দুটি কমান্ড লাইন আর্গুমেন্ট গ্রহণ করতে সক্ষম করা: ফাইলের পাথ এবং যে স্ট্রিংটি সার্চ করা হবে সেটি। অর্থাৎ, আমরা আমাদের প্রোগ্রামটি cargo run
দিয়ে চালাতে চাই, এরপর দুটি হাইফেন দিয়ে বোঝানো হবে যে পরের আর্গুমেন্টগুলো cargo
-র জন্য নয় বরং আমাদের প্রোগ্রামের জন্য, তারপর সার্চ করার জন্য একটি স্ট্রিং এবং সার্চ করার জন্য একটি ফাইলের পাথ থাকবে, যেমন:
$ cargo run -- searchstring example-filename.txt
এই মুহূর্তে, cargo new
দ্বারা তৈরি প্রোগ্রামটি আমাদের দেওয়া আর্গুমেন্টগুলো প্রসেস করতে পারে না। crates.io-তে কিছু লাইব্রেরি রয়েছে যা কমান্ড লাইন আর্গুমেন্ট গ্রহণ করে এমন প্রোগ্রাম লিখতে সাহায্য করতে পারে, কিন্তু যেহেতু আমরা এই ধারণাটি কেবল শিখছি, তাই আমরা এই ক্ষমতাটি নিজেরাই তৈরি করব।
আর্গুমেন্টের ভ্যালুগুলো পড়া (Reading the Argument Values)
minigrep
যাতে আমরা পাস করা কমান্ড লাইন আর্গুমেন্টের ভ্যালুগুলো পড়তে পারে, তার জন্য আমাদের Rust-এর standard library-তে থাকা std::env::args
ফাংশনটি ব্যবহার করতে হবে। এই ফাংশনটি minigrep
-এ পাস করা কমান্ড লাইন আর্গুমেন্টগুলোর একটি iterator রিটার্ন করে। আমরা অধ্যায় ১৩-তে iterator নিয়ে বিস্তারিত আলোচনা করব। আপাতত, iterator সম্পর্কে আপনার কেবল দুটি বিষয় জানলেই চলবে: iterator একটির পর একটি ভ্যালু তৈরি করে, এবং আমরা একটি iterator-এর উপর collect
মেথড কল করে এটিকে একটি কালেকশন, যেমন vector-এ পরিণত করতে পারি, যেখানে iterator দ্বারা তৈরি সমস্ত এলিমেন্ট থাকবে।
লিস্টিং ১২-১ এর কোডটি আপনার minigrep
প্রোগ্রামকে পাস করা যেকোনো কমান্ড লাইন আর্গুমেন্ট পড়তে এবং তারপর সেই ভ্যালুগুলোকে একটি vector-এ সংগ্রহ করতে সাহায্য করবে।
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
প্রথমে আমরা use
স্টেটমেন্ট ব্যবহার করে std::env
মডিউলটিকে স্কোপে নিয়ে আসি যাতে আমরা এর args
ফাংশনটি ব্যবহার করতে পারি। লক্ষ্য করুন যে std::env::args
ফাংশনটি দুটি মডিউল লেভেলে নেস্টেড আছে। যেমনটি আমরা অধ্যায় ৭-এ আলোচনা করেছি, যেখানে কাঙ্ক্ষিত ফাংশনটি একাধিক মডিউলের মধ্যে নেস্টেড থাকে, সেখানে আমরা ফাংশনের পরিবর্তে প্যারেন্ট মডিউলটিকে স্কোপে নিয়ে এসেছি। এর মাধ্যমে, আমরা সহজেই std::env
থেকে অন্যান্য ফাংশন ব্যবহার করতে পারি। এটি use std::env::args
যোগ করে শুধুমাত্র args
দিয়ে ফাংশন কল করার চেয়ে কম দ্ব্যর্থক, কারণ args
-কে সহজেই বর্তমান মডিউলে ডিফাইন করা কোনো ফাংশন বলে ভুল হতে পারে।
args
ফাংশন এবং অবৈধ ইউনিকোডমনে রাখবেন যে
std::env::args
প্যানিক করবে যদি কোনো আর্গুমেন্টে অবৈধ ইউনিকোড (Unicode) থাকে। আপনার প্রোগ্রামে যদি অবৈধ ইউনিকোডযুক্ত আর্গুমেন্ট গ্রহণ করার প্রয়োজন হয়, তাহলে এর পরিবর্তেstd::env::args_os
ব্যবহার করুন। সেই ফাংশনটি একটি iterator রিটার্ন করে যাString
ভ্যালুর পরিবর্তেOsString
ভ্যালু তৈরি করে। আমরা এখানে সরলতার জন্যstd::env::args
ব্যবহার করেছি কারণOsString
ভ্যালুগুলো প্ল্যাটফর্ম ভেদে ভিন্ন হয় এবংString
ভ্যালুর চেয়ে কাজ করা বেশি জটিল।
main
-এর প্রথম লাইনে, আমরা env::args
কল করি এবং তাৎক্ষণিকভাবে collect
ব্যবহার করে iterator-টিকে একটি vector-এ পরিণত করি, যেখানে iterator দ্বারা তৈরি সমস্ত ভ্যালু থাকবে। আমরা collect
ফাংশনটি বিভিন্ন ধরণের কালেকশন তৈরি করতে ব্যবহার করতে পারি, তাই আমরা args
-এর টাইপ স্পষ্টভাবে Vec<String>
উল্লেখ করে দিই যে আমরা স্ট্রিং-এর একটি vector চাই। যদিও Rust-এ খুব কমই টাইপ উল্লেখ করার প্রয়োজন হয়, collect
এমন একটি ফাংশন যার জন্য প্রায়শই টাইপ উল্লেখ করতে হয়, কারণ Rust অনুমান করতে পারে না যে আপনি কোন ধরণের কালেকশন চান।
সবশেষে, আমরা ডিবাগ ম্যাক্রো ব্যবহার করে vector-টি প্রিন্ট করি। চলুন কোডটি প্রথমে কোনো আর্গুমেন্ট ছাড়া এবং তারপর দুটি আর্গুমেন্ট দিয়ে চালিয়ে দেখি:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]```
```console
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
লক্ষ্য করুন যে ভেক্টরের প্রথম ভ্যালুটি হলো "target/debug/minigrep"
, যা আমাদের বাইনারির নাম। এটি C-তে আর্গুমেন্ট লিস্টের আচরণের সাথে মিলে যায়, যা প্রোগ্রামগুলোকে তাদের এক্সিকিউশনের সময় ব্যবহৃত নামটি ব্যবহার করতে দেয়। প্রোগ্রামের নামটি জানা প্রায়শই সুবিধাজনক, যদি আপনি মেসেজে এটি প্রিন্ট করতে চান বা প্রোগ্রামটি চালু করার জন্য কোন কমান্ড লাইন অ্যালিয়াস ব্যবহার করা হয়েছে তার উপর ভিত্তি করে প্রোগ্রামের আচরণ পরিবর্তন করতে চান। কিন্তু এই অধ্যায়ের জন্য, আমরা এটিকে উপেক্ষা করব এবং শুধুমাত্র আমাদের প্রয়োজনীয় দুটি আর্গুমেন্ট সেভ করব।
আর্গুমেন্টের ভ্যালুগুলো ভেরিয়েবলে সেভ করা (Saving the Argument Values in Variables)
প্রোগ্রামটি বর্তমানে কমান্ড লাইন আর্গুমেন্ট হিসেবে নির্দিষ্ট করা ভ্যালুগুলো অ্যাক্সেস করতে সক্ষম। এখন আমাদের দুটি আর্গুমেন্টের ভ্যালু ভেরিয়েবলে সেভ করতে হবে যাতে আমরা প্রোগ্রামের বাকি অংশে এই ভ্যালুগুলো ব্যবহার করতে পারি। এটি আমরা লিস্টিং ১২-২-এ করছি।
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
যেমনটি আমরা vector প্রিন্ট করার সময় দেখেছি, প্রোগ্রামের নামটি args[0]
-তে ভেক্টরের প্রথম স্থানটি নেয়, তাই আমরা ইনডেক্স ১ থেকে আর্গুমেন্ট শুরু করছি। minigrep
যে প্রথম আর্গুমেন্টটি নেয় তা হল আমরা যে স্ট্রিংটি খুঁজছি, তাই আমরা প্রথম আর্গুমেন্টের একটি রেফারেন্স query
ভেরিয়েবলে রাখি। দ্বিতীয় আর্গুমেন্টটি হবে ফাইল পাথ, তাই আমরা দ্বিতীয় আর্গুমেন্টের একটি রেফারেন্স file_path
ভেরিয়েবলে রাখি।
কোডটি আমাদের উদ্দেশ্য অনুযায়ী কাজ করছে কিনা তা প্রমাণ করার জন্য আমরা সাময়িকভাবে এই ভেরিয়েবলগুলোর ভ্যালু প্রিন্ট করি। চলুন এই প্রোগ্রামটি আবার test
এবং sample.txt
আর্গুমেন্ট দিয়ে চালাই:
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
খুব ভালো, প্রোগ্রামটি কাজ করছে! আমাদের প্রয়োজনীয় আর্গুমেন্টগুলোর ভ্যালু সঠিক ভেরিয়েবলে সেভ হচ্ছে। পরে আমরা কিছু সম্ভাব্য ত্রুটিপূর্ণ পরিস্থিতি, যেমন ব্যবহারকারী কোনো আর্গুমেন্ট না দিলে, সেগুলো মোকাবেলা করার জন্য কিছু এরর হ্যান্ডলিং যোগ করব; আপাতত, আমরা সেই পরিস্থিতি উপেক্ষা করে ফাইল রিডিং ক্ষমতা যোগ করার দিকে মনোযোগ দেব।
ফাইল পড়া (Reading a File)
এখন আমরা file_path
আর্গুমেন্টে নির্দিষ্ট করা ফাইলটি পড়ার জন্য ফাংশনালিটি যোগ করব। প্রথমে, এটি পরীক্ষা করার জন্য আমাদের একটি স্যাম্পল ফাইল দরকার: আমরা একাধিক লাইনে কিছু টেক্সট এবং কিছু পুনরাবৃত্তিমূলক শব্দসহ একটি ফাইল ব্যবহার করব। লিস্টিং ১২-৩-এ এমিলি ডিকিনসনের একটি কবিতা রয়েছে যা এই কাজের জন্য বেশ উপযুক্ত হবে! আপনার প্রজেক্টের রুট লেভেলে poem.txt নামে একটি ফাইল তৈরি করুন এবং "I’m Nobody! Who are you?" কবিতাটি লিখুন।
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!
টেক্সট প্রস্তুত হয়ে গেলে, src/main.rs ফাইলটি এডিট করুন এবং ফাইলটি পড়ার জন্য কোড যোগ করুন, যেমনটি লিস্টিং ১২-৪-এ দেখানো হয়েছে।
use std::env;
use std::fs;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
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}");
}
প্রথমে আমরা use
স্টেটমেন্টের মাধ্যমে standard library-র একটি প্রাসঙ্গিক অংশ নিয়ে আসি: ফাইল হ্যান্ডেল করার জন্য আমাদের std::fs
প্রয়োজন।
main
ফাংশনের নতুন স্টেটমেন্ট fs::read_to_string
file_path
আর্গুমেন্টটি গ্রহণ করে, সেই ফাইলটি খোলে এবং ফাইলের কন্টেন্ট সহ একটি std::io::Result<String>
টাইপের ভ্যালু রিটার্ন করে।
এর পরে, আমরা আবার একটি অস্থায়ী println!
স্টেটমেন্ট যোগ করেছি যা ফাইল পড়ার পরে contents
-এর ভ্যালু প্রিন্ট করে, যাতে আমরা পরীক্ষা করতে পারি যে প্রোগ্রামটি এখন পর্যন্ত ঠিকভাবে কাজ করছে কিনা।
চলুন এই কোডটি প্রথম কমান্ড লাইন আর্গুমেন্ট হিসেবে যেকোনো স্ট্রিং (কারণ আমরা এখনও সার্চিং অংশটি তৈরি করিনি) এবং দ্বিতীয় আর্গুমেন্ট হিসেবে poem.txt ফাইলটি দিয়ে চালাই:
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
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!
চমৎকার! কোডটি ফাইলের বিষয়বস্তু পড়েছে এবং তারপর প্রিন্ট করেছে। কিন্তু কোডটিতে কয়েকটি ত্রুটি রয়েছে। এই মুহূর্তে, main
ফাংশনের একাধিক দায়িত্ব রয়েছে: সাধারণত, প্রতিটি ফাংশন যদি কেবল একটি কাজের জন্য দায়ী থাকে তবে ফাংশনগুলো আরও পরিষ্কার এবং রক্ষণাবেক্ষণ করা সহজ হয়। অন্য সমস্যাটি হলো আমরা যেভাবে এরর হ্যান্ডলিং করতে পারতাম, সেভাবে করছি না। প্রোগ্রামটি এখনও ছোট, তাই এই ত্রুটিগুলো বড় কোনো সমস্যা নয়, কিন্তু প্রোগ্রাম বড় হওয়ার সাথে সাথে এগুলোকে পরিষ্কারভাবে ঠিক করা আরও কঠিন হয়ে উঠবে। একটি প্রোগ্রাম তৈরির সময় প্রথম দিকেই রিফ্যাক্টরিং (refactoring) শুরু করা একটি ভালো অভ্যাস, কারণ অল্প পরিমাণ কোড রিফ্যাক্টর করা অনেক সহজ। আমরা এর পরেই তা করব।
মডুলারিটি এবং এরর হ্যান্ডলিং উন্নত করার জন্য রিফ্যাক্টরিং (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-এ করা হবে।
চলুন এই নতুন মডুলারিটির সুবিধা নিয়ে এমন কিছু করি যা পুরোনো কোড দিয়ে করা কঠিন ছিল কিন্তু নতুন কোড দিয়ে সহজ: আমরা কিছু টেস্ট লিখব!
টেস্ট-ড্রিভেন ডেভেলপমেন্ট (TDD) ব্যবহার করে লাইব্রেরির ফাংশনালিটি তৈরি করা
যেহেতু এখন আমাদের সার্চ লজিকটি main
ফাংশন থেকে আলাদা করে src/lib.rs-এ রাখা হয়েছে, তাই আমাদের কোডের মূল ফাংশনালিটির জন্য টেস্ট লেখা অনেক সহজ হয়ে গেছে। আমরা বিভিন্ন আর্গুমেন্ট দিয়ে সরাসরি ফাংশন কল করতে পারি এবং কমান্ড লাইন থেকে আমাদের বাইনারি কল না করেই রিটার্ন ভ্যালু পরীক্ষা করতে পারি।
এই বিভাগে, আমরা টেস্ট-ড্রিভেন ডেভেলপমেন্ট (TDD) প্রক্রিয়া ব্যবহার করে minigrep
প্রোগ্রামে সার্চিং লজিক যোগ করব। এর জন্য আমরা নিম্নলিখিত ধাপগুলো অনুসরণ করব:
- একটি টেস্ট লিখুন যা ফেইল করবে এবং এটি চালিয়ে নিশ্চিত হন যে এটি আপনার প্রত্যাশিত কারণেই ফেইল করছে।
- নতুন টেস্টটি পাস করানোর জন্য শুধুমাত্র প্রয়োজনীয় কোড লিখুন বা পরিবর্তন করুন।
- আপনি এইমাত্র যে কোড যোগ বা পরিবর্তন করেছেন তা রিফ্যাক্টর করুন এবং নিশ্চিত করুন যে টেস্টগুলো পাস করছে।
- ধাপ ১ থেকে পুনরাবৃত্তি করুন!
যদিও সফটওয়্যার লেখার অনেক পদ্ধতির মধ্যে এটি একটি, TDD কোড ডিজাইনকে চালিত করতে সাহায্য করতে পারে। যে কোডটি টেস্ট পাস করাবে, তা লেখার আগে টেস্টটি লিখে ফেললে পুরো প্রক্রিয়া জুড়ে হাই টেস্ট কভারেজ বজায় রাখতে সাহায্য করে।
আমরা সেই ফাংশনালিটির ইমপ্লিমেন্টেশন টেস্ট-ড্রাইভ করব যা ফাইলের কন্টেন্টে কোয়েরি স্ট্রিং খুঁজবে এবং কোয়েরির সাথে মেলে এমন লাইনের একটি তালিকা তৈরি করবে। আমরা এই ফাংশনালিটি search
নামের একটি ফাংশনে যোগ করব।
একটি ফেইলিং টেস্ট লেখা (Writing a Failing Test)
src/lib.rs-এ, আমরা অধ্যায় ১১-এর মতো একটি tests
মডিউল এবং একটি টেস্ট ফাংশন যোগ করব। টেস্ট ফাংশনটি search
ফাংশনের প্রত্যাশিত আচরণ নির্দিষ্ট করবে: এটি একটি কোয়েরি এবং সার্চ করার জন্য টেক্সট নেবে এবং টেক্সট থেকে শুধুমাত্র সেই লাইনগুলোই রিটার্ন করবে যেগুলোতে কোয়েরিটি রয়েছে। লিস্টিং ১২-১৫ এই টেস্টটি দেখাচ্ছে।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
এই টেস্টটি "duct"
স্ট্রিংটি সার্চ করছে। আমরা যে টেক্সটটি সার্চ করছি তা তিন লাইনের, যার মধ্যে কেবল একটিতে "duct"
রয়েছে (লক্ষ্য করুন যে ওপেনিং ডাবল কোটের পরে ব্যাকস্ল্যাশ Rust-কে বলে যে এই স্ট্রিং লিটারেলের শুরুতে একটি নিউলাইন ক্যারেক্টার যোগ না করতে)। আমরা assert
করছি যে search
ফাংশন থেকে রিটার্ন করা ভ্যালুতে শুধুমাত্র আমাদের প্রত্যাশিত লাইনটি রয়েছে।
যদি আমরা এই টেস্টটি চালাই, এটি বর্তমানে ফেইল করবে কারণ unimplemented!
ম্যাক্রো "not implemented" মেসেজ দিয়ে প্যানিক করবে। TDD নীতি অনুসারে, আমরা একটি ছোট পদক্ষেপ নেব এবং টেস্টটি যাতে ফাংশন কল করার সময় প্যানিক না করে তার জন্য যথেষ্ট কোড যোগ করব। এর জন্য আমরা search
ফাংশনটিকে সর্বদা একটি খালি ভেক্টর রিটার্ন করার জন্য ডিফাইন করব, যেমনটি লিস্টিং ১২-১৬-তে দেখানো হয়েছে। তাহলে টেস্টটি কম্পাইল হবে এবং ফেইল করবে কারণ একটি খালি ভেক্টর "safe, fast, productive."
লাইনসহ একটি ভেক্টরের সাথে মিলবে না।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}```
</Listing>
এখন আসুন আলোচনা করি কেন আমাদের `search`-এর সিগনেচারে একটি সুস্পষ্ট লাইফটাইম `'a` ডিফাইন করতে হবে এবং সেই লাইফটাইমটি `contents` আর্গুমেন্ট এবং রিটার্ন ভ্যালুর সাথে ব্যবহার করতে হবে। [অধ্যায় ১০][ch10-lifetimes]<!-- ignore --> থেকে স্মরণ করুন যে লাইফটাইম প্যারামিটারগুলো নির্দিষ্ট করে যে কোন আর্গুমেন্টের লাইফটাইম রিটার্ন ভ্যালুর লাইফটাইমের সাথে সংযুক্ত। এই ক্ষেত্রে, আমরা নির্দেশ করছি যে রিটার্ন করা ভেক্টরে স্ট্রিং স্লাইস থাকবে যা `contents` আর্গুমেন্টের স্লাইসকে রেফারেন্স করে ( `query` আর্গুমেন্টকে নয়)।
অন্য কথায়, আমরা Rust-কে বলছি যে `search` ফাংশন দ্বারা রিটার্ন করা ডেটা তত সময় পর্যন্ত বেঁচে থাকবে, যত সময় `contents` আর্গুমেন্টে `search` ফাংশনে পাস করা ডেটা বেঁচে থাকবে। এটি গুরুত্বপূর্ণ! একটি স্লাইস দ্বারা রেফারেন্স করা ডেটা রেফারেন্সটি বৈধ হওয়ার জন্য অবশ্যই বৈধ হতে হবে; যদি কম্পাইলার ধরে নেয় যে আমরা `contents`-এর পরিবর্তে `query`-এর স্ট্রিং স্লাইস তৈরি করছি, তবে এটি তার সেফটি চেকিং ভুলভাবে করবে।
যদি আমরা লাইফটাইম অ্যানোটেশন ভুলে যাই এবং এই ফাংশনটি কম্পাইল করার চেষ্টা করি, আমরা এই এররটি পাব:
```console
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
1 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust জানতে পারে না যে আউটপুটের জন্য আমাদের দুটি প্যারামিটারের মধ্যে কোনটি প্রয়োজন, তাই আমাদের এটি স্পষ্টভাবে বলতে হবে। লক্ষ্য করুন যে সাহায্যকারী টেক্সটটি সমস্ত প্যারামিটার এবং আউটপুট টাইপের জন্য একই লাইফটাইম প্যারামিটার নির্দিষ্ট করার পরামর্শ দেয়, যা ভুল! যেহেতু contents
হলো সেই প্যারামিটার যেখানে আমাদের সমস্ত টেক্সট রয়েছে এবং আমরা সেই টেক্সটের যে অংশগুলো মেলে তা রিটার্ন করতে চাই, আমরা জানি যে শুধুমাত্র contents
প্যারামিটারটিই লাইফটাইম সিনট্যাক্স ব্যবহার করে রিটার্ন ভ্যালুর সাথে সংযুক্ত হওয়া উচিত।
অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজে আপনাকে সিগনেচারে আর্গুমেন্টগুলোকে রিটার্ন ভ্যালুর সাথে সংযুক্ত করতে হয় না, কিন্তু এই অনুশীলনটি সময়ের সাথে সাথে সহজ হয়ে যাবে। আপনি এই উদাহরণটি অধ্যায় ১০-এর "Validating References with Lifetimes" বিভাগের উদাহরণগুলোর সাথে তুলনা করতে পারেন।
টেস্ট পাস করার জন্য কোড লেখা (Writing Code to Pass the Test)
বর্তমানে, আমাদের টেস্টটি ফেইল করছে কারণ আমরা সবসময় একটি খালি ভেক্টর রিটার্ন করি। এটি ঠিক করতে এবং search
ইমপ্লিমেন্ট করতে, আমাদের প্রোগ্রামকে এই ধাপগুলো অনুসরণ করতে হবে:
- কন্টেন্টের প্রতিটি লাইনের মধ্যে দিয়ে ইটারেট (iterate) করা।
- লাইনটিতে আমাদের কোয়েরি স্ট্রিং আছে কিনা তা পরীক্ষা করা।
- যদি থাকে, তবে এটিকে আমরা যে ভ্যালুগুলো রিটার্ন করছি তার তালিকায় যুক্ত করা।
- যদি না থাকে, তবে কিছুই না করা।
- যে রেজাল্টগুলো ম্যাচ করে তার তালিকা রিটার্ন করা।
চলুন প্রতিটি ধাপ নিয়ে কাজ করা যাক, লাইন ইটারেট করা দিয়ে শুরু করি।
lines
মেথড দিয়ে লাইন বরাবর ইটারেট করা
Rust-এর একটি সহায়ক মেথড আছে যা স্ট্রিং-এর লাইন-বাই-লাইন ইটারেশন পরিচালনা করে, যার সুবিধাজনক নাম lines
, যা লিস্টিং ১২-১৭-তে দেখানো হয়েছে। মনে রাখবেন যে এটি এখনও কম্পাইল হবে না।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
lines
মেথড একটি iterator রিটার্ন করে। আমরা অধ্যায় ১৩-তে iterator নিয়ে গভীরভাবে আলোচনা করব, কিন্তু স্মরণ করুন যে আপনি লিস্টিং ৩-৫-এ iterator ব্যবহারের এই উপায়টি দেখেছেন, যেখানে আমরা একটি কালেকশনের প্রতিটি আইটেমের উপর কিছু কোড চালানোর জন্য একটি for
লুপের সাথে একটি iterator ব্যবহার করেছি।
কোয়েরির জন্য প্রতিটি লাইন সার্চ করা
এরপরে, আমরা পরীক্ষা করব যে বর্তমান লাইনে আমাদের কোয়েরি স্ট্রিং আছে কিনা। সৌভাগ্যবশত, স্ট্রিং-এর contains
নামে একটি সহায়ক মেথড আছে যা আমাদের জন্য এই কাজটি করে দেয়! search
ফাংশনে contains
মেথডের একটি কল যোগ করুন, যেমনটি লিস্টিং ১২-১৮-তে দেখানো হয়েছে। মনে রাখবেন যে এটি এখনও কম্পাইল হবে না।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
এই মুহূর্তে, আমরা ফাংশনালিটি তৈরি করছি। কোডটি কম্পাইল করার জন্য, আমাদের ফাংশন সিগনেচারে যেমনটি নির্দেশ করেছিলাম, তেমন একটি ভ্যালু বডি থেকে রিটার্ন করতে হবে।
ম্যাচ করা লাইনগুলো সংরক্ষণ করা
এই ফাংশনটি শেষ করার জন্য, আমাদের ম্যাচ করা লাইনগুলো সংরক্ষণ করার একটি উপায় দরকার যা আমরা রিটার্ন করতে চাই। এর জন্য, আমরা for
লুপের আগে একটি মিউটেবল ভেক্টর তৈরি করতে পারি এবং ভেক্টরে একটি line
সংরক্ষণ করতে push
মেথড কল করতে পারি। for
লুপের পরে, আমরা ভেক্টরটি রিটার্ন করি, যেমনটি লিস্টিং ১২-১৯-এ দেখানো হয়েছে।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
এখন search
ফাংশনটি শুধুমাত্র সেই লাইনগুলো রিটার্ন করবে যেগুলোতে query
রয়েছে এবং আমাদের টেস্ট পাস করা উচিত। চলুন টেস্টটি চালাই:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
আমাদের টেস্ট পাস হয়েছে, তাই আমরা জানি এটি কাজ করছে!
এই পর্যায়ে, আমরা টেস্টগুলো পাস করিয়ে রেখে একই ফাংশনালিটি বজায় রেখে সার্চ ফাংশনের ইমপ্লিমেন্টেশন রিফ্যাক্টর করার সুযোগ বিবেচনা করতে পারি। সার্চ ফাংশনের কোডটি খুব খারাপ নয়, তবে এটি iterator-এর কিছু দরকারী বৈশিষ্ট্যের সুবিধা নেয় না। আমরা অধ্যায় ১৩-তে এই উদাহরণে ফিরে আসব, যেখানে আমরা iterator বিস্তারিতভাবে অন্বেষণ করব এবং দেখব কীভাবে এটিকে উন্নত করা যায়।
এখন পুরো প্রোগ্রামটি কাজ করা উচিত! চলুন এটি চেষ্টা করে দেখি, প্রথমে এমন একটি শব্দ দিয়ে যা এমিলি ডিকিনসনের কবিতা থেকে ঠিক একটি লাইন রিটার্ন করবে: frog।
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
দারুণ! এখন আসুন এমন একটি শব্দ চেষ্টা করি যা একাধিক লাইনের সাথে মিলবে, যেমন body:
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
এবং অবশেষে, আসুন নিশ্চিত করি যে আমরা যখন এমন একটি শব্দ সার্চ করব যা কবিতায় কোথাও নেই, যেমন monomorphization, তখন আমরা কোনো লাইন পাব না:
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
চমৎকার! আমরা একটি ক্লাসিক টুলের নিজস্ব মিনি সংস্করণ তৈরি করেছি এবং অ্যাপ্লিকেশন কীভাবে গঠন করতে হয় সে সম্পর্কে অনেক কিছু শিখেছি। আমরা ফাইল ইনপুট এবং আউটপুট, লাইফটাইম, টেস্টিং এবং কমান্ড লাইন পার্সিং সম্পর্কেও কিছু শিখেছি।
এই প্রজেক্টটি শেষ করার জন্য, আমরা সংক্ষেপে দেখাব কীভাবে এনভায়রনমেন্ট ভেরিয়েবলের সাথে কাজ করতে হয় এবং কীভাবে স্ট্যান্ডার্ড এরর-এ প্রিন্ট করতে হয়, উভয়ই কমান্ড লাইন প্রোগ্রাম লেখার সময় দরকারী।
এনভায়রনমেন্ট ভেরিয়েবল (Environment Variables) নিয়ে কাজ করা
আমরা minigrep
বাইনারিটিকে একটি অতিরিক্ত ফিচার যোগ করে উন্নত করব: case-insensitive (ছোট বা বড় হাতের অক্ষর নির্বিশেষে) সার্চিংয়ের একটি অপশন, যা ব্যবহারকারী একটি এনভায়রনমেন্ট ভেরিয়েবলের মাধ্যমে চালু করতে পারবেন। আমরা এই ফিচারটিকে একটি কমান্ড লাইন অপশন হিসেবে তৈরি করতে পারতাম এবং ব্যবহারকারীদের প্রতিবার এটি ব্যবহার করার জন্য টাইপ করতে বলতে পারতাম। কিন্তু এর পরিবর্তে এটিকে একটি এনভায়রনমেন্ট ভেরিয়েবল হিসেবে তৈরি করার মাধ্যমে আমরা আমাদের ব্যবহারকারীদের একবার এনভায়রনমেন্ট ভেরিয়েবল সেট করার সুযোগ দিচ্ছি, এবং সেই টার্মিনাল সেশনে তাদের সমস্ত সার্চ case-insensitive হবে।
Case-Insensitive search
ফাংশনের জন্য একটি ফেইলিং টেস্ট লেখা
প্রথমে আমরা minigrep
লাইব্রেরিতে একটি নতুন search_case_insensitive
ফাংশন যোগ করব যা এনভায়রনমেন্ট ভেরিয়েবলের ভ্যালু থাকলে কল করা হবে। আমরা TDD প্রক্রিয়া অনুসরণ করা চালিয়ে যাব, তাই প্রথম ধাপটি হলো আবার একটি ফেইলিং টেস্ট লেখা। আমরা নতুন search_case_insensitive
ফাংশনের জন্য একটি নতুন টেস্ট যোগ করব এবং আমাদের পুরনো টেস্টের নাম one_result
থেকে case_sensitive
-এ পরিবর্তন করব যাতে দুটি টেস্টের মধ্যে পার্থক্য স্পষ্ট হয়, যেমনটি লিস্টিং ১২-২০-এ দেখানো হয়েছে।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
লক্ষ্য করুন যে আমরা পুরনো টেস্টের contents
-ও এডিট করেছি। আমরা একটি নতুন লাইন যোগ করেছি "Duct tape."
টেক্সট সহ, যেখানে একটি বড় হাতের D ব্যবহার করা হয়েছে, যা case-sensitive ভাবে সার্চ করার সময় "duct"
কোয়েরির সাথে ম্যাচ করা উচিত নয়। পুরনো টেস্টটি এভাবে পরিবর্তন করা নিশ্চিত করতে সাহায্য করে যে আমরা ইতোমধ্যে প্রয়োগ করা case-sensitive সার্চ ফাংশনালিটি দুর্ঘটনাক্রমে নষ্ট করে ফেলছি না। এই টেস্টটি এখন পাস করা উচিত এবং case-insensitive সার্চ নিয়ে কাজ করার সময়ও পাস করা উচিত।
case-insensitive সার্চের জন্য নতুন টেস্টটি তার কোয়েরি হিসেবে "rUsT"
ব্যবহার করে। আমরা যে search_case_insensitive
ফাংশনটি যোগ করতে চলেছি, সেখানে "rUsT"
কোয়েরিটি বড় হাতের R সহ "Rust:"
লাইনটির সাথে ম্যাচ করা উচিত এবং "Trust me."
লাইনটির সাথেও ম্যাচ করা উচিত, যদিও উভয়ের কেসিং কোয়েরি থেকে ভিন্ন। এটি আমাদের ফেইলিং টেস্ট, এবং এটি কম্পাইল করতে ব্যর্থ হবে কারণ আমরা এখনও search_case_insensitive
ফাংশনটি ডিফাইন করিনি। আপনি নির্দ্বিধায় একটি স্কেলেটন ইমপ্লিমেন্টেশন যোগ করতে পারেন যা সর্বদা একটি খালি ভেক্টর রিটার্ন করে, যেমনটি আমরা লিস্টিং ১২-১৬-তে search
ফাংশনের জন্য করেছিলাম, যাতে টেস্টটি কম্পাইল হয় এবং ফেইল করে।
search_case_insensitive
ফাংশন ইমপ্লিমেন্ট করা
search_case_insensitive
ফাংশনটি, যা লিস্টিং ১২-২১-এ দেখানো হয়েছে, প্রায় search
ফাংশনের মতোই হবে। একমাত্র পার্থক্য হলো আমরা query
এবং প্রতিটি line
-কে লোয়ারকেস (lowercase) করব, যাতে ইনপুট আর্গুমেন্টের কেস যাই হোক না কেন, লাইনটিতে কোয়েরি আছে কিনা তা পরীক্ষা করার সময় তারা একই কেস-এ থাকবে।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
প্রথমে আমরা query
স্ট্রিংকে লোয়ারকেস করি এবং এটিকে একই নামের একটি নতুন ভেরিয়েবলে সংরক্ষণ করি, যা মূল query
-কে শ্যাডো (shadow) করে। কোয়েরির উপর to_lowercase
কল করা প্রয়োজন যাতে ব্যবহারকারীর কোয়েরি "rust"
, "RUST"
, "Rust"
, বা "
rUsT"
যাই হোক না কেন, আমরা কোয়েরিটিকে "rust"
হিসেবে বিবেচনা করব এবং কেস-এর প্রতি সংবেদনশীল থাকব না। যদিও to_lowercase
বেসিক ইউনিকোড (Unicode) হ্যান্ডেল করবে, এটি ১০০ শতাংশ সঠিক হবে না। যদি আমরা একটি বাস্তব অ্যাপ্লিকেশন লিখতাম, তবে আমাদের এখানে আরও কিছু কাজ করতে হতো, কিন্তু এই বিভাগটি এনভায়রনমেন্ট ভেরিয়েবল সম্পর্কে, ইউনিকোড সম্পর্কে নয়, তাই আমরা এখানেই এটি ছেড়ে দেব।
লক্ষ্য করুন যে query
এখন একটি স্ট্রিং স্লাইসের পরিবর্তে একটি String
, কারণ to_lowercase
কল করা বিদ্যমান ডেটাকে রেফারেন্স করার পরিবর্তে নতুন ডেটা তৈরি করে। উদাহরণস্বরূপ, ধরুন কোয়েরিটি "rUsT"
: সেই স্ট্রিং স্লাইসে আমাদের ব্যবহারের জন্য ছোট হাতের u
বা t
নেই, তাই আমাদের "rust"
ধারণকারী একটি নতুন String
অ্যালোকেট করতে হবে। যখন আমরা এখন contains
মেথডের আর্গুমেন্ট হিসেবে query
পাস করব, আমাদের একটি অ্যামপারস্যান্ড (&) যোগ করতে হবে কারণ contains
-এর সিগনেচার একটি স্ট্রিং স্লাইস নেওয়ার জন্য ডিফাইন করা হয়েছে।
এরপরে, আমরা প্রতিটি line
-এ to_lowercase
-এ একটি কল যোগ করি সমস্ত ক্যারেক্টার লোয়ারকেস করতে। এখন যেহেতু আমরা line
এবং query
উভয়কেই লোয়ারকেসে রূপান্তর করেছি, কোয়েরির কেস যাই হোক না কেন আমরা ম্যাচ খুঁজে পাব।
দেখা যাক এই ইমপ্লিমেন্টেশনটি টেস্ট পাস করে কিনা:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
চমৎকার! তারা পাস করেছে। এখন, আসুন run
ফাংশন থেকে নতুন search_case_insensitive
ফাংশনটি কল করি। প্রথমে আমরা Config
স্ট্রাকটে case-sensitive এবং case-insensitive সার্চের মধ্যে সুইচ করার জন্য একটি কনফিগারেশন অপশন যোগ করব। এই ফিল্ডটি যোগ করলে কম্পাইলার এরর দেখা যাবে কারণ আমরা এখনও কোথাও এই ফিল্ডটি ইনিশিয়ালাইজ করছি না:
ফাইলের নাম: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --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);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
আমরা ignore_case
ফিল্ডটি যোগ করেছি যা একটি বুলিয়ান (boolean) ধারণ করে। এরপরে, আমাদের run
ফাংশনটিকে ignore_case
ফিল্ডের ভ্যালু পরীক্ষা করতে হবে এবং search
ফাংশন বা search_case_insensitive
ফাংশন কল করার সিদ্ধান্ত নিতে হবে, যেমনটি লিস্টিং ১২-২২-এ দেখানো হয়েছে। এটি এখনও কম্পাইল হবে না।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --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);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
অবশেষে, আমাদের এনভায়রনমেন্ট ভেরিয়েবল পরীক্ষা করতে হবে। এনভায়রনমেন্ট ভেরিয়েবলের সাথে কাজ করার ফাংশনগুলো standard library-র env
মডিউলে রয়েছে, যা ইতোমধ্যে src/main.rs-এর শীর্ষে স্কোপে রয়েছে। আমরা env
মডিউলের var
ফাংশনটি ব্যবহার করে দেখব IGNORE_CASE
নামের কোনো এনভায়রনমেন্ট ভেরিয়েবলের জন্য কোনো ভ্যালু সেট করা হয়েছে কিনা, যেমনটি লিস্টিং ১২-২৩-এ দেখানো হয়েছে।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
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);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
এখানে, আমরা একটি নতুন ভেরিয়েবল ignore_case
তৈরি করি। এর ভ্যালু সেট করতে, আমরা env::var
ফাংশন কল করি এবং এতে IGNORE_CASE
এনভায়রনমেন্ট ভেরিয়েবলের নাম পাস করি। env::var
ফাংশনটি একটি Result
রিটার্ন করে যা সফল Ok
ভ্যারিয়েন্ট হবে এবং এনভায়রনমেন্ট ভেরিয়েবলের ভ্যালু ধারণ করবে যদি এনভায়রনমেন্ট ভেরিয়েবলটি কোনো ভ্যালুতে সেট করা থাকে। যদি এনভায়রনমেন্ট ভেরিয়েবলটি সেট না করা থাকে তবে এটি Err
ভ্যারিয়েন্ট রিটার্ন করবে।
এনভায়রনমেন্ট ভেরিয়েবলটি সেট করা আছে কিনা তা পরীক্ষা করার জন্য আমরা Result
-এর is_ok
মেথড ব্যবহার করছি, যার মানে প্রোগ্রামটি একটি case-insensitive সার্চ করবে। যদি IGNORE_CASE
এনভায়রনমেন্ট ভেরিয়েবলটি কিছুতেই সেট না করা থাকে, is_ok
false
রিটার্ন করবে এবং প্রোগ্রামটি একটি case-sensitive সার্চ করবে। আমরা এনভায়রনমেন্ট ভেরিয়েবলের ভ্যালু নিয়ে চিন্তিত নই, শুধু এটি সেট করা আছে নাকি নেই তা নিয়েই ভাবছি, তাই আমরা unwrap
, expect
, বা Result
-এ আমরা দেখেছি এমন অন্য কোনো মেথড ব্যবহার না করে is_ok
পরীক্ষা করছি।
আমরা ignore_case
ভেরিয়েবলের ভ্যালুটি Config
ইনস্ট্যান্সে পাস করি যাতে run
ফাংশনটি সেই ভ্যালুটি পড়তে পারে এবং search_case_insensitive
বা search
কল করার সিদ্ধান্ত নিতে পারে, যেমনটি আমরা লিস্টিং ১২-২২-এ ইমপ্লিমেন্ট করেছি।
চলুন চেষ্টা করে দেখা যাক! প্রথমে আমরা এনভায়রনমেন্ট ভেরিয়েবল সেট না করে এবং to
কোয়েরি দিয়ে আমাদের প্রোগ্রামটি চালাব, যা সমস্ত ছোট হাতের অক্ষরে to শব্দটি ধারণকারী যেকোনো লাইনের সাথে ম্যাচ করা উচিত:
$ cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
মনে হচ্ছে এটি এখনও কাজ করছে! এখন আসুন IGNORE_CASE
-কে 1
এ সেট করে এবং একই কোয়েরি to দিয়ে প্রোগ্রামটি চালাই:
$ IGNORE_CASE=1 cargo run -- to poem.txt
আপনি যদি PowerShell ব্যবহার করেন, তবে আপনাকে এনভায়রনমেন্ট ভেরিয়েবল সেট করতে হবে এবং প্রোগ্রামটি আলাদা কমান্ড হিসাবে চালাতে হবে:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
এটি আপনার শেল সেশনের বাকি অংশের জন্য IGNORE_CASE
-কে স্থায়ী করবে। এটি Remove-Item
cmdlet দিয়ে আনসেট করা যেতে পারে:
PS> Remove-Item Env:IGNORE_CASE
আমাদের এমন লাইন পাওয়া উচিত যাতে to শব্দটি রয়েছে এবং যার মধ্যে বড় হাতের অক্ষর থাকতে পারে:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
চমৎকার, আমরা To ধারণকারী লাইনগুলোও পেয়েছি! আমাদের minigrep
প্রোগ্রাম এখন একটি এনভায়রনমেন্ট ভেরিয়েবল দ্বারা নিয়ন্ত্রিত case-insensitive সার্চিং করতে পারে। এখন আপনি জানেন কীভাবে কমান্ড লাইন আর্গুমেন্ট বা এনভায়রনমেন্ট ভেরিয়েবল ব্যবহার করে সেট করা অপশনগুলো পরিচালনা করতে হয়।
কিছু প্রোগ্রাম একই কনফিগারেশনের জন্য আর্গুমেন্ট এবং এনভায়রনমেন্ট ভেরিয়েবল উভয়ই অনুমোদন করে। সেই ক্ষেত্রে, প্রোগ্রামগুলো সিদ্ধান্ত নেয় যে একটির উপর আরেকটির অগ্রাধিকার থাকবে। আপনার নিজের জন্য আরেকটি অনুশীলনী হিসেবে, একটি কমান্ড লাইন আর্গুমেন্ট বা একটি এনভায়রনমেন্ট ভেরিয়েবলের মাধ্যমে case sensitivity নিয়ন্ত্রণ করার চেষ্টা করুন। প্রোগ্রামটি যদি একটি case sensitive এবং অন্যটি ignore case-এ সেট করে চালানো হয় তবে কমান্ড লাইন আর্গুমেন্ট বা এনভায়রনমেন্ট ভেরিয়েবলের মধ্যে কোনটি অগ্রাধিকার পাবে তা নির্ধারণ করুন।
std::env
মডিউলে এনভায়রনমেন্ট ভেরিয়েবলের সাথে কাজ করার জন্য আরও অনেক দরকারী ফিচার রয়েছে: কী কী উপলব্ধ আছে তা দেখতে এর ডকুমেন্টেশন দেখুন।
স্ট্যান্ডার্ড আউটপুটের পরিবর্তে স্ট্যান্ডার্ড এররে (Standard Error) এরর মেসেজ লেখা
এই মুহূর্তে, আমরা println!
ম্যাক্রো ব্যবহার করে আমাদের সমস্ত আউটপুট টার্মিনালে লিখছি। বেশিরভাগ টার্মিনালে দুই ধরনের আউটপুট থাকে: সাধারণ তথ্যের জন্য স্ট্যান্ডার্ড আউটপুট (stdout
) এবং এরর মেসেজের জন্য স্ট্যান্ডার্ড এরর (stderr
)। এই পার্থক্য ব্যবহারকারীদের একটি প্রোগ্রামের সফল আউটপুটকে একটি ফাইলে পাঠানোর সুযোগ দেয়, কিন্তু তারপরেও এরর মেসেজগুলো স্ক্রিনে প্রিন্ট করতে পারে।
println!
ম্যাক্রো শুধুমাত্র স্ট্যান্ডার্ড আউটপুটে প্রিন্ট করতে সক্ষম, তাই স্ট্যান্ডার্ড এররে প্রিন্ট করার জন্য আমাদের অন্য কিছু ব্যবহার করতে হবে।
এররগুলো কোথায় লেখা হচ্ছে তা পরীক্ষা করা
প্রথমে চলুন দেখি minigrep
দ্বারা প্রিন্ট করা কন্টেন্ট বর্তমানে কীভাবে স্ট্যান্ডার্ড আউটপুটে লেখা হচ্ছে, যার মধ্যে সেইসব এরর মেসেজও অন্তর্ভুক্ত যা আমরা স্ট্যান্ডার্ড এররে লিখতে চাই। আমরা ইচ্ছাকৃতভাবে একটি এরর ঘটিয়ে স্ট্যান্ডার্ড আউটপুট স্ট্রিমকে একটি ফাইলে রিডাইরেক্ট করে এটি করব। আমরা স্ট্যান্ডার্ড এরর স্ট্রিম রিডাইরেক্ট করব না, তাই স্ট্যান্ডার্ড এররে পাঠানো যেকোনো কন্টেন্ট স্ক্রিনে প্রদর্শিত হতে থাকবে।
কমান্ড লাইন প্রোগ্রামগুলো থেকে আশা করা হয় যে তারা এরর মেসেজ স্ট্যান্ডার্ড এরর স্ট্রিমে পাঠাবে যাতে আমরা স্ট্যান্ডার্ড আউটপুট স্ট্রিম একটি ফাইলে রিডাইরেক্ট করলেও স্ক্রিনে এরর মেসেজ দেখতে পাই। আমাদের প্রোগ্রাম বর্তমানে সঠিকভাবে আচরণ করছে না: আমরা দেখতে চলেছি যে এটি এরর মেসেজ আউটপুট একটি ফাইলে সংরক্ষণ করছে!
এই আচরণটি দেখানোর জন্য, আমরা প্রোগ্রামটি >
এবং ফাইলের পাথ, output.txt দিয়ে চালাব, যেখানে আমরা স্ট্যান্ডার্ড আউটপুট স্ট্রিম রিডাইরেক্ট করতে চাই। আমরা কোনো আর্গুমেন্ট পাস করব না, যা একটি এরর তৈরি করবে:
$ cargo run > output.txt
>
সিনট্যাক্সটি শেলকে বলে স্ট্যান্ডার্ড আউটপুটের কন্টেন্ট স্ক্রিনের পরিবর্তে output.txt-এ লিখতে। আমরা স্ক্রিনে প্রত্যাশিত এরর মেসেজটি দেখতে পাইনি, তার মানে এটি অবশ্যই ফাইলে চলে গেছে। output.txt ফাইলে এটি রয়েছে:
Problem parsing arguments: not enough arguments
হ্যাঁ, আমাদের এরর মেসেজটি স্ট্যান্ডার্ড আউটপুটে প্রিন্ট হচ্ছে। এই ধরনের এরর মেসেজ স্ট্যান্ডার্ড এররে প্রিন্ট করা অনেক বেশি দরকারী যাতে শুধুমাত্র একটি সফল রানের ডেটা ফাইলে শেষ হয়। আমরা এটি পরিবর্তন করব।
স্ট্যান্ডার্ড এররে এরর প্রিন্ট করা
এরর মেসেজগুলো কীভাবে প্রিন্ট করা হয় তা পরিবর্তন করার জন্য আমরা লিস্টিং ১২-২৪-এর কোড ব্যবহার করব। এই অধ্যায়ের শুরুতে আমরা যে রিফ্যাক্টরিং করেছি তার কারণে, এরর মেসেজ প্রিন্ট করা সমস্ত কোড একটি ফাংশন, main
-এর মধ্যে রয়েছে। স্ট্যান্ডার্ড লাইব্রেরি eprintln!
ম্যাক্রো সরবরাহ করে যা স্ট্যান্ডার্ড এরর স্ট্রিমে প্রিন্ট করে, তাই আসুন আমরা যে দুটি জায়গায় এরর প্রিন্ট করার জন্য println!
কল করছিলাম, সেখানে eprintln!
ব্যবহার করি।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
আসুন এখন প্রোগ্রামটি আবার একইভাবে চালাই, কোনো আর্গুমেন্ট ছাড়াই এবং >
দিয়ে স্ট্যান্ডার্ড আউটপুট রিডাইরেক্ট করে:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
এখন আমরা স্ক্রিনে এররটি দেখতে পাচ্ছি এবং output.txt ফাইলে কিছুই নেই, যা কমান্ড লাইন প্রোগ্রাম থেকে আমরা আশা করি।
আসুন প্রোগ্রামটি আবার এমন আর্গুমেন্ট দিয়ে চালাই যা কোনো এরর তৈরি করে না কিন্তু স্ট্যান্ডার্ড আউটপুট একটি ফাইলে রিডাইরেক্ট করে, যেমন:
$ cargo run -- to poem.txt > output.txt
আমরা টার্মিনালে কোনো আউটপুট দেখতে পাব না, এবং output.txt আমাদের ফলাফল ধারণ করবে:
ফাইলের নাম: output.txt
Are you nobody, too?
How dreary to be somebody!
এটি প্রমাণ করে যে আমরা এখন সফল আউটপুটের জন্য স্ট্যান্ডার্ড আউটপুট এবং এরর আউটপুটের জন্য যথাযথভাবে স্ট্যান্ডার্ড এরর ব্যবহার করছি।
সারসংক্ষেপ
এই অধ্যায়ে আমরা এ পর্যন্ত শেখা প্রধান কিছু ধারণা পুনরালোচনা করেছি এবং রাস্ট-এ সাধারণ I/O অপারেশনগুলো কীভাবে করতে হয় তা কভার করেছি। কমান্ড লাইন আর্গুমেন্ট, ফাইল, এনভায়রনমেন্ট ভেরিয়েবল, এবং এরর প্রিন্ট করার জন্য eprintln!
ম্যাক্রো ব্যবহার করে, আপনি এখন কমান্ড লাইন অ্যাপ্লিকেশন লেখার জন্য প্রস্তুত। পূর্ববর্তী অধ্যায়গুলোর ধারণার সাথে মিলিত হয়ে, আপনার কোড সুসংগঠিত হবে, উপযুক্ত ডেটা স্ট্রাকচারে কার্যকরভাবে ডেটা সংরক্ষণ করবে, সুন্দরভাবে এরর হ্যান্ডেল করবে এবং ভালোভাবে টেস্ট করা থাকবে।
এরপরে, আমরা ফাংশনাল ল্যাঙ্গুয়েজ দ্বারা প্রভাবিত রাস্টের কিছু ফিচার অন্বেষণ করব: ক্লোজার এবং ইটারেটর।
ফাংশনাল ভাষার বৈশিষ্ট্য: ইটারেটর এবং ক্লোজার
রাস্টের ডিজাইন অনেক বিদ্যমান ভাষা এবং কৌশল থেকে অনুপ্রাণিত হয়েছে, এবং এর মধ্যে একটি গুরুত্বপূর্ণ প্রভাব হলো ফাংশনাল প্রোগ্রামিং (functional programming)। ফাংশনাল স্টাইলে প্রোগ্রামিং করার সময় প্রায়ই ফাংশনগুলোকে ভ্যালু হিসেবে ব্যবহার করা হয়, যেমন তাদেরকে আর্গুমেন্ট হিসেবে পাস করা, অন্য ফাংশন থেকে রিটার্ন করা, এবং পরে এক্সিকিউট করার জন্য ভ্যারিয়েবলে অ্যাসাইন করা ইত্যাদি।
এই অধ্যায়ে, আমরা ফাংশনাল প্রোগ্রামিং কী বা কী নয়, তা নিয়ে বিতর্ক করব না। বরং আমরা রাস্টের এমন কিছু বৈশিষ্ট্য নিয়ে আলোচনা করব যা ফাংশনাল হিসেবে পরিচিত অনেক ভাষার বৈশিষ্ট্যের সাথে সাদৃশ্যপূর্ণ।
বিশেষভাবে, আমরা আলোচনা করব:
- ক্লোজার (Closures), যা ফাংশনের মতো একটি গঠন এবং একে ভ্যারিয়েবলে সংরক্ষণ করা যায়।
- ইটারেটর (Iterators), যা দিয়ে একগুচ্ছ এলিমেন্টকে পর্যায়ক্রমে প্রসেস করা যায়।
- Chapter 12-এর I/O প্রজেক্টকে আরও উন্নত করতে কীভাবে ক্লোজার এবং ইটারেটর ব্যবহার করা যায়।
- ক্লোজার এবং ইটারেটরের পারফরম্যান্স (স্পয়লার: আপনি যা ভাবছেন, এগুলো তার চেয়েও দ্রুত!)
আমরা ইতোমধ্যেই রাস্টের অন্য কিছু বৈশিষ্ট্য, যেমন pattern matching
এবং enums
নিয়ে আলোচনা করেছি, যেগুলো ফাংশনাল স্টাইল দ্বারা প্রভাবিত। যেহেতু সাবলীল (idiomatic) এবং দ্রুতগতির রাস্ট কোড লেখার জন্য ক্লোজার এবং ইটারেটর আয়ত্ত করা একটি গুরুত্বপূর্ণ অংশ, তাই আমরা এই পুরো অধ্যায়টি তাদের জন্যই উৎসর্গ করব।
ক্লোজার (Closures): অ্যানোনিমাস ফাংশন যা তার এনভায়রনমেন্ট ক্যাপচার করতে পারে
রাস্টের ক্লোজার হলো অ্যানোনিমাস ফাংশন (anonymous functions) যা আপনি একটি ভ্যারিয়েবলে সংরক্ষণ করতে পারেন বা অন্য ফাংশনে আর্গুমেন্ট হিসেবে পাস করতে পারেন। আপনি এক জায়গায় ক্লোজার তৈরি করে পরে অন্য কোনো কনটেক্সটে (context) কল করে তাকে এক্সিকিউট করতে পারেন। সাধারণ ফাংশনের মতো নয়, ক্লোজারগুলো যে স্কোপে (scope) তৈরি হয়, সেই স্কোপের ভ্যালু ক্যাপচার করতে পারে। আমরা দেখাব কীভাবে ক্লোজারের এই বৈশিষ্ট্যগুলো কোড পুনঃব্যবহার (reuse) এবং আচরণ কাস্টমাইজ (behavior customization) করার সুযোগ করে দেয়।
ক্লোজার দিয়ে এনভায়রনমেন্ট ক্যাপচার করা
আমরা প্রথমে দেখব কীভাবে ক্লোজার ব্যবহার করে তাদের 정의কৃত এনভায়রনমেন্ট থেকে মান ক্যাপচার করা যায় এবং পরে ব্যবহার করা যায়। দৃশ্যপটটি এরকম: আমাদের টি-শার্ট কোম্পানি মাঝে মাঝে প্রচারের অংশ হিসেবে আমাদের মেইলিং লিস্টের কাউকে একটি এক্সক্লুসিভ, লিমিটেড-এডিশন শার্ট উপহার দেয়। মেইলিং লিস্টের সদস্যরা চাইলে তাদের প্রোফাইলে তাদের প্রিয় রঙ যোগ করতে পারেন। যদি বিনামূল্যে শার্টের জন্য নির্বাচিত ব্যক্তির পছন্দের রঙ সেট করা থাকে, তবে তিনি সেই রঙের শার্ট পাবেন। আর যদি তিনি পছন্দের রঙ উল্লেখ না করে থাকেন, তবে কোম্পানি যে রঙের শার্ট সবচেয়ে বেশি স্টক করেছে, সেটি পাবেন।
এটি বিভিন্ন উপায়ে প্রয়োগ করা যেতে পারে। এই উদাহরণের জন্য, আমরা ShirtColor
নামে একটি enum
ব্যবহার করব, যার দুটি ভ্যারিয়েন্ট থাকবে: Red
এবং Blue
(সহজবোধ্যতার জন্য রঙের সংখ্যা সীমিত রাখা হয়েছে)। কোম্পানির ইনভেন্টরিকে আমরা একটি Inventory
struct দিয়ে প্রকাশ করছি, যার shirts
নামে একটি ফিল্ড আছে। এই ফিল্ডটিতে Vec<ShirtColor>
রয়েছে, যা বর্তমানে স্টকে থাকা শার্টের রঙগুলোকে উপস্থাপন করে। Inventory
struct-এর উপর giveaway
নামে একটি মেথড ডিফাইন করা হয়েছে, যা বিনামূল্যে শার্ট বিজয়ীর পছন্দের রঙের (যদি থাকে) অপশনাল মানটি নেয় এবং ব্যবহারকারী কোন রঙের শার্টটি পাবেন তা রিটার্ন করে। এই সেটআপটি Listing 13-1-এ দেখানো হয়েছে।
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
main
ফাংশনে ডিফাইন করা store
-এ এই লিমিটেড-এডিশন প্রোমোশনের জন্য দুটি নীল এবং একটি লাল শার্ট অবশিষ্ট আছে। আমরা giveaway
মেথডটি একজন ব্যবহারকারীর জন্য কল করি যার পছন্দের রঙ লাল এবং আরেকজন ব্যবহারকারীর জন্য যার কোনো পছন্দের রঙ নেই।
আবারও বলি, এই কোডটি অনেক উপায়ে প্রয়োগ করা যেতে পারে। এখানে, ক্লোজারের উপর মনোযোগ কেন্দ্রীভূত করার জন্য, আমরা কেবল সেই ধারণাগুলো ব্যবহার করেছি যা আপনি ইতিমধ্যে শিখেছেন, শুধুমাত্র giveaway
মেথডের বডি ছাড়া, যেখানে একটি ক্লোজার ব্যবহৃত হয়েছে। giveaway
মেথডে, আমরা ব্যবহারকারীর পছন্দকে Option<ShirtColor>
টাইপের একটি প্যারামিটার হিসাবে গ্রহণ করি এবং user_preference
-এর উপর unwrap_or_else
মেথডটি কল করি। Option<T>
-এর unwrap_or_else
মেথডটি স্ট্যান্ডার্ড লাইব্রেরি দ্বারা ডিফাইন করা। এটি একটি আর্গুমেন্ট নেয়: একটি ক্লোজার যার কোনো আর্গুমেন্ট নেই এবং এটি T
টাইপের একটি ভ্যালু রিটার্ন করে (এই ক্ষেত্রে ShirtColor
, যা Option<T>
-এর Some
ভ্যারিয়েন্টে সংরক্ষিত টাইপের সমান)। যদি Option<T>
-টি Some
ভ্যারিয়েন্ট হয়, unwrap_or_else
সেই Some
-এর ভেতরের ভ্যালুটি রিটার্ন করে। যদি Option<T>
-টি None
ভ্যারিয়েন্ট হয়, unwrap_or_else
ক্লোজারটিকে কল করে এবং ক্লোজারের রিটার্ন করা ভ্যালুটি রিটার্ন করে।
আমরা unwrap_or_else
-এর আর্গুমেন্ট হিসেবে || self.most_stocked()
ক্লোজার এক্সপ্রেশনটি উল্লেখ করেছি। এটি এমন একটি ক্লোজার যা নিজে কোনো প্যারামিটার নেয় না (যদি ক্লোজারের প্যারামিটার থাকত, তবে সেগুলি দুটি ভার্টিকেল পাইপের মধ্যে থাকত)। ক্লোজারের বডি self.most_stocked()
-কে কল করে। আমরা এখানে ক্লোজারটি ডিফাইন করছি, এবং unwrap_or_else
-এর ইমপ্লিমেন্টেশন পরে প্রয়োজন হলে ক্লোজারটিকে মূল্যায়ন করবে।
এই কোডটি রান করলে নিম্নলিখিত আউটপুট প্রিন্ট হবে:
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
এখানে একটি আকর্ষণীয় দিক হলো, আমরা এমন একটি ক্লোজার পাস করেছি যা বর্তমান Inventory
ইনস্ট্যান্সের উপর self.most_stocked()
মেথডটিকে কল করে। স্ট্যান্ডার্ড লাইব্রেরির আমাদের ডিফাইন করা Inventory
বা ShirtColor
টাইপ সম্পর্কে বা এই পরিস্থিতিতে আমরা যে লজিক ব্যবহার করতে চাই সে সম্পর্কে কিছুই জানার প্রয়োজন ছিল না। ক্লোজারটি self
Inventory
ইনস্ট্যান্সের একটি ইমিউটেবল রেফারেন্স (immutable reference) ক্যাপচার করে এবং আমাদের নির্দিষ্ট করা কোডের সাথে unwrap_or_else
মেথডে পাস করে। অন্যদিকে, ফাংশনগুলো এইভাবে তাদের এনভায়রনমেন্ট ক্যাপচার করতে পারে না।
ক্লোজারের টাইপ ইনফারেন্স এবং অ্যানোটেশন
ফাংশন এবং ক্লোজারের মধ্যে আরও কিছু পার্থক্য রয়েছে। ক্লোজারের ক্ষেত্রে সাধারণত আপনাকে fn
ফাংশনের মতো প্যারামিটারের টাইপ বা রিটার্ন ভ্যালুর টাইপ অ্যানোটেট (annotate) করতে হয় না। ফাংশনের উপর টাইপ অ্যানোটেশন প্রয়োজন কারণ টাইপগুলো আপনার ব্যবহারকারীদের কাছে প্রকাশিত একটি সুস্পষ্ট ইন্টারফেসের (explicit interface) অংশ। একটি ফাংশন কী ধরনের ভ্যালু ব্যবহার করে এবং রিটার্ন করে সে বিষয়ে সবাই যাতে একমত হয়, তা নিশ্চিত করার জন্য এই ইন্টারফেসটিকে কঠোরভাবে ডিফাইন করা গুরুত্বপূর্ণ। অন্যদিকে, ক্লোজারগুলো এমন কোনো প্রকাশিত ইন্টারফেসে ব্যবহৃত হয় না: এগুলি ভ্যারিয়েবলে সংরক্ষণ করা হয় এবং নাম না দিয়ে এবং আমাদের লাইব্রেরির ব্যবহারকারীদের কাছে প্রকাশ না করেই ব্যবহার করা হয়।
ক্লোজারগুলো সাধারণত সংক্ষিপ্ত হয় এবং যেকোনো নির্বিচার পরিস্থিতির পরিবর্তে শুধুমাত্র একটি সংকীর্ণ কনটেক্সটের মধ্যে প্রাসঙ্গিক হয়। এই সীমিত কনটেক্সটগুলোর মধ্যে, কম্পাইলার প্যারামিটারের টাইপ এবং রিটার্ন টাইপ অনুমান (infer) করতে পারে, ঠিক যেমন এটি বেশিরভাগ ভ্যারিয়েবলের টাইপ অনুমান করতে সক্ষম (বিরল ক্ষেত্রে কম্পাইলারেরও ক্লোজার টাইপ অ্যানোটেশন প্রয়োজন হয়)।
ভ্যারিয়েবলের মতোই, আমরা চাইলে টাইপ অ্যানোটেশন যোগ করতে পারি যাতে কোডটি আরও সুস্পষ্ট এবং পরিষ্কার হয়, যদিও এর জন্য কোডটি প্রয়োজনের চেয়ে বেশি ভার্বোস (verbose) হয়ে যায়। একটি ক্লোজারের জন্য টাইপ অ্যানোটেট করা Listing 13-2-এ দেখানো সংজ্ঞার মতো দেখাবে। এই উদাহরণে, আমরা একটি ক্লোজার ডিফাইন করে সেটিকে একটি ভ্যারিয়েবলে সংরক্ষণ করছি, Listing 13-1-এর মতো আর্গুমেন্ট হিসাবে পাস করার সময় ডিফাইন করার পরিবর্তে।
use std::thread; use std::time::Duration; fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!("Today, do {} pushups!", expensive_closure(intensity)); println!("Next, do {} situps!", expensive_closure(intensity)); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_closure(intensity) ); } } } fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout(simulated_user_specified_value, simulated_random_number); }
টাইপ অ্যানোটেশন যোগ করার সাথে সাথে ক্লোজারের সিনট্যাক্স ফাংশনের সিনট্যাক্সের সাথে আরও সাদৃশ্যপূর্ণ দেখায়। এখানে, তুলনার জন্য আমরা একটি ফাংশন ডিফাইন করছি যা তার প্যারামিটারে 1 যোগ করে এবং একই আচরণের একটি ক্লোজার ডিফাইন করছি। প্রাসঙ্গিক অংশগুলো মেলানোর জন্য আমরা কিছু স্পেস যোগ করেছি। এটি দেখায় যে ক্লোজারের সিনট্যাক্স ফাংশনের সিনট্যাক্সের মতোই, শুধু পাইপ-এর ব্যবহার এবং সিনট্যাক্সের ঐচ্ছিক অংশগুলোর পরিমাণ ছাড়া:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
প্রথম লাইনে একটি ফাংশন ডেফিনিশন এবং দ্বিতীয় লাইনে একটি সম্পূর্ণ অ্যানোটেটেড ক্লোজার ডেফিনিশন দেখানো হয়েছে। তৃতীয় লাইনে, আমরা ক্লোজার ডেফিনিশন থেকে টাইপ অ্যানোটেশনগুলো সরিয়ে দিয়েছি। চতুর্থ লাইনে, আমরা ব্র্যাকেটগুলো সরিয়ে দিয়েছি, যা ঐচ্ছিক কারণ ক্লোজারের বডিতে শুধুমাত্র একটি এক্সপ্রেশন আছে। এগুলি সবই বৈধ ডেফিনিশন যা কল করা হলে একই আচরণ তৈরি করবে। add_one_v3
এবং add_one_v4
লাইনগুলোর জন্য ক্লোজারগুলোকে মূল্যায়ন করা প্রয়োজন যাতে কম্পাইল করা যায়, কারণ টাইপগুলো তাদের ব্যবহার থেকে অনুমান করা হবে। এটি let v = Vec::new();
-এর মতো, যেখানে রাস্টের টাইপ অনুমান করতে পারার জন্য হয় টাইপ অ্যানোটেশন প্রয়োজন অথবা Vec
-এর মধ্যে কোনো টাইপের ভ্যালু ঢোকানো প্রয়োজন।
ক্লোজার ডেফিনিশনের জন্য, কম্পাইলার প্রতিটি প্যারামিটার এবং তাদের রিটার্ন ভ্যালুর জন্য একটি করে কনক্রিট টাইপ (concrete type) অনুমান করবে। উদাহরণস্বরূপ, Listing 13-3 একটি সংক্ষিপ্ত ক্লোজারের ডেফিনিশন দেখায় যা কেবল তার প্যারামিটার হিসাবে প্রাপ্ত ভ্যালুটি রিটার্ন করে। এই ক্লোজারটি এই উদাহরণের উদ্দেশ্য ছাড়া তেমন কার্যকর নয়। লক্ষ্য করুন যে আমরা ডেফিনিশনে কোনো টাইপ অ্যানোটেশন যোগ করিনি। যেহেতু কোনো টাইপ অ্যানোটেশন নেই, তাই আমরা যেকোনো টাইপ দিয়ে ক্লোজারটিকে কল করতে পারি, যা আমরা এখানে প্রথমবার String
দিয়ে করেছি। যদি আমরা এরপর example_closure
-কে একটি ইন্টিজার (integer) দিয়ে কল করার চেষ্টা করি, তাহলে আমরা একটি এরর পাব।
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
কম্পাইলার আমাদের এই এররটি দেয়:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^- help: try using a conversion method: `.to_string()`
| | |
| | expected `String`, found integer
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
প্রথমবার যখন আমরা example_closure
-কে String
ভ্যালু দিয়ে কল করি, কম্পাইলার x
-এর টাইপ এবং ক্লোজারের রিটার্ন টাইপ String
হিসাবে অনুমান করে। সেই টাইপগুলো তখন example_closure
-এর ক্লোজারে লক হয়ে যায়, এবং আমরা যখন পরবর্তী সময়ে একই ক্লোজারের সাথে একটি ভিন্ন টাইপ ব্যবহার করার চেষ্টা করি তখন একটি টাইপ এরর পাই।
রেফারেন্স ক্যাপচার করা বা মালিকানা মুভ করা
ক্লোজার তাদের এনভায়রনমেন্ট থেকে তিনটি উপায়ে ভ্যালু ক্যাপচার করতে পারে, যা সরাসরি একটি ফাংশনের প্যারামিটার নেওয়ার তিনটি উপায়ের সাথে মিলে যায়: ইমিউটেবলভাবে ধার করা (borrowing immutably), মিউটেবলভাবে ধার করা (borrowing mutably), এবং মালিকানা নেওয়া (taking ownership)। ক্লোজারের ফাংশন বডি ক্যাপচার করা ভ্যালুগুলো দিয়ে কী করে তার উপর ভিত্তি করে ক্লোজার সিদ্ধান্ত নেবে কোনটি ব্যবহার করতে হবে।
Listing 13-4-এ, আমরা একটি ক্লোজার ডিফাইন করেছি যা list
নামের ভেক্টরের একটি ইমিউটেবল রেফারেন্স ক্যাপচার করে কারণ এটির কেবল ভ্যালু প্রিন্ট করার জন্য একটি ইমিউটেবল রেফারেন্স প্রয়োজন।
fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); let only_borrows = || println!("From closure: {list:?}"); println!("Before calling closure: {list:?}"); only_borrows(); println!("After calling closure: {list:?}"); }
এই উদাহরণটি আরও দেখায় যে একটি ভ্যারিয়েবল একটি ক্লোজার ডেফিনিশনে বাইন্ড হতে পারে, এবং আমরা পরে ভ্যারিয়েবলের নাম এবং প্যারেনথেসিস ব্যবহার করে ক্লোজারটিকে কল করতে পারি যেন ভ্যারিয়েবলের নামটি একটি ফাংশনের নাম।
যেহেতু আমরা একই সময়ে list
-এর একাধিক ইমিউটেবল রেফারেন্স রাখতে পারি, তাই list
ক্লোজার ডেফিনিশনের আগে, ক্লোজার ডেফিনিশনের পরে কিন্তু কল করার আগে, এবং ক্লোজার কল করার পরেও অ্যাক্সেসযোগ্য। এই কোডটি কম্পাইল হয়, রান করে এবং প্রিন্ট করে:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
এরপরে, Listing 13-5-এ, আমরা ক্লোজারের বডি পরিবর্তন করে list
ভেক্টরে একটি এলিমেন্ট যোগ করি। ক্লোজারটি এখন একটি মিউটেবল রেফারেন্স ক্যাপচার করে।
fn main() { let mut list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!("After calling closure: {list:?}"); }
এই কোডটি কম্পাইল হয়, রান করে এবং প্রিন্ট করে:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
লক্ষ্য করুন যে borrows_mutably
ক্লোজারের ডেফিনিশন এবং কলের মধ্যে আর কোনো println!
নেই: যখন borrows_mutably
ডিফাইন করা হয়, তখন এটি list
-এর একটি মিউটেবল রেফারেন্স ক্যাপচার করে। ক্লোজারটি কল করার পরে আমরা আর ক্লোজারটি ব্যবহার করি না, তাই মিউটেবল বরো (mutable borrow) শেষ হয়ে যায়। ক্লোজার ডেফিনিশন এবং ক্লোজার কলের মধ্যে, প্রিন্ট করার জন্য একটি ইমিউটেবল বরো অনুমোদিত নয় কারণ যখন একটি মিউটেবল বরো থাকে তখন অন্য কোনো বরো অনুমোদিত নয়। আপনি কী এরর মেসেজ পান তা দেখতে সেখানে একটি println!
যোগ করার চেষ্টা করুন!
আপনি যদি ক্লোজারকে তার এনভায়রনমেন্টে ব্যবহৃত ভ্যালুগুলোর মালিকানা নিতে বাধ্য করতে চান, যদিও ক্লোজারের বডির কঠোরভাবে মালিকানার প্রয়োজন নেই, আপনি প্যারামিটার তালিকার আগে move
কীওয়ার্ড ব্যবহার করতে পারেন।
এই কৌশলটি বেশিরভাগ সময় একটি নতুন থ্রেডে (thread) ক্লোজার পাস করার সময় উপযোগী হয়, যাতে ডেটা মুভ করে নতুন থ্রেডের মালিকানাধীন করা যায়। আমরা Chapter 16-এ যখন কনকারেন্সি (concurrency) নিয়ে আলোচনা করব তখন থ্রেড এবং কেন আপনি সেগুলি ব্যবহার করতে চাইবেন সে সম্পর্কে বিস্তারিত আলোচনা করব, কিন্তু আপাতত, move
কীওয়ার্ড প্রয়োজন এমন একটি ক্লোজার ব্যবহার করে একটি নতুন থ্রেড স্পন (spawn) করার বিষয়টি সংক্ষেপে অন্বেষণ করা যাক। Listing 13-6, Listing 13-4-কে পরিবর্তন করে দেখায় কীভাবে মেইন থ্রেডের পরিবর্তে একটি নতুন থ্রেডে ভেক্টর প্রিন্ট করা যায়।
use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {list:?}"); thread::spawn(move || println!("From thread: {list:?}")) .join() .unwrap(); }
আমরা একটি নতুন থ্রেড স্পন করি, এবং থ্রেডটিকে চালানোর জন্য একটি ক্লোজার আর্গুমেন্ট হিসাবে দিই। ক্লোজারের বডি লিস্টটি প্রিন্ট করে। Listing 13-4-এ, ক্লোজারটি শুধুমাত্র একটি ইমিউটেবল রেফারেন্স ব্যবহার করে list
ক্যাপচার করেছিল কারণ list
প্রিন্ট করার জন্য এটিই সর্বনিম্ন অ্যাক্সেস যা প্রয়োজন ছিল। এই উদাহরণে, যদিও ক্লোজারের বডিকে এখনও শুধুমাত্র একটি ইমিউটেবল রেফারেন্স প্রয়োজন, আমাদের নির্দিষ্ট করতে হবে যে list
-কে ক্লোজারের মধ্যে মুভ করা উচিত এবং এর জন্য ক্লোজার ডেফিনিশনের শুরুতে move
কীওয়ার্ডটি বসাতে হবে। যদি মেইন থ্রেড নতুন থ্রেডে join
কল করার আগে আরও কাজ সম্পাদন করত, তাহলে নতুন থ্রেডটি মেইন থ্রেডের বাকি অংশ শেষ হওয়ার আগে শেষ হতে পারত, অথবা মেইন থ্রেডটি আগে শেষ হতে পারত। যদি মেইন থ্রেড list
-এর মালিকানা বজায় রাখত কিন্তু নতুন থ্রেড শেষ হওয়ার আগেই শেষ হয়ে যেত এবং list
-কে ড্রপ করে দিত, তাহলে থ্রেডের ইমিউটেবল রেফারেন্সটি অবৈধ হয়ে যেত। অতএব, কম্পাইলারের প্রয়োজন হয় যে list
-কে নতুন থ্রেডে দেওয়া ক্লোজারের মধ্যে মুভ করা হোক যাতে রেফারেন্সটি বৈধ থাকে। আপনি কী কম্পাইলার এরর পান তা দেখতে move
কীওয়ার্ডটি সরিয়ে দেওয়ার বা ক্লোজার ডিফাইন করার পরে মেইন থ্রেডে list
ব্যবহার করার চেষ্টা করুন!
ক্যাপচার করা মান ক্লোজারের বাইরে মুভ করা এবং Fn
ট্রেইট
যখন একটি ক্লোজার তার এনভায়রনমেন্ট থেকে একটি রেফারেন্স বা একটি মানের মালিকানা ক্যাপচার করে (যা ক্লোজারের ভিতরে কী মুভ হবে তা প্রভাবিত করে), তখন ক্লোজারের বডির কোড নির্ধারণ করে যে ক্লোজারটি পরে মূল্যায়ন করা হলে সেই রেফারেন্স বা মানগুলোর কী হবে (যা ক্লোজারের বাইরে কী মুভ হবে তা প্রভাবিত করে)।
একটি ক্লোজারের বডি নিম্নলিখিত যেকোনোটি করতে পারে: একটি ক্যাপচার করা মান ক্লোজারের বাইরে মুভ করা, ক্যাপচার করা মান পরিবর্তন করা, মানটি মুভ বা পরিবর্তন না করা, অথবা শুরু থেকেই এনভায়রনমেন্ট থেকে কিছুই ক্যাপচার না করা।
একটি ক্লোজার যেভাবে এনভায়রনমেন্ট থেকে মান ক্যাপচার এবং হ্যান্ডেল করে তা প্রভাবিত করে যে ক্লোজারটি কোন ট্রেইট (trait) ইমপ্লিমেন্ট করবে, এবং ফাংশন ও স্ট্রাকটগুলো ট্রেইটের মাধ্যমেই নির্দিষ্ট করতে পারে যে তারা কোন ধরনের ক্লোজার ব্যবহার করতে পারবে। ক্লোজারগুলো তাদের বডি কীভাবে মান হ্যান্ডেল করে তার উপর নির্ভর করে, এই তিনটি Fn
ট্রেইটের মধ্যে একটি, দুটি, বা তিনটিই স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করবে:
FnOnce
সেইসব ক্লোজারের ক্ষেত্রে প্রযোজ্য যা একবার কল করা যেতে পারে। সমস্ত ক্লোজার অন্তত এই ট্রেইটটি ইমপ্লিমেন্ট করে কারণ সমস্ত ক্লোজার কল করা যায়। একটি ক্লোজার যা তার বডি থেকে ক্যাপচার করা মান মুভ করে ফেলে, সেটি শুধুমাত্রFnOnce
ইমপ্লিমেন্ট করবে এবং অন্য কোনোFn
ট্রেইট করবে না, কারণ এটি শুধুমাত্র একবারই কল করা যেতে পারে।FnMut
সেইসব ক্লোজারের ক্ষেত্রে প্রযোজ্য যা তাদের বডি থেকে ক্যাপচার করা মান মুভ করে না, কিন্তু ক্যাপচার করা মান পরিবর্তন (mutate) করতে পারে। এই ক্লোজারগুলো একাধিকবার কল করা যেতে পারে।Fn
সেইসব ক্লোজারের ক্ষেত্রে প্রযোজ্য যা তাদের বডি থেকে ক্যাপচার করা মান মুভ করে না এবং ক্যাপচার করা মান পরিবর্তনও করে না, সেইসাথে সেই ক্লোজারগুলো যা এনভায়রনমেন্ট থেকে কিছুই ক্যাপচার করে না। এই ক্লোজারগুলো তাদের এনভায়রনমেন্ট পরিবর্তন না করে একাধিকবার কল করা যেতে পারে, যা কনকারেন্টভাবে একটি ক্লোজারকে একাধিকবার কল করার মতো ক্ষেত্রে গুরুত্বপূর্ণ।
আসুন Option<T>
-এর unwrap_or_else
মেথডের ডেফিনিশন দেখি যা আমরা Listing 13-1-এ ব্যবহার করেছি:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
স্মরণ করুন যে T
হলো জেনেরিক টাইপ যা Option
-এর Some
ভ্যারিয়েন্টের মানের টাইপকে প্রতিনিধিত্ব করে। সেই T
টাইপটি unwrap_or_else
ফাংশনের রিটার্ন টাইপও: উদাহরণস্বরূপ, Option<String>
-এর উপর unwrap_or_else
কল করা কোড একটি String
পাবে।
এরপর লক্ষ্য করুন যে unwrap_or_else
ফাংশনের একটি অতিরিক্ত জেনেরিক টাইপ প্যারামিটার F
আছে। F
টাইপটি f
নামের প্যারামিটারের টাইপ, যা হলো সেই ক্লোজার যা আমরা unwrap_or_else
কল করার সময় সরবরাহ করি।
জেনেরিক টাইপ F
-এর উপর নির্দিষ্ট করা ট্রেইট বাউন্ড (trait bound) হলো FnOnce() -> T
, যার মানে F
-কে অবশ্যই একবার কল করা যেতে হবে, কোনো আর্গুমেন্ট না নিতে হবে, এবং একটি T
রিটার্ন করতে হবে। ট্রেইট বাউন্ডে FnOnce
ব্যবহার করার মাধ্যমে এই সীমাবদ্ধতা প্রকাশ করা হয় যে unwrap_or_else
f
-কে সর্বোচ্চ একবার কল করবে। unwrap_or_else
-এর বডিতে আমরা দেখতে পাই যে যদি Option
হয় Some
, তাহলে f
কল করা হবে না। যদি Option
হয় None
, তাহলে f
একবার কল করা হবে। যেহেতু সমস্ত ক্লোজার FnOnce
ইমপ্লিমেন্ট করে, তাই unwrap_or_else
সব তিন ধরনের ক্লোজার গ্রহণ করে এবং যতটা সম্ভব ফ্লেক্সিবল।
দ্রষ্টব্য: যদি আমাদের যা করতে হবে তার জন্য এনভায়রনমেন্ট থেকে কোনো মান ক্যাপচার করার প্রয়োজন না হয়, তাহলে যেখানে আমাদের
Fn
ট্রেইটগুলোর একটি ইমপ্লিমেন্ট করে এমন কিছু প্রয়োজন সেখানে আমরা একটি ক্লোজারের পরিবর্তে একটি ফাংশনের নাম ব্যবহার করতে পারি। উদাহরণস্বরূপ, একটিOption<Vec<T>>
মানের উপর, আমরাunwrap_or_else(Vec::new)
কল করতে পারি যাতে মানটিNone
হলে একটি নতুন, খালি ভেক্টর পাওয়া যায়। কম্পাইলার স্বয়ংক্রিয়ভাবে একটি ফাংশন ডেফিনিশনের জন্য প্রযোজ্যFn
ট্রেইটগুলোর যেকোনোটি ইমপ্লিমেন্ট করে।
এখন আসুন স্ট্যান্ডার্ড লাইব্রেরি মেথড sort_by_key
, যা স্লাইসের উপর ডিফাইন করা আছে, দেখি এটি unwrap_or_else
-এর থেকে কীভাবে আলাদা এবং কেন sort_by_key
ট্রেইট বাউন্ডের জন্য FnOnce
-এর পরিবর্তে FnMut
ব্যবহার করে। ক্লোজারটি স্লাইসের বর্তমান আইটেমের একটি রেফারেন্স আকারে একটি আর্গুমেন্ট পায় এবং K
টাইপের একটি মান রিটার্ন করে যা অর্ডার করা যায়। এই ফাংশনটি উপযোগী যখন আপনি প্রতিটি আইটেমের একটি নির্দিষ্ট অ্যাট্রিবিউট দ্বারা একটি স্লাইস সর্ট করতে চান। Listing 13-7-এ, আমাদের কাছে Rectangle
ইনস্ট্যান্সের একটি তালিকা আছে এবং আমরা সেগুলোকে তাদের width
অ্যাট্রিবিউট দ্বারা ছোট থেকে বড় ক্রমে সাজানোর জন্য sort_by_key
ব্যবহার করি।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!("{list:#?}"); }
এই কোডটি প্রিন্ট করে:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
sort_by_key
একটি FnMut
ক্লোজার নেওয়ার জন্য ডিফাইন করা হয়েছে কারণ এটি ক্লোজারটিকে একাধিকবার কল করে: স্লাইসের প্রতিটি আইটেমের জন্য একবার। |r| r.width
ক্লোজারটি তার এনভায়রনমেন্ট থেকে কিছু ক্যাপচার, পরিবর্তন বা মুভ করে না, তাই এটি ট্রেইট বাউন্ডের প্রয়োজনীয়তা পূরণ করে।
বিপরীতে, Listing 13-8 একটি ক্লোজারের উদাহরণ দেখায় যা শুধুমাত্র FnOnce
ট্রেইট ইমপ্লিমেন্ট করে, কারণ এটি এনভায়রনমেন্ট থেকে একটি মান মুভ করে। কম্পাইলার আমাদের এই ক্লোজারটি sort_by_key
-এর সাথে ব্যবহার করতে দেবে না।
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
এটি list
সর্ট করার সময় sort_by_key
কতবার ক্লোজার কল করে তা গণনা করার একটি কৃত্রিম, জটিল উপায় (যা কাজ করে না)। এই কোডটি sort_operations
ভেক্টরে value
—ক্লোজারের এনভায়রনমেন্ট থেকে একটি String
—পুশ করে এই গণনা করার চেষ্টা করে। ক্লোজারটি value
ক্যাপচার করে এবং তারপর value
-এর মালিকানা sort_operations
ভেক্টরে স্থানান্তর করে ক্লোজারের বাইরে মুভ করে দেয়। এই ক্লোজারটি একবার কল করা যেতে পারে; দ্বিতীয়বার কল করার চেষ্টা করলে কাজ করবে না কারণ value
আর এনভায়রনমেন্টে থাকবে না যে আবার sort_operations
-এ পুশ করা যায়! তাই, এই ক্লোজারটি শুধুমাত্র FnOnce
ইমপ্লিমেন্ট করে। যখন আমরা এই কোডটি কম্পাইল করার চেষ্টা করি, তখন আমরা এই এরর পাই যে value
-কে ক্লোজারের বাইরে মুভ করা যাবে না কারণ ক্লোজারটিকে অবশ্যই FnMut
ইমপ্লিমেন্ট করতে হবে:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
এররটি ক্লোজারের বডির সেই লাইনের দিকে নির্দেশ করে যা value
-কে এনভায়রনমেন্টের বাইরে মুভ করে। এটি ঠিক করতে, আমাদের ক্লোজারের বডি পরিবর্তন করতে হবে যাতে এটি এনভায়রনমেন্ট থেকে মান মুভ না করে। এনভায়রনমেন্টে একটি কাউন্টার রাখা এবং ক্লোজারের বডিতে এর মান বাড়ানো হলো ক্লোজারটি কতবার কল করা হয়েছে তা গণনা করার একটি সহজ উপায়। Listing 13-9-এর ক্লোজারটি sort_by_key
-এর সাথে কাজ করে কারণ এটি শুধুমাত্র num_sort_operations
কাউন্টারের একটি মিউটেবল রেফারেন্স ক্যাপচার করছে এবং তাই একাধিকবার কল করা যেতে পারে:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!("{list:#?}, sorted in {num_sort_operations} operations"); }
ক্লোজার ব্যবহার করে এমন ফাংশন বা টাইপ ডিফাইন বা ব্যবহার করার সময় Fn
ট্রেইটগুলো গুরুত্বপূর্ণ। পরবর্তী বিভাগে, আমরা ইটারেটর নিয়ে আলোচনা করব। অনেক ইটারেটর মেথড ক্লোজার আর্গুমেন্ট নেয়, তাই আমরা যখন এগোব তখন এই ক্লোজারের বিস্তারিত মনে রাখবেন!
ইটারেটর ব্যবহার করে আইটেমের সিরিজ প্রসেস করা
ইটারেটর প্যাটার্ন (iterator pattern) আপনাকে একটি সিকোয়েন্সের (sequence) প্রতিটি আইটেমের উপর পর্যায়ক্রমে কোনো কাজ করার সুযোগ দেয়। একটি ইটারেটর প্রতিটি আইটেমের উপর পুনরাবৃত্তি (iterating) করার এবং সিকোয়েন্সটি কখন শেষ হয়েছে তা নির্ধারণ করার লজিকের জন্য দায়ী থাকে। আপনি যখন ইটারেটর ব্যবহার করেন, তখন আপনাকে সেই লজিকটি নিজে থেকে পুনরায় ইমপ্লিমেন্ট (reimplement) করতে হয় না।
রাস্টে, ইটারেটরগুলো lazy (অলস), যার মানে হলো যতক্ষণ না আপনি ইটারেটরটিকে ব্যবহার করার জন্য কোনো মেথড কল করছেন, ততক্ষণ পর্যন্ত এর কোনো প্রভাব থাকে না। উদাহরণস্বরূপ, Listing 13-10-এর কোড v1
ভেক্টরের আইটেমগুলোর উপর একটি ইটারেটর তৈরি করে, যা Vec<T>
-তে ডিফাইন করা iter
মেথড কল করার মাধ্যমে করা হয়। এই কোডটি নিজে থেকে কোনো দরকারী কাজ করে না।
fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }``` </Listing> ইটারেটরটি `v1_iter` ভ্যারিয়েবলে সংরক্ষণ করা হয়েছে। একবার আমরা একটি ইটারেটর তৈরি করলে, আমরা এটিকে বিভিন্ন উপায়ে ব্যবহার করতে পারি। Listing 3-5-এ, আমরা একটি `for` লুপ ব্যবহার করে একটি অ্যারের উপর ইটারেট করেছিলাম এবং প্রতিটি আইটেমের উপর কিছু কোড এক্সিকিউট করেছিলাম। পর্দার আড়ালে, এটি একটি ইটারেটর তৈরি করে এবং তারপর তা ব্যবহার করে, কিন্তু এখন পর্যন্ত আমরা এটি ঠিক কীভাবে কাজ করে তা বিস্তারিত আলোচনা করিনি। Listing 13-11-এর উদাহরণে, আমরা ইটারেটর তৈরি করা এবং `for` লুপে ইটারেটর ব্যবহার করাকে আলাদা করেছি। যখন `v1_iter`-এর ইটারেটর ব্যবহার করে `for` লুপ কল করা হয়, তখন ইটারেটরের প্রতিটি এলিমেন্ট লুপের একটি ইটারেশনে ব্যবহৃত হয়, যা প্রতিটি মান প্রিন্ট করে। <Listing number="13-11" file-name="src/main.rs" caption="`for` লুপে একটি ইটারেটর ব্যবহার করা"> ```rust fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {val}"); } }
যেসব ভাষায় তাদের স্ট্যান্ডার্ড লাইব্রেরিতে ইটারেটর সরবরাহ করা হয় না, সেখানে আপনাকে সম্ভবত একই কার্যকারিতা লিখতে হতো একটি ভ্যারিয়েবলকে ইনডেক্স ০ থেকে শুরু করে, সেই ভ্যারিয়েবলটি ব্যবহার করে ভেক্টর থেকে একটি মান পেতে, এবং লুপের মধ্যে ভ্যারিয়েবলের মান বাড়িয়ে যতক্ষণ না এটি ভেক্টরের মোট আইটেমের সংখ্যায় পৌঁছায়।
ইটারেটর আপনার জন্য এই সমস্ত লজিক পরিচালনা করে, যা পুনরাবৃত্তিমূলক কোড কমিয়ে দেয় এবং সম্ভাব্য ভুল এড়াতে সাহায্য করে। ইটারেটর আপনাকে অনেক বিভিন্ন ধরণের সিকোয়েন্সের সাথে একই লজিক ব্যবহার করার জন্য আরও বেশি ফ্লেক্সিবিলিটি দেয়, শুধু ভেক্টরের মতো ডেটা স্ট্রাকচার নয় যা আপনি ইনডেক্স করতে পারেন। আসুন দেখি ইটারেটর কীভাবে তা করে।
Iterator
ট্রেইট এবং next
মেথড
সমস্ত ইটারেটর Iterator
নামের একটি ট্রেইট (trait) ইমপ্লিমেন্ট করে যা স্ট্যান্ডার্ড লাইব্রেরিতে ডিফাইন করা আছে। ট্রেইটের ডেফিনিশনটি দেখতে এইরকম:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided } }
লক্ষ্য করুন যে এই ডেফিনিশনে কিছু নতুন সিনট্যাক্স ব্যবহার করা হয়েছে: type Item
এবং Self::Item
, যা এই ট্রেইটের সাথে একটি associated type ডিফাইন করছে। আমরা Chapter 20-এ associated type নিয়ে গভীরভাবে আলোচনা করব। আপাতত, আপনার শুধু এটুকু জানলেই চলবে যে এই কোডটি বলছে Iterator
ট্রেইট ইমপ্লিমেন্ট করার জন্য আপনাকে একটি Item
টাইপও ডিফাইন করতে হবে, এবং এই Item
টাইপটি next
মেথডের রিটার্ন টাইপে ব্যবহৃত হয়। অন্য কথায়, Item
টাইপটি হবে ইটারেটর থেকে রিটার্ন করা টাইপ।
Iterator
ট্রেইট ইমপ্লিমেন্ট করার জন্য শুধুমাত্র একটি মেথড ডিফাইন করতে হয়: next
মেথড, যা ইটারেটরের একটি করে আইটেম Some
-এ মুড়িয়ে রিটার্ন করে, এবং যখন ইটারেশন শেষ হয়ে যায়, তখন None
রিটার্ন করে।
আমরা ইটারেটরের উপর সরাসরি next
মেথড কল করতে পারি; Listing 13-12 দেখায় যে ভেক্টর থেকে তৈরি করা ইটারেটরের উপর বারবার next
কল করলে কী মান রিটার্ন হয়।
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
লক্ষ্য করুন যে আমাদের v1_iter
-কে মিউটেবল (mutable) করতে হয়েছিল: একটি ইটারেটরের উপর next
মেথড কল করলে এর অভ্যন্তরীণ অবস্থা পরিবর্তিত হয়, যা ইটারেটর সিকোয়েন্সে তার অবস্থান ট্র্যাক করতে ব্যবহার করে। অন্য কথায়, এই কোডটি ইটারেটরটিকে কনজিউম (consumes) বা ব্যবহার করে ফেলে। প্রতিটি next
কল ইটারেটর থেকে একটি আইটেম গ্রহণ করে। for
লুপ ব্যবহার করার সময় আমাদের v1_iter
-কে মিউটেবল করতে হয়নি কারণ লুপটি v1_iter
-এর মালিকানা নিয়ে পর্দার আড়ালে এটিকে মিউটেবল করে দিয়েছিল।
আরও লক্ষ্য করুন যে next
কল থেকে আমরা যে মানগুলো পাই তা ভেক্টরের মানগুলোর ইমিউটেবল রেফারেন্স (immutable references)। iter
মেথড ইমিউটেবল রেফারেন্সের উপর একটি ইটারেটর তৈরি করে। যদি আমরা এমন একটি ইটারেটর তৈরি করতে চাই যা v1
-এর মালিকানা নেয় এবং ওউনড ভ্যালু (owned values) রিটার্ন করে, তাহলে আমরা iter
-এর পরিবর্তে into_iter
কল করতে পারি। একইভাবে, যদি আমরা মিউটেবল রেফারেন্সের উপর ইটারেট করতে চাই, তাহলে আমরা iter
-এর পরিবর্তে iter_mut
কল করতে পারি।
যে মেথডগুলো ইটারেটরকে ব্যবহার করে ফেলে (consume)
Iterator
ট্রেইটে স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সরবরাহ করা ডিফল্ট ইমপ্লিমেন্টেশনসহ বেশ কয়েকটি ভিন্ন মেথড রয়েছে; আপনি Iterator
ট্রেইটের জন্য স্ট্যান্ডার্ড লাইব্রেরি API ডকুমেন্টেশনে এই মেথডগুলো সম্পর্কে জানতে পারবেন। এই মেথডগুলোর মধ্যে কিছু তাদের ডেফিনিশনে next
মেথডকে কল করে, যে কারণে Iterator
ট্রেইট ইমপ্লিমেন্ট করার সময় আপনাকে next
মেথড ইমপ্লিমেন্ট করতে হয়।
যে মেথডগুলো next
কল করে, সেগুলোকে কনজিউমিং অ্যাডাপ্টার (consuming adapters) বলা হয়, কারণ এগুলো কল করলে ইটারেটরটি ব্যবহৃত হয়ে যায়। একটি উদাহরণ হলো sum
মেথড, যা ইটারেটরের মালিকানা নেয় এবং বারবার next
কল করে আইটেমগুলোর মধ্য দিয়ে ইটারেট করে, ফলে ইটারেটরটি ব্যবহৃত হয়। এটি ইটারেট করার সময় প্রতিটি আইটেমকে একটি চলমান মোটের সাথে যোগ করে এবং ইটারেশন সম্পূর্ণ হলে মোটটি রিটার্ন করে। Listing 13-13-এ sum
মেথডের ব্যবহার দেখানো একটি টেস্ট রয়েছে।
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
sum
কল করার পরে আমরা v1_iter
ব্যবহার করতে পারব না কারণ sum
যে ইটারেটরের উপর কল করা হয় তার মালিকানা নিয়ে নেয়।
যে মেথডগুলো অন্য ইটারেটর তৈরি করে
ইটারেটর অ্যাডাপ্টার (Iterator adapters) হলো Iterator
ট্রেইটে ডিফাইন করা এমন মেথড যা ইটারেটরকে ব্যবহার করে না। বরং, এগুলো মূল ইটারেটরের কিছু দিক পরিবর্তন করে ভিন্ন ইটারেটর তৈরি করে।
Listing 13-14 ইটারেটর অ্যাডাপ্টার মেথড map
কল করার একটি উদাহরণ দেখায়, যা একটি ক্লোজার নেয় এবং আইটেমগুলোর উপর ইটারেট করার সময় প্রতিটি আইটেমের উপর সেই ক্লোজারকে কল করে। map
মেথড একটি নতুন ইটারেটর রিটার্ন করে যা পরিবর্তিত আইটেমগুলো তৈরি করে। এখানকার ক্লোজারটি একটি নতুন ইটারেটর তৈরি করে যেখানে ভেক্টরের প্রতিটি আইটেমের মান ১ করে বাড়ানো হবে।
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
তবে, এই কোডটি একটি সতর্কবার্তা (warning) তৈরি করে:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
Listing 13-14-এর কোডটি কিছুই করে না; আমরা যে ক্লোজারটি নির্দিষ্ট করেছি তা কখনই কল করা হয় না। সতর্কবার্তাটি আমাদের মনে করিয়ে দেয় কেন: ইটারেটর অ্যাডাপ্টারগুলো lazy, এবং আমাদের এখানে ইটারেটরটি ব্যবহার করতে হবে।
এই সতর্কবার্তাটি ঠিক করতে এবং ইটারেটরটি ব্যবহার করতে, আমরা collect
মেথড ব্যবহার করব, যা আমরা Listing 12-1-এ env::args
-এর সাথে ব্যবহার করেছিলাম। এই মেথডটি ইটারেটরকে ব্যবহার করে এবং ফলস্বরূপ মানগুলোকে একটি কালেকশন ডেটা টাইপে সংগ্রহ করে।
Listing 13-15-এ, আমরা map
কল থেকে রিটার্ন করা ইটারেটরের উপর ইটারেট করার ফলাফল একটি ভেক্টরে সংগ্রহ করি। এই ভেক্টরটিতে মূল ভেক্টরের প্রতিটি আইটেম থাকবে, যার মান ১ করে বাড়ানো হয়েছে।
fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
যেহেতু map
একটি ক্লোজার নেয়, তাই আমরা প্রতিটি আইটেমের উপর যেকোনো অপারেশন নির্দিষ্ট করতে পারি। এটি একটি চমৎকার উদাহরণ যে কীভাবে ক্লোজার আপনাকে কিছু আচরণ কাস্টমাইজ করতে দেয় এবং একই সাথে Iterator
ট্রেইট দ্বারা প্রদত্ত ইটারেশন আচরণটি পুনরায় ব্যবহার করতে দেয়।
আপনি একাধিক ইটারেটর অ্যাডাপ্টার কল চেইন করে জটিল কাজগুলো একটি পঠনযোগ্য উপায়ে সম্পাদন করতে পারেন। কিন্তু যেহেতু সমস্ত ইটারেটর lazy, তাই ইটারেটর অ্যাডাপ্টার কল থেকে ফলাফল পেতে আপনাকে কনজিউমিং অ্যাডাপ্টার মেথডগুলোর একটি কল করতে হবে।
এনভায়রনমেন্ট ক্যাপচার করে এমন ক্লোজার ব্যবহার করা
অনেক ইটারেটর অ্যাডাপ্টার আর্গুমেন্ট হিসেবে ক্লোজার নেয়, এবং সাধারণত আমরা ইটারেটর অ্যাডাপ্টারের আর্গুমেন্ট হিসেবে যে ক্লোজারগুলো নির্দিষ্ট করব তা তাদের এনভায়রনমেন্ট ক্যাপচার করে।
এই উদাহরণের জন্য, আমরা filter
মেথড ব্যবহার করব যা একটি ক্লোজার নেয়। ক্লোজারটি ইটারেটর থেকে একটি আইটেম পায় এবং একটি bool
রিটার্ন করে। যদি ক্লোজারটি true
রিটার্ন করে, তবে মানটি filter
দ্বারা উৎপাদিত ইটারেশনে অন্তর্ভুক্ত হবে। যদি ক্লোজারটি false
রিটার্ন করে, তবে মানটি অন্তর্ভুক্ত হবে না।
Listing 13-16-এ, আমরা filter
ব্যবহার করি একটি ক্লোজারের সাথে যা তার এনভায়রনমেন্ট থেকে shoe_size
ভ্যারিয়েবলটি ক্যাপচার করে Shoe
struct ইনস্ট্যান্সের একটি কালেকশনের উপর ইটারেট করার জন্য। এটি শুধুমাত্র নির্দিষ্ট আকারের জুতা রিটার্ন করবে।
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
shoes_in_size
ফাংশনটি প্যারামিটার হিসেবে একটি জুতার ভেক্টরের মালিকানা এবং একটি জুতার সাইজ নেয়। এটি শুধুমাত্র নির্দিষ্ট আকারের জুতা ধারণকারী একটি ভেক্টর রিটার্ন করে।
shoes_in_size
-এর বডিতে, আমরা into_iter
কল করে একটি ইটারেটর তৈরি করি যা ভেক্টরের মালিকানা নেয়। তারপর আমরা filter
কল করে সেই ইটারেটরটিকে একটি নতুন ইটারেটরে অ্যাডাপ্ট করি যা শুধুমাত্র সেই এলিমেন্টগুলো ধারণ করে যার জন্য ক্লোজারটি true
রিটার্ন করে।
ক্লোজারটি এনভায়রনমেন্ট থেকে shoe_size
প্যারামিটারটি ক্যাপচার করে এবং প্রতিটি জুতার আকারের সাথে মানটি তুলনা করে, শুধুমাত্র নির্দিষ্ট আকারের জুতাগুলো রাখে। অবশেষে, collect
কল করা অ্যাডাপ্টেড ইটারেটর দ্বারা রিটার্ন করা মানগুলোকে একটি ভেক্টরে সংগ্রহ করে যা ফাংশন দ্বারা রিটার্ন করা হয়।
টেস্টটি দেখায় যে যখন আমরা shoes_in_size
কল করি, তখন আমরা শুধুমাত্র সেই জুতাগুলো ফেরত পাই যেগুলোর আকার আমাদের নির্দিষ্ট করা মানের সমান।
আমাদের I/O প্রজেক্টের উন্নতি সাধন
ইটারেটর সম্পর্কে আমাদের এই নতুন জ্ঞানের মাধ্যমে, আমরা Chapter 12-এর I/O প্রজেক্টকে উন্নত করতে পারি। ইটারেটর ব্যবহার করে কোডের কিছু অংশ আরও স্পষ্ট এবং সংক্ষিপ্ত করা সম্ভব। আসুন দেখি কীভাবে ইটারেটর আমাদের Config::build
ফাংশন এবং search
ফাংশনের ইমপ্লিমেন্টেশনকে উন্নত করতে পারে।
ইটারেটর ব্যবহার করে clone
সরানো
Listing 12-6-এ, আমরা এমন কোড যোগ করেছিলাম যা String
ভ্যালুর একটি স্লাইস (slice) নিত এবং স্লাইসে ইনডেক্সিং করে ও ভ্যালুগুলো ক্লোন (cloning) করে Config
struct-এর একটি ইনস্ট্যান্স তৈরি করত, যার ফলে Config
struct সেই ভ্যালুগুলোর মালিকানা (own) পেত। Listing 13-17-এ, আমরা Config::build
ফাংশনের ইমপ্লিমেন্টেশনটি পুনরায় তুলে ধরেছি, যা Listing 12-23-এ ছিল।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
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);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
সেই সময়ে, আমরা বলেছিলাম অদক্ষ clone
কলগুলো নিয়ে চিন্তা না করতে, কারণ আমরা ভবিষ্যতে সেগুলো সরিয়ে ফেলব। এখন সেই সময় এসেছে!
এখানে আমাদের clone
প্রয়োজন হয়েছিল কারণ args
প্যারামিটারে আমাদের String
এলিমেন্টসহ একটি স্লাইস ছিল, কিন্তু build
ফাংশন args
-এর মালিক ছিল না। Config
ইনস্ট্যান্সের মালিকানা রিটার্ন করার জন্য, আমাদের Config
-এর query
এবং file_path
ফিল্ডের ভ্যালুগুলো ক্লোন করতে হয়েছিল যাতে Config
ইনস্ট্যান্স তার ভ্যালুগুলোর মালিকানা পেতে পারে।
ইটারেটর সম্পর্কে আমাদের নতুন জ্ঞানের মাধ্যমে, আমরা build
ফাংশনটি পরিবর্তন করে স্লাইস ধার (borrow) করার পরিবর্তে আর্গুমেন্ট হিসেবে একটি ইটারেটরের মালিকানা নিতে পারি। আমরা স্লাইসের দৈর্ঘ্য পরীক্ষা করা এবং নির্দিষ্ট লোকেশনে ইনডেক্স করার কোডের পরিবর্তে ইটারেটরের কার্যকারিতা ব্যবহার করব। এটি Config::build
ফাংশনটি কী করছে তা আরও স্পষ্ট করবে কারণ ইটারেটর ভ্যালুগুলো অ্যাক্সেস করবে।
যখন Config::build
ইটারেটরের মালিকানা নেবে এবং ধার করা ইনডেক্সিং অপারেশন ব্যবহার করা বন্ধ করবে, তখন আমরা clone
কল করে নতুন মেমোরি অ্যালোকেশন করার পরিবর্তে ইটারেটর থেকে String
ভ্যালুগুলো Config
-এ মুভ (move) করতে পারব।
সরাসরি রিটার্ন করা ইটারেটর ব্যবহার করা
আপনার I/O প্রজেক্টের src/main.rs ফাইলটি খুলুন, যা দেখতে এমন হওয়া উচিত:
ফাইলের নাম: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
আমরা প্রথমে main
ফাংশনের শুরুটি পরিবর্তন করব, যা Listing 12-24-এ ছিল। এবার আমরা Listing 13-18-এর কোডটি ব্যবহার করব, যা একটি ইটারেটর ব্যবহার করে। এটি ততক্ষণ পর্যন্ত কম্পাইল হবে না যতক্ষণ না আমরা Config::build
আপডেট করছি।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args
ফাংশনটি একটি ইটারেটর রিটার্ন করে! ইটারেটরের ভ্যালুগুলোকে একটি ভেক্টরে সংগ্রহ করে তারপর Config::build
-এ একটি স্লাইস পাস করার পরিবর্তে, এখন আমরা env::args
থেকে রিটার্ন করা ইটারেটরের মালিকানা সরাসরি Config::build
-কে পাস করছি।
এরপর, আমাদের Config::build
-এর ডেফিনিশন আপডেট করতে হবে। আসুন Config::build
-এর সিগনেচার (signature) পরিবর্তন করে Listing 13-19-এর মতো করি। এটি এখনও কম্পাইল হবে না, কারণ আমাদের ফাংশনের বডি আপডেট করতে হবে।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = 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();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args
ফাংশনের জন্য স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশন দেখায় যে এটি যে ইটারেটর রিটার্ন করে তার টাইপ হলো std::env::Args
, এবং সেই টাইপটি Iterator
ট্রেইট ইমপ্লিমেন্ট করে এবং String
ভ্যালু রিটার্ন করে।
আমরা Config::build
ফাংশনের সিগনেচার আপডেট করেছি যাতে args
প্যারামিটারটির &[String]
-এর পরিবর্তে impl Iterator<Item = String>
ট্রেইট বাউন্ডসহ একটি জেনেরিক টাইপ থাকে। Chapter 10-এর "Traits as Parameters" বিভাগে আলোচনা করা impl Trait
সিনট্যাক্সের এই ব্যবহারটির অর্থ হলো args
যেকোনো টাইপের হতে পারে যা Iterator
ট্রেইট ইমপ্লিমেন্ট করে এবং String
আইটেম রিটার্ন করে।
যেহেতু আমরা args
-এর মালিকানা নিচ্ছি এবং এর উপর ইটারেট করে args
-কে পরিবর্তন (mutate) করব, তাই আমরা args
প্যারামিটারের স্পেসিফিকেশনে mut
কীওয়ার্ড যোগ করে এটিকে মিউটেবল করতে পারি।
ইনডেক্সিং এর পরিবর্তে Iterator
ট্রেইট মেথড ব্যবহার করা
এরপর, আমরা Config::build
-এর বডি ঠিক করব। যেহেতু args
, Iterator
ট্রেইট ইমপ্লিমেন্ট করে, আমরা জানি যে আমরা এর উপর next
মেথড কল করতে পারি! Listing 13-20, Listing 12-23-এর কোডটি next
মেথড ব্যবহার করার জন্য আপডেট করে।
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
মনে রাখবেন env::args
-এর রিটার্ন ভ্যালুর প্রথম মানটি হলো প্রোগ্রামের নাম। আমরা সেটি উপেক্ষা করে পরবর্তী ভ্যালুটি পেতে চাই, তাই প্রথমে আমরা next
কল করি এবং রিটার্ন ভ্যালু নিয়ে কিছুই করি না। তারপর আমরা Config
-এর query
ফিল্ডে যে ভ্যালু রাখতে চাই তা পেতে আবার next
কল করি। যদি next
একটি Some
রিটার্ন করে, আমরা ভ্যালুটি এক্সট্র্যাক্ট করতে একটি match
ব্যবহার করি। যদি এটি None
রিটার্ন করে, তার মানে যথেষ্ট আর্গুমেন্ট দেওয়া হয়নি এবং আমরা একটি Err
ভ্যালু দিয়ে আগেভাগেই রিটার্ন করি। আমরা file_path
ভ্যালুর জন্যও একই কাজ করি।
ইটারেটর অ্যাডাপ্টার দিয়ে কোড আরও স্পষ্ট করা
আমরা আমাদের I/O প্রজেক্টের search
ফাংশনেও ইটারেটরের সুবিধা নিতে পারি, যা এখানে Listing 13-21-এ পুনঃপ্রস্তুত করা হয়েছে যেমনটি Listing 12-19-এ ছিল।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
আমরা এই কোডটি ইটারেটর অ্যাডাপ্টার মেথড ব্যবহার করে আরও সংক্ষিপ্তভাবে লিখতে পারি। এটি করলে আমরা একটি মিউটেবল অন্তর্বর্তী results
ভেক্টর এড়াতে পারি। ফাংশনাল প্রোগ্রামিং স্টাইল কোডকে আরও স্পষ্ট করার জন্য মিউটেবল স্টেট (mutable state) কমানো পছন্দ করে। মিউটেবল স্টেট অপসারণ ভবিষ্যতে সমান্তরালভাবে সার্চিং করার জন্য একটি enhancement সক্ষম করতে পারে কারণ আমাদের results
ভেক্টরের কনকারেন্ট অ্যাক্সেস পরিচালনা করতে হবে না। Listing 13-22 এই পরিবর্তনটি দেখায়।
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
স্মরণ করুন যে search
ফাংশনের উদ্দেশ্য হলো contents
-এর মধ্যে query
ধারণকারী সমস্ত লাইন রিটার্ন করা। Listing 13-16-এর filter
উদাহরণের মতো, এই কোডটি filter
অ্যাডাপ্টার ব্যবহার করে শুধুমাত্র সেই লাইনগুলো রাখে যার জন্য line.contains(query)
true
রিটার্ন করে। তারপর আমরা collect
দিয়ে মিলে যাওয়া লাইনগুলোকে অন্য একটি ভেক্টরে সংগ্রহ করি। অনেক সহজ! search_case_insensitive
ফাংশনেও ইটারেটর মেথড ব্যবহার করার জন্য নির্দ্বিধায় একই পরিবর্তন করুন।
আরও উন্নতির জন্য, collect
কলটি সরিয়ে দিয়ে এবং রিটার্ন টাইপ পরিবর্তন করে impl Iterator<Item = &'a str>
করে search
ফাংশন থেকে একটি ইটারেটর রিটার্ন করুন যাতে ফাংশনটি একটি ইটারেটর অ্যাডাপ্টার হয়ে যায়। লক্ষ্য করুন যে আপনাকে টেস্টগুলোও আপডেট করতে হবে! এই পরিবর্তন করার আগে এবং পরে আপনার minigrep
টুল ব্যবহার করে একটি বড় ফাইল সার্চ করে আচরণের পার্থক্য পর্যবেক্ষণ করুন। এই পরিবর্তনের আগে, প্রোগ্রামটি সমস্ত ফলাফল সংগ্রহ না করা পর্যন্ত কোনো ফলাফল প্রিন্ট করবে না, কিন্তু পরিবর্তনের পরে, প্রতিটি মিলে যাওয়া লাইন খুঁজে পাওয়ার সাথে সাথে ফলাফলগুলো প্রিন্ট হবে কারণ run
ফাংশনের for
লুপ ইটারেটরের অলসতার (laziness) সুবিধা নিতে সক্ষম।
লুপ এবং ইটারেটরের মধ্যে একটি বেছে নেওয়া
পরবর্তী যৌক্তিক প্রশ্ন হলো আপনার নিজের কোডে কোন স্টাইলটি বেছে নেওয়া উচিত এবং কেন: Listing 13-21-এর মূল ইমপ্লিমেন্টেশন নাকি Listing 13-22-এর ইটারেটর ব্যবহার করা সংস্করণটি (ধরে নিচ্ছি আমরা ইটারেটর রিটার্ন না করে সমস্ত ফলাফল সংগ্রহ করছি)। বেশিরভাগ রাস্ট প্রোগ্রামার ইটারেটর স্টাইল ব্যবহার করতে পছন্দ করেন। এটি প্রথমে আয়ত্ত করা কিছুটা কঠিন, কিন্তু একবার আপনি বিভিন্ন ইটারেটর অ্যাডাপ্টার এবং সেগুলো কী করে সে সম্পর্কে ধারণা পেয়ে গেলে, ইটারেটর বোঝা সহজ হতে পারে। লুপের বিভিন্ন অংশ এবং নতুন ভেক্টর তৈরি করার ঝামেলার পরিবর্তে, কোডটি লুপের উচ্চ-স্তরের উদ্দেশ্যের উপর মনোযোগ দেয়। এটি কিছু সাধারণ কোডকে অ্যাবস্ট্রাক্ট করে দেয় যাতে এই কোডের জন্য অনন্য ধারণাগুলো, যেমন ইটারেটরের প্রতিটি এলিমেন্টকে যে ফিল্টারিং শর্তটি পাস করতে হবে, তা দেখা সহজ হয়।
কিন্তু দুটি ইমপ্লিমেন্টেশন কি সত্যিই সমতুল্য? স্বতঃস্ফূর্ত ধারণা হতে পারে যে নিম্ন-স্তরের (lower-level) লুপটি দ্রুততর হবে। আসুন পারফরম্যান্স নিয়ে কথা বলি।
পারফরম্যান্সের তুলনা: লুপ বনাম ইটারেটর
লুপ নাকি ইটারেটর ব্যবহার করবেন, তা নির্ধারণ করার জন্য আপনাকে জানতে হবে কোন ইমপ্লিমেন্টেশনটি দ্রুততর: সুস্পষ্ট for
লুপসহ search
ফাংশনের সংস্করণটি নাকি ইটারেটরসহ সংস্করণটি।
আমরা স্যার আর্থার কোনান ডয়েলের লেখা দ্য অ্যাডভেঞ্চারস অফ শার্লক হোমস এর সম্পূর্ণ বিষয়বস্তু একটি String
-এ লোড করে এবং বিষয়বস্তুর মধ্যে the শব্দটি খুঁজে একটি বেঞ্চমার্ক (benchmark) চালিয়েছি। এখানে for
লুপ ব্যবহার করে search
ফাংশনের সংস্করণ এবং ইটারেটর ব্যবহার করে সংস্করণটির বেঞ্চমার্কের ফলাফল দেওয়া হলো:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
দুটি ইমপ্লিমেন্টেশনের পারফরম্যান্স প্রায় একই! আমরা এখানে বেঞ্চমার্ক কোডটি ব্যাখ্যা করব না কারণ মূল উদ্দেশ্য দুটি সংস্করণ যে সমতুল্য তা প্রমাণ করা নয়, বরং এই দুটি ইমপ্লিমেন্টেশন পারফরম্যান্সের দিক থেকে কীভাবে তুলনা করা হয় সে সম্পর্কে একটি সাধারণ ধারণা দেওয়া।
আরও বিস্তারিত বেঞ্চমার্কের জন্য, আপনার বিভিন্ন আকারের টেক্সটকে contents
হিসেবে, বিভিন্ন শব্দ এবং বিভিন্ন দৈর্ঘ্যের শব্দকে query
হিসেবে, এবং অন্যান্য সব ধরনের ভিন্নতা ব্যবহার করে পরীক্ষা করা উচিত। মূল কথা হলো: ইটারেটর, যদিও একটি উচ্চ-স্তরের (high-level) অ্যাবস্ট্র্যাকশন, সেগুলো প্রায় একই কোডে কম্পাইল হয় যা আপনি নিজে থেকে নিম্ন-স্তরের (lower-level) কোড লিখলে হতো। ইটারেটর হলো রাস্টের একটি জিরো-কস্ট অ্যাবস্ট্র্যাকশন (zero-cost abstractions), যার মাধ্যমে আমরা বোঝাই যে অ্যাবস্ট্র্যাকশন ব্যবহার করলে কোনো অতিরিক্ত রানটাইম ওভারহেড (runtime overhead) হয় না। এটি C++ এর মূল ডিজাইনার এবং ইমপ্লিমেন্টার Bjarne Stroustrup যেভাবে "Foundations of C++" (2012) বইতে জিরো-ওভারহেড (zero-overhead) সংজ্ঞায়িত করেছেন তার অনুরূপ:
সাধারণভাবে, C++ ইমপ্লিমেন্টেশনগুলো জিরো-ওভারহেড নীতি মেনে চলে: যা আপনি ব্যবহার করেন না, তার জন্য আপনাকে মূল্য দিতে হয় না। এবং আরও: যা আপনি ব্যবহার করেন, তা আপনি হাতে লিখে এর চেয়ে ভালো কোড তৈরি করতে পারতেন না।
অনেক ক্ষেত্রে, ইটারেটর ব্যবহার করে লেখা রাস্ট কোড সেই একই অ্যাসেম্বলি কোডে কম্পাইল হয় যা আপনি হাতে লিখতেন। লুপ আনরোলিং (loop unrolling) এবং অ্যারে অ্যাক্সেসের ক্ষেত্রে বাউন্ডস চেকিং (bounds checking) বাদ দেওয়ার মতো অপ্টিমাইজেশনগুলো প্রয়োগ হয় এবং ফলস্বরূপ কোডটিকে অত্যন্ত দক্ষ করে তোলে। এখন যেহেতু আপনি এটি জানেন, আপনি ভয় ছাড়াই ইটারেটর এবং ক্লোজার ব্যবহার করতে পারেন! এগুলি কোডকে উচ্চ-স্তরের বলে মনে করায়, কিন্তু তা করার জন্য কোনো রানটাইম পারফরম্যান্স পেনাল্টি আরোপ করে না।
সারসংক্ষেপ
ক্লোজার এবং ইটারেটর হলো ফাংশনাল প্রোগ্রামিং ভাষার ধারণা দ্বারা অনুপ্রাণিত রাস্টের বৈশিষ্ট্য। এগুলো রাস্টের উচ্চ-স্তরের ধারণাগুলোকে নিম্ন-স্তরের পারফরম্যান্সে স্পষ্টভাবে প্রকাশ করার ক্ষমতায় অবদান রাখে। ক্লোজার এবং ইটারেটরের ইমপ্লিমেন্টেশন এমনভাবে করা হয়েছে যাতে রানটাইম পারফরম্যান্স প্রভাবিত না হয়। এটি রাস্টের জিরো-কস্ট অ্যাবস্ট্র্যাকশন প্রদানের লক্ষ্যের একটি অংশ।
এখন যেহেতু আমরা আমাদের I/O প্রজেক্টের প্রকাশক্ষমতা উন্নত করেছি, আসুন cargo
-এর আরও কিছু বৈশিষ্ট্য দেখি যা আমাদের প্রজেক্টটি বিশ্বের সাথে শেয়ার করতে সাহায্য করবে।
কার্গো এবং Crates.io সম্পর্কে আরও তথ্য
এখন পর্যন্ত, আমরা আমাদের কোড বিল্ড, রান এবং টেস্ট করার জন্য কার্গোর শুধুমাত্র সবচেয়ে প্রাথমিক ফিচারগুলো ব্যবহার করেছি, কিন্তু এটি আরও অনেক কিছু করতে পারে। এই অধ্যায়ে, আমরা এর কিছু অ্যাডভান্সড ফিচার নিয়ে আলোচনা করব যা আপনাকে নিম্নলিখিত কাজগুলো করতে সাহায্য করবে:
- রিলিজ প্রোফাইলের মাধ্যমে আপনার বিল্ড কাস্টমাইজ করা
- crates.io-তে লাইব্রেরি পাবলিশ করা
- ওয়ার্কস্পেস দিয়ে বড় প্রজেক্ট সাজানো
- crates.io-থেকে বাইনারি ইনস্টল করা
- কাস্টম কমান্ড ব্যবহার করে কার্গোকে এক্সটেন্ড করা
এই অধ্যায়ে আমরা যে কার্যকারিতাগুলো তুলে ধরব, কার্গো তার থেকেও বেশি কিছু করতে পারে, তাই এর সমস্ত ফিচারের সম্পূর্ণ বিবরণের জন্য, এর ডকুমেন্টেশন দেখুন।
রিলিজ প্রোফাইলের মাধ্যমে বিল্ড কাস্টমাইজ করা
Rust-এ, রিলিজ প্রোফাইল (release profiles) হলো পূর্ব-নির্ধারিত এবং কাস্টমাইজযোগ্য প্রোফাইল যেখানে বিভিন্ন কনফিগারেশন থাকে। এটি একজন প্রোগ্রামারকে কোড কম্পাইল করার বিভিন্ন অপশনের উপর আরও বেশি নিয়ন্ত্রণ দেয়। প্রতিটি প্রোফাইল অন্যগুলো থেকে স্বাধীনভাবে কনফিগার করা হয়।
কার্গোর দুটি প্রধান প্রোফাইল আছে: dev
প্রোফাইল, যা cargo build
কমান্ড চালালে ব্যবহৃত হয়, এবং release
প্রোফাইল, যা cargo build --release
কমান্ড চালালে ব্যবহৃত হয়। dev
প্রোফাইলটি ডেভেলপমেন্টের জন্য উপযুক্ত ডিফল্ট সেটিংস দিয়ে তৈরি, এবং release
প্রোফাইলে রিলিজ বিল্ডের জন্য উপযুক্ত ডিফল্ট সেটিংস থাকে।
এই প্রোফাইলের নামগুলো আপনার বিল্ডের আউটপুট থেকে পরিচিত হতে পারে:
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s
dev
এবং release
এই ভিন্ন প্রোফাইলগুলো কম্পাইলার ব্যবহার করে।
কার্গোর প্রতিটি প্রোফাইলের জন্য ডিফল্ট সেটিংস রয়েছে, যা আপনার প্রজেক্টের Cargo.toml ফাইলে [profile.*]
সেকশন যোগ না করা পর্যন্ত প্রযোজ্য থাকে। আপনি যে প্রোফাইলটি কাস্টমাইজ করতে চান, তার জন্য [profile.*]
সেকশন যোগ করে ডিফল্ট সেটিংসের যেকোনো অংশকে ওভাররাইড করতে পারেন। উদাহরণস্বরূপ, এখানে dev
এবং release
প্রোফাইলের জন্য opt-level
সেটিংয়ের ডিফল্ট ভ্যালুগুলো দেওয়া হলো:
ফাইলের নাম: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
সেটিংটি আপনার কোডে Rust কতগুলো অপটিমাইজেশন প্রয়োগ করবে তা নিয়ন্ত্রণ করে, যার মান ০ থেকে ৩ পর্যন্ত হতে পারে। বেশি অপটিমাইজেশন প্রয়োগ করলে কম্পাইলিংয়ের সময় বেড়ে যায়, তাই ডেভেলপমেন্টের সময় যখন আপনি ঘন ঘন কোড কম্পাইল করেন, তখন দ্রুত কম্পাইল করার জন্য কম অপটিমাইজেশন দরকার হয়, যদিও এর ফলে তৈরি হওয়া কোড কিছুটা ধীরগতিতে চলে। একারণে, dev
প্রোফাইলের জন্য ডিফল্ট opt-level
হলো 0
। যখন আপনি আপনার কোড রিলিজ করার জন্য প্রস্তুত হবেন, তখন কম্পাইল করতে বেশি সময় ব্যয় করা ভালো। আপনি রিলিজ মোডে শুধু একবার কম্পাইল করবেন, কিন্তু কম্পাইল করা প্রোগ্রামটি অনেকবার চালাবেন, তাই রিলিজ মোড দ্রুতগতির কোডের জন্য দীর্ঘ কম্পাইল সময় বেছে নেয়। এ কারণেই release
প্রোফাইলের ডিফল্ট opt-level
হলো 3
।
আপনি Cargo.toml ফাইলে কোনো ডিফল্ট সেটিংয়ের জন্য ভিন্ন ভ্যালু যোগ করে সেটিকে ওভাররাইড করতে পারেন। উদাহরণস্বরূপ, আমরা যদি ডেভেলপমেন্ট প্রোফাইলে অপটিমাইজেশন লেভেল 1 ব্যবহার করতে চাই, তাহলে আমরা আমাদের প্রজেক্টের Cargo.toml ফাইলে এই দুটি লাইন যোগ করতে পারি:
ফাইলের নাম: Cargo.toml
[profile.dev]
opt-level = 1
এই কোডটি 0
-এর ডিফল্ট সেটিংকে ওভাররাইড করে। এখন আমরা যখন cargo build
চালাব, কার্গো dev
প্রোফাইলের ডিফল্ট সেটিংসের সাথে আমাদের কাস্টমাইজ করা opt-level
ব্যবহার করবে। যেহেতু আমরা opt-level
কে 1
সেট করেছি, কার্গো ডিফল্টের চেয়ে বেশি অপটিমাইজেশন প্রয়োগ করবে, কিন্তু রিলিজ বিল্ডের মতো অত বেশি নয়।
প্রতিটি প্রোফাইলের জন্য কনফিগারেশন অপশন এবং ডিফল্টগুলোর সম্পূর্ণ তালিকার জন্য, কার্গোর ডকুমেন্টেশন দেখুন।
Crates.io-তে একটি ক্রেট পাবলিশ করা
আমরা আমাদের প্রজেক্টের ডিপেন্ডেন্সি হিসেবে crates.io-থেকে প্যাকেজ ব্যবহার করেছি, কিন্তু আপনি আপনার নিজের প্যাকেজ পাবলিশ করে অন্যদের সাথে আপনার কোড শেয়ার করতে পারেন। crates.io-এর ক্রেট রেজিস্ট্রি আপনার প্যাকেজের সোর্স কোড বিতরণ করে, তাই এটি মূলত ওপেন সোর্স কোড হোস্ট করে।
Rust এবং কার্গোর এমন কিছু ফিচার রয়েছে যা আপনার পাবলিশ করা প্যাকেজকে মানুষের খুঁজে পেতে এবং ব্যবহার করতে সহজ করে তোলে। আমরা এই ফিচারগুলোর কয়েকটি নিয়ে আলোচনা করব এবং তারপরে একটি প্যাকেজ কীভাবে পাবলিশ করতে হয় তা ব্যাখ্যা করব।
দরকারি ডকুমেন্টেশন কমেন্ট তৈরি করা
আপনার প্যাকেজগুলোকে সঠিকভাবে ডকুমেন্টেশন করা হলে অন্য ব্যবহারকারীরা জানতে পারবে কীভাবে এবং কখন সেগুলো ব্যবহার করতে হবে, তাই ডকুমেন্টেশন লেখার জন্য সময় ব্যয় করা সার্থক। অধ্যায় ৩-এ, আমরা দুটি স্ল্যাশ, //
ব্যবহার করে কীভাবে Rust কোডে কমেন্ট করতে হয় তা আলোচনা করেছি। Rust-এর ডকুমেন্টেশনের জন্য একটি বিশেষ ধরনের কমেন্টও রয়েছে, যা সুবিধাজনকভাবে ডকুমেন্টেশন কমেন্ট (documentation comment) নামে পরিচিত, যা HTML ডকুমেন্টেশন তৈরি করবে। HTML ডকুমেন্টেশন পাবলিক API আইটেমগুলোর জন্য ডকুমেন্টেশন কমেন্টের বিষয়বস্তু প্রদর্শন করে, যা সেইসব প্রোগ্রামারদের জন্য তৈরি, যারা আপনার ক্রেট কীভাবে বাস্তবায়ন করা হয়েছে তার চেয়ে কীভাবে ব্যবহার করতে হয় তা জানতে আগ্রহী।
ডকুমেন্টেশন কমেন্ট দুটির পরিবর্তে তিনটি স্ল্যাশ, ///
ব্যবহার করে এবং টেক্সট ফরম্যাট করার জন্য Markdown নোটেশন সমর্থন করে। যে আইটেমটি ডকুমেন্ট করা হচ্ছে তার ঠিক আগে ডকুমেন্টেশন কমেন্ট রাখুন। তালিকা ১৪-১-এ my_crate
নামের একটি ক্রেটে add_one
ফাংশনের জন্য ডকুমেন্টেশন কমেন্ট দেখানো হয়েছে।
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
এখানে, আমরা add_one
ফাংশনটি কী করে তার একটি বিবরণ দিচ্ছি, Examples
শিরোনাম দিয়ে একটি সেকশন শুরু করছি, এবং তারপর add_one
ফাংশনটি কীভাবে ব্যবহার করতে হয় তা প্রদর্শন করে এমন কোড প্রদান করছি। আমরা cargo doc
চালিয়ে এই ডকুমেন্টেশন কমেন্ট থেকে HTML ডকুমেন্টেশন তৈরি করতে পারি। এই কমান্ডটি Rust-এর সাথে ডিস্ট্রিবিউট করা rustdoc
টুলটি চালায় এবং জেনারেট করা HTML ডকুমেন্টেশনটি target/doc ডিরেক্টরিতে রাখে।
সুবিধার জন্য, cargo doc --open
চালালে আপনার বর্তমান ক্রেটের ডকুমেন্টেশনের জন্য HTML তৈরি হবে (পাশাপাশি আপনার ক্রেটের সমস্ত ডিপেন্ডেন্সির ডকুমেন্টেশনও) এবং ফলাফলটি একটি ওয়েব ব্রাউজারে খুলবে। add_one
ফাংশনে নেভিগেট করুন এবং আপনি দেখতে পাবেন ডকুমেন্টেশন কমেন্টের টেক্সট কীভাবে রেন্ডার করা হয়েছে, যেমনটি চিত্র ১৪-১-এ দেখানো হয়েছে।

চিত্র ১৪-১: add_one
ফাংশনের জন্য HTML ডকুমেন্টেশন
সাধারণত ব্যবহৃত সেকশন
আমরা তালিকা ১৪-১-এ # Examples
Markdown শিরোনাম ব্যবহার করে HTML-এ "Examples" শিরোনাম সহ একটি সেকশন তৈরি করেছি। এখানে আরও কিছু সেকশন রয়েছে যা ক্রেট লেখকরা সাধারণত তাদের ডকুমেন্টেশনে ব্যবহার করেন:
- Panics: যে পরিস্থিতিতে ডকুমেন্ট করা ফাংশনটি প্যানিক করতে পারে। ফাংশনের কলাররা যারা চান না তাদের প্রোগ্রাম প্যানিক করুক, তাদের নিশ্চিত করা উচিত যে তারা এই পরিস্থিতিতে ফাংশনটি কল না করে।
- Errors: যদি ফাংশনটি একটি
Result
রিটার্ন করে, তাহলে কী ধরনের এরর ঘটতে পারে এবং কোন শর্তে সেই এররগুলো রিটার্ন হতে পারে তা বর্ণনা করা কলারদের জন্য সহায়ক হতে পারে, যাতে তারা বিভিন্ন ধরণের এরর বিভিন্ন উপায়ে হ্যান্ডেল করার জন্য কোড লিখতে পারে। - Safety: যদি ফাংশনটি কল করা
unsafe
হয় (আমরা অধ্যায় ২০-এ unsafe নিয়ে আলোচনা করব), তবে একটি সেকশন থাকা উচিত যা ব্যাখ্যা করে কেন ফাংশনটি unsafe এবং ফাংশনটি কলারদের কাছ থেকে কী কী ইনভ্যারিয়েন্ট আশা করে।
বেশিরভাগ ডকুমেন্টেশন কমেন্টের জন্য এই সমস্ত সেকশনের প্রয়োজন হয় না, তবে এটি একটি ভালো চেকলিস্ট যা আপনাকে আপনার কোডের সেই দিকগুলো মনে করিয়ে দেবে যা ব্যবহারকারীরা জানতে আগ্রহী হবে।
ডকুমেন্টেশন কমেন্টকে টেস্ট হিসাবে ব্যবহার
আপনার ডকুমেন্টেশন কমেন্টে উদাহরণ কোড ব্লক যোগ করা আপনার লাইব্রেরি কীভাবে ব্যবহার করতে হয় তা দেখাতে সাহায্য করতে পারে, এবং এটি করার একটি অতিরিক্ত বোনাস রয়েছে: cargo test
চালালে আপনার ডকুমেন্টেশনের কোড উদাহরণগুলো টেস্ট হিসাবে চলবে! উদাহরণের সাথে ডকুমেন্টেশনের চেয়ে ভালো আর কিছু নেই। কিন্তু এমন উদাহরণের চেয়ে খারাপ আর কিছু নেই যা কাজ করে না কারণ ডকুমেন্টেশন লেখার পর কোড পরিবর্তন হয়েছে। যদি আমরা তালিকা ১৪-১ থেকে add_one
ফাংশনের ডকুমেন্টেশন সহ cargo test
চালাই, আমরা টেস্ট ফলাফলে একটি সেকশন দেখতে পাব যা এইরকম দেখায়:
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
এখন, যদি আমরা ফাংশন বা উদাহরণটি এমনভাবে পরিবর্তন করি যাতে উদাহরণের assert_eq!
প্যানিক করে, এবং আবার cargo test
চালাই, আমরা দেখব যে ডক টেস্টগুলো ধরে ফেলেছে যে উদাহরণ এবং কোড একে অপরের সাথে সিঙ্কে নেই!
কন্টেইনড আইটেম কমেন্টিং
ডক কমেন্টের //!
স্টাইলটি কমেন্টের পরবর্তী আইটেমের পরিবর্তে যে আইটেমটি কমেন্টগুলো ধারণ করে তার ডকুমেন্টেশন যোগ করে। আমরা সাধারণত এই ডক কমেন্টগুলো ক্রেট রুট ফাইলে (প্রচলিতভাবে src/lib.rs) বা একটি মডিউলের ভিতরে ব্যবহার করি যাতে ক্রেট বা মডিউলটিকে সামগ্রিকভাবে ডকুমেন্ট করা যায়।
উদাহরণস্বরূপ, my_crate
ক্রেট, যা add_one
ফাংশনটি ধারণ করে, তার উদ্দেশ্য বর্ণনা করে এমন ডকুমেন্টেশন যোগ করার জন্য, আমরা //!
দিয়ে শুরু হওয়া ডকুমেন্টেশন কমেন্টগুলো src/lib.rs ফাইলের শুরুতে যোগ করি, যেমনটি তালিকা ১৪-২-এ দেখানো হয়েছে।
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
লক্ষ্য করুন, //!
দিয়ে শুরু হওয়া শেষ লাইনের পরে কোনো কোড নেই। যেহেতু আমরা কমেন্টগুলো ///
-এর পরিবর্তে //!
দিয়ে শুরু করেছি, আমরা এই কমেন্টের পরবর্তী আইটেমের পরিবর্তে এই কমেন্ট ধারণকারী আইটেমটিকে ডকুমেন্ট করছি। এই ক্ষেত্রে, সেই আইটেমটি হল src/lib.rs ফাইল, যা ক্রেট রুট। এই কমেন্টগুলো পুরো ক্রেটকে বর্ণনা করে।
যখন আমরা cargo doc --open
চালাই, তখন এই কমেন্টগুলো my_crate
-এর ডকুমেন্টেশনের প্রথম পৃষ্ঠায়, ক্রেটের পাবলিক আইটেমের তালিকার উপরে প্রদর্শিত হবে, যেমনটি চিত্র ১৪-২-এ দেখানো হয়েছে।

চিত্র ১৪-২: my_crate
-এর জন্য রেন্ডার করা ডকুমেন্টেশন, যা ক্রেটকে বর্ণনা করা সামগ্রিক কমেন্ট অন্তর্ভুক্ত করে
আইটেমের মধ্যে ডকুমেন্টেশন কমেন্ট ক্রেট এবং মডিউল বর্ণনা করার জন্য বিশেষভাবে কার্যকর। আপনার ব্যবহারকারীদের ক্রেটের অর্গানাইজেশন বুঝতে সাহায্য করার জন্য কন্টেইনারের সামগ্রিক উদ্দেশ্য ব্যাখ্যা করতে এগুলো ব্যবহার করুন।
pub use
দিয়ে একটি সুবিধাজনক পাবলিক API এক্সপোর্ট করা
একটি ক্রেট পাবলিশ করার সময় আপনার পাবলিক API-এর গঠন একটি প্রধান বিবেচ্য বিষয়। যারা আপনার ক্রেট ব্যবহার করে তারা আপনার চেয়ে এর গঠনের সাথে কম পরিচিত এবং যদি আপনার ক্রেটের একটি বড় মডিউল হায়ারার্কি থাকে তবে তারা যে অংশগুলো ব্যবহার করতে চায় তা খুঁজে পেতে অসুবিধা হতে পারে।
অধ্যায় ৭-এ, আমরা pub
কীওয়ার্ড ব্যবহার করে কীভাবে আইটেম পাবলিক করতে হয় এবং use
কীওয়ার্ড দিয়ে কীভাবে স্কোপে আইটেম আনতে হয় তা আলোচনা করেছি। যাইহোক, আপনি যখন একটি ক্রেট ডেভেলপ করছেন তখন যে গঠনটি আপনার কাছে যৌক্তিক মনে হতে পারে, তা আপনার ব্যবহারকারীদের জন্য খুব সুবিধাজনক নাও হতে পারে। আপনি আপনার struct-গুলোকে একাধিক স্তরের একটি হায়ারার্কিতে সাজাতে চাইতে পারেন, কিন্তু তারপর যারা হায়ারার্কির গভীরে আপনার সংজ্ঞায়িত একটি টাইপ ব্যবহার করতে চায় তাদের জন্য সেই টাইপটি যে বিদ্যমান তা খুঁজে বের করা কঠিন হতে পারে। use my_crate::UsefulType;
-এর পরিবর্তে use my_crate::some_module::another_module::UsefulType;
লিখতে বাধ্য হওয়ায় তারা বিরক্তও হতে পারে।
ভালো খবর হল যে যদি গঠনটি অন্য লাইব্রেরি থেকে ব্যবহারের জন্য সুবিধাজনক না হয়, তবে আপনাকে আপনার অভ্যন্তরীণ অর্গানাইজেশন পুনর্বিন্যাস করতে হবে না: পরিবর্তে, আপনি pub use
ব্যবহার করে আপনার ব্যক্তিগত গঠন থেকে ভিন্ন একটি পাবলিক গঠন তৈরি করতে আইটেমগুলো রি-এক্সপোর্ট করতে পারেন। রি-এক্সপোর্ট করা (Re-exporting) একটি স্থানের পাবলিক আইটেমকে নিয়ে অন্য একটি স্থানে পাবলিক করে, যেন এটি সেই অন্য স্থানে সংজ্ঞায়িত করা হয়েছিল।
উদাহরণস্বরূপ, ধরা যাক আমরা শৈল্পিক ধারণা মডেল করার জন্য art
নামে একটি লাইব্রেরি তৈরি করেছি। এই লাইব্রেরির মধ্যে দুটি মডিউল রয়েছে: একটি kinds
মডিউল যাতে PrimaryColor
এবং SecondaryColor
নামে দুটি enum রয়েছে এবং একটি utils
মডিউল যাতে mix
নামে একটি ফাংশন রয়েছে, যেমনটি তালিকা ১৪-৩-এ দেখানো হয়েছে।
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
unimplemented!();
}
}
চিত্র ১৪-৩ দেখায় যে cargo doc
দ্বারা তৈরি এই ক্রেটের ডকুমেন্টেশনের প্রথম পাতাটি কেমন দেখাবে।

চিত্র ১৪-৩: art
-এর ডকুমেন্টেশনের প্রথম পাতা যা kinds
এবং utils
মডিউল তালিকাভুক্ত করে
লক্ষ্য করুন যে PrimaryColor
এবং SecondaryColor
টাইপগুলো প্রথম পৃষ্ঠায় তালিকাভুক্ত নয়, mix
ফাংশনটিও নয়। তাদের দেখতে আমাদের kinds
এবং utils
এ ক্লিক করতে হবে।
এই লাইব্রেরির উপর নির্ভরশীল অন্য একটি ক্রেটের use
স্টেটমেন্টের প্রয়োজন হবে যা art
থেকে আইটেমগুলোকে স্কোপে আনবে, বর্তমানে সংজ্ঞায়িত মডিউল কাঠামো উল্লেখ করে। তালিকা ১৪-৪ এমন একটি ক্রেটের উদাহরণ দেখায় যা art
ক্রেট থেকে PrimaryColor
এবং mix
আইটেম ব্যবহার করে।
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
তালিকা ১৪-৪ এর কোডের লেখক, যিনি art
ক্রেট ব্যবহার করছেন, তাকে বের করতে হয়েছে যে PrimaryColor
kinds
মডিউলে এবং mix
utils
মডিউলে রয়েছে। art
ক্রেটের মডিউল কাঠামো art
ক্রেটে কাজ করা ডেভেলপারদের জন্য বেশি প্রাসঙ্গিক, যারা এটি ব্যবহার করছে তাদের চেয়ে। অভ্যন্তরীণ কাঠামোটি art
ক্রেট কীভাবে ব্যবহার করতে হয় তা বোঝার চেষ্টা করা কারো জন্য কোনো দরকারী তথ্য ধারণ করে না, বরং বিভ্রান্তি সৃষ্টি করে কারণ এটি ব্যবহারকারী ডেভেলপারদের কোথায় খুঁজতে হবে তা বের করতে হয় এবং use
স্টেটমেন্টে মডিউলের নাম উল্লেখ করতে হয়।
পাবলিক API থেকে অভ্যন্তরীণ অর্গানাইজেশন অপসারণ করতে, আমরা তালিকা ১৪-৩-এর art
ক্রেট কোডটি পরিবর্তন করে pub use
স্টেটমেন্ট যোগ করতে পারি যাতে আইটেমগুলো টপ লেভেলে রি-এক্সপোর্ট করা যায়, যেমনটি তালিকা ১৪-৫-এ দেখানো হয়েছে।
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
// --snip--
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Orange
}
}
এই ক্রেটের জন্য cargo doc
যে API ডকুমেন্টেশন তৈরি করবে তা এখন প্রথম পৃষ্ঠায় রি-এক্সপোর্টগুলো তালিকাভুক্ত করবে এবং লিঙ্ক করবে, যেমনটি চিত্র ১৪-৪-এ দেখানো হয়েছে, যা PrimaryColor
ও SecondaryColor
টাইপ এবং mix
ফাংশনটিকে খুঁজে পাওয়া সহজ করে তোলে।

চিত্র ১৪-৪: art
-এর ডকুমেন্টেশনের প্রথম পাতা যা রি-এক্সপোর্টগুলো তালিকাভুক্ত করে
art
ক্রেট ব্যবহারকারীরা এখনও তালিকা ১৪-৩ থেকে অভ্যন্তরীণ কাঠামো দেখতে এবং ব্যবহার করতে পারেন যেমনটি তালিকা ১৪-৪-এ দেখানো হয়েছে, অথবা তারা তালিকা ১৪-৫-এর আরও সুবিধাজনক কাঠামো ব্যবহার করতে পারেন, যেমনটি তালিকা ১৪-৬-এ দেখানো হয়েছে।
use art::PrimaryColor;
use art::mix;
fn main() {
// --snip--
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
যেখানে অনেকগুলো নেস্টেড মডিউল রয়েছে, সেখানে pub use
দিয়ে টপ লেভেলে টাইপগুলো রি-এক্সপোর্ট করা ক্রেট ব্যবহারকারীদের অভিজ্ঞতায় একটি উল্লেখযোগ্য পার্থক্য আনতে পারে। pub use
-এর আরেকটি সাধারণ ব্যবহার হল বর্তমান ক্রেটে একটি ডিপেনডেন্সির ডেফিনিশন রি-এক্সপোর্ট করা যাতে সেই ক্রেটের ডেফিনিশনগুলো আপনার ক্রেটের পাবলিক API-এর অংশ হয়ে যায়।
একটি দরকারি পাবলিক API কাঠামো তৈরি করা বিজ্ঞানের চেয়ে বেশি শিল্প, এবং আপনি আপনার ব্যবহারকারীদের জন্য সবচেয়ে ভালো কাজ করে এমন API খুঁজে বের করার জন্য পুনরাবৃত্তি করতে পারেন। pub use
বেছে নেওয়া আপনাকে আপনার ক্রেট অভ্যন্তরীণভাবে কীভাবে গঠন করবেন সে সম্পর্কে নমনীয়তা দেয় এবং সেই অভ্যন্তরীণ কাঠামোকে আপনি আপনার ব্যবহারকারীদের কাছে যা উপস্থাপন করেন তা থেকে বিচ্ছিন্ন করে। আপনি ইনস্টল করেছেন এমন কিছু ক্রেটের কোড দেখুন যে তাদের অভ্যন্তরীণ কাঠামো তাদের পাবলিক API থেকে ভিন্ন কিনা।
Crates.io অ্যাকাউন্ট সেট আপ করা
আপনি কোনো ক্রেট পাবলিশ করার আগে, আপনাকে crates.io-তে একটি অ্যাকাউন্ট তৈরি করতে হবে এবং একটি API টোকেন পেতে হবে। এটি করার জন্য, crates.io-এর হোম পেজে যান এবং একটি GitHub অ্যাকাউন্টের মাধ্যমে লগ ইন করুন। (বর্তমানে GitHub অ্যাকাউন্ট একটি আবশ্যকতা, তবে ভবিষ্যতে সাইটটি অ্যাকাউন্ট তৈরির অন্যান্য উপায় সমর্থন করতে পারে।) একবার আপনি লগ ইন করলে, https://crates.io/me/-এ আপনার অ্যাকাউন্ট সেটিংসে যান এবং আপনার API কী পুনরুদ্ধার করুন। তারপর cargo login
কমান্ডটি চালান এবং অনুরোধ করা হলে আপনার API কী পেস্ট করুন, এরকম:
$ cargo login
abcdefghijklmnopqrstuvwxyz012345
এই কমান্ডটি কার্গোকে আপনার API টোকেন সম্পর্কে জানাবে এবং এটি স্থানীয়ভাবে ~/.cargo/credentials.toml-এ সংরক্ষণ করবে। মনে রাখবেন যে এই টোকেনটি একটি গোপনীয় বিষয়: এটি অন্য কারো সাথে শেয়ার করবেন না। যদি আপনি কোনো কারণে এটি কারো সাথে শেয়ার করেন, তাহলে আপনার উচিত এটি প্রত্যাহার করা এবং crates.io-তে একটি নতুন টোকেন তৈরি করা।
একটি নতুন ক্রেটে মেটাডেটা যোগ করা
ধরা যাক আপনার একটি ক্রেট আছে যা আপনি পাবলিশ করতে চান। পাবলিশ করার আগে, আপনাকে ক্রেটের Cargo.toml ফাইলের [package]
সেকশনে কিছু মেটাডেটা যোগ করতে হবে।
আপনার ক্রেটের একটি ইউনিক নাম প্রয়োজন হবে। আপনি যখন স্থানীয়ভাবে একটি ক্রেটে কাজ করছেন, তখন আপনি ক্রেটের যা খুশি নাম দিতে পারেন। যাইহোক, crates.io-তে ক্রেটের নাম ফার্স্ট-কাম, ফার্স্ট-সার্ভড ভিত্তিতে বরাদ্দ করা হয়। একবার একটি ক্রেটের নাম নেওয়া হয়ে গেলে, অন্য কেউ সেই নামে ক্রেট পাবলিশ করতে পারবে না। একটি ক্রেট পাবলিশ করার চেষ্টা করার আগে, আপনি যে নামটি ব্যবহার করতে চান তা সার্চ করুন। যদি নামটি ব্যবহার করা হয়ে থাকে, আপনাকে অন্য একটি নাম খুঁজে বের করতে হবে এবং পাবলিশ করার জন্য নতুন নামটি ব্যবহার করতে Cargo.toml ফাইলের [package]
সেকশনের অধীনে name
ফিল্ডটি এডিট করতে হবে, এভাবে:
ফাইলের নাম: Cargo.toml
[package]
name = "guessing_game"
এমনকি যদি আপনি একটি ইউনিক নাম বেছে নিয়ে থাকেন, আপনি যখন এই সময়ে ক্রেটটি পাবলিশ করার জন্য cargo publish
চালান, আপনি একটি ওয়ার্নিং এবং তারপর একটি এরর পাবেন:
$ cargo publish
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error (status 400 Bad Request): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields
এর ফলে একটি এরর হয় কারণ আপনার কিছু গুরুত্বপূর্ণ তথ্য অনুপস্থিত: একটি বিবরণ এবং লাইসেন্স প্রয়োজন যাতে লোকেরা জানতে পারে আপনার ক্রেট কী করে এবং কোন শর্তে তারা এটি ব্যবহার করতে পারে। Cargo.toml-এ, একটি বিবরণ যোগ করুন যা মাত্র এক বা দুটি বাক্য, কারণ এটি সার্চ ফলাফলে আপনার ক্রেটের সাথে প্রদর্শিত হবে। license
ফিল্ডের জন্য, আপনাকে একটি লাইসেন্স আইডেন্টিফায়ার ভ্যালু দিতে হবে। লিনাক্স ফাউন্ডেশনের সফটওয়্যার প্যাকেজ ডেটা এক্সচেঞ্জ (SPDX) আপনি এই মানের জন্য ব্যবহার করতে পারেন এমন আইডেন্টিফায়ারগুলো তালিকাভুক্ত করে। উদাহরণস্বরূপ, আপনি আপনার ক্রেটকে MIT লাইসেন্স ব্যবহার করে লাইসেন্স করেছেন তা নির্দিষ্ট করতে, MIT
আইডেন্টিফায়ার যোগ করুন:
ফাইলের নাম: Cargo.toml
[package]
name = "guessing_game"
license = "MIT"
আপনি যদি এমন একটি লাইসেন্স ব্যবহার করতে চান যা SPDX-এ প্রদর্শিত হয় না, আপনাকে সেই লাইসেন্সের টেক্সট একটি ফাইলে রাখতে হবে, ফাইলটি আপনার প্রজেক্টে অন্তর্ভুক্ত করতে হবে এবং তারপর license
কী ব্যবহার করার পরিবর্তে সেই ফাইলের নাম নির্দিষ্ট করতে license-file
ব্যবহার করতে হবে।
আপনার প্রজেক্টের জন্য কোন লাইসেন্স উপযুক্ত সে সম্পর্কে নির্দেশনা এই বইয়ের সুযোগের বাইরে। রাস্ট সম্প্রদায়ের অনেক লোক তাদের প্রজেক্টগুলোকে রাস্টের মতোই লাইসেন্স করে, MIT OR Apache-2.0
-এর একটি দ্বৈত লাইসেন্স ব্যবহার করে। এই অনুশীলনটি দেখায় যে আপনি আপনার প্রজেক্টের জন্য একাধিক লাইসেন্স পেতে OR
দ্বারা পৃথক করা একাধিক লাইসেন্স আইডেন্টিফায়ারও নির্দিষ্ট করতে পারেন।
একটি ইউনিক নাম, সংস্করণ, আপনার বিবরণ এবং একটি লাইসেন্স যোগ করার পরে, পাবলিশ করার জন্য প্রস্তুত একটি প্রজেক্টের Cargo.toml ফাইলটি এইরকম দেখতে হতে পারে:
ফাইলের নাম: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
কার্গোর ডকুমেন্টেশন অন্যান্য মেটাডেটা বর্ণনা করে যা আপনি নির্দিষ্ট করতে পারেন যাতে অন্যরা আপনার ক্রেট আরও সহজে আবিষ্কার করতে এবং ব্যবহার করতে পারে।
Crates.io-তে পাবলিশ করা
এখন যেহেতু আপনি একটি অ্যাকাউন্ট তৈরি করেছেন, আপনার API টোকেন সংরক্ষণ করেছেন, আপনার ক্রেটের জন্য একটি নাম বেছে নিয়েছেন এবং প্রয়োজনীয় মেটাডেটা নির্দিষ্ট করেছেন, আপনি পাবলিশ করার জন্য প্রস্তুত! একটি ক্রেট পাবলিশ করা একটি নির্দিষ্ট সংস্করণ crates.io-তে আপলোড করে যাতে অন্যরা এটি ব্যবহার করতে পারে।
সাবধান থাকুন, কারণ একটি পাবলিশ স্থায়ী। সংস্করণটি কখনই ওভাররাইট করা যাবে না এবং কোডটি কিছু নির্দিষ্ট পরিস্থিতি ছাড়া ডিলিট করা যাবে না। Crates.io-এর একটি প্রধান লক্ষ্য হল কোডের একটি স্থায়ী আর্কাইভ হিসাবে কাজ করা যাতে crates.io-থেকে ক্রেটের উপর নির্ভরশীল সমস্ত প্রজেক্টের বিল্ড কাজ করতে থাকে। সংস্করণ ডিলিট করার অনুমতি দিলে সেই লক্ষ্য পূরণ করা অসম্ভব হয়ে পড়বে। যাইহোক, আপনি কতগুলো ক্রেট সংস্করণ পাবলিশ করতে পারবেন তার কোনো সীমা নেই।
cargo publish
কমান্ডটি আবার চালান। এটি এখন সফল হওয়া উচিত:
$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Packaged 6 files, 1.2KiB (895.0B compressed)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
Uploaded guessing_game v0.1.0 to registry `crates-io`
note: waiting for `guessing_game v0.1.0` to be available at registry
`crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
Published guessing_game v0.1.0 at registry `crates-io`
অভিনন্দন! আপনি এখন রাস্ট সম্প্রদায়ের সাথে আপনার কোড শেয়ার করেছেন, এবং যে কেউ সহজেই আপনার ক্রেটকে তাদের প্রজেক্টের একটি ডিপেনডেন্সি হিসাবে যোগ করতে পারে।
একটি বিদ্যমান ক্রেটের নতুন সংস্করণ পাবলিশ করা
আপনি যখন আপনার ক্রেটে পরিবর্তন করেছেন এবং একটি নতুন সংস্করণ রিলিজ করার জন্য প্রস্তুত, তখন আপনি আপনার Cargo.toml ফাইলে নির্দিষ্ট version
-এর মান পরিবর্তন করুন এবং পুনরায় পাবলিশ করুন। আপনি কী ধরনের পরিবর্তন করেছেন তার উপর ভিত্তি করে একটি উপযুক্ত পরবর্তী সংস্করণ নম্বর কী হবে তা নির্ধারণ করতে Semantic Versioning rules ব্যবহার করুন। তারপর নতুন সংস্করণ আপলোড করতে cargo publish
চালান।
cargo yank
দিয়ে Crates.io থেকে সংস্করণ অপসারণ করা
যদিও আপনি একটি ক্রেটের পূর্ববর্তী সংস্করণগুলো সরাতে পারবেন না, আপনি ভবিষ্যতের যেকোনো প্রজেক্টকে নতুন ডিপেনডেন্সি হিসেবে যোগ করা থেকে বিরত রাখতে পারেন। এটি তখন কার্যকর হয় যখন একটি ক্রেট সংস্করণ কোনো না কোনো কারণে ভাঙা থাকে। এই ধরনের পরিস্থিতিতে, কার্গো একটি ক্রেট সংস্করণকে yank করা সমর্থন করে।
একটি সংস্করণকে Yank করা নতুন প্রজেক্টগুলোকে সেই সংস্করণের উপর নির্ভর করতে বাধা দেয় এবং এর উপর নির্ভরশীল সমস্ত বিদ্যমান প্রজেক্টকে চলতে দেয়। মূলত, একটি yank মানে হল যে Cargo.lock সহ সমস্ত প্রজেক্ট ভাঙবে না, এবং ভবিষ্যতে তৈরি করা কোনো Cargo.lock ফাইল yank করা সংস্করণটি ব্যবহার করবে না।
একটি ক্রেটের একটি সংস্করণ yank করতে, আপনি পূর্বে পাবলিশ করেছেন এমন ক্রেটের ডিরেক্টরিতে, cargo yank
চালান এবং আপনি কোন সংস্করণটি yank করতে চান তা নির্দিষ্ট করুন। উদাহরণস্বরূপ, যদি আমরা guessing_game
নামের একটি ক্রেটের ১.০.১ সংস্করণ পাবলিশ করে থাকি এবং আমরা এটিকে yank করতে চাই, guessing_game
এর প্রজেক্ট ডিরেক্টরিতে আমরা চালাব:
$ cargo yank --vers 1.0.1
Updating crates.io index
Yank guessing_game@1.0.1
কমান্ডে --undo
যোগ করে, আপনি একটি yank বাতিল করতে পারেন এবং প্রজেক্টগুলোকে আবার একটি সংস্করণের উপর নির্ভর করার অনুমতি দিতে পারেন:
$ cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank guessing_game@1.0.1
একটি yank কোনো কোড ডিলিট করে না। এটি, উদাহরণস্বরূপ, ভুলবশত আপলোড করা গোপন তথ্য ডিলিট করতে পারে না। যদি এমন হয়, তাহলে আপনাকে অবিলম্বে সেই গোপন তথ্যগুলো রিসেট করতে হবে।
কার্গো ওয়ার্কস্পেস (Cargo Workspaces)
অধ্যায় ১২-এ, আমরা একটি প্যাকেজ তৈরি করেছিলাম যেখানে একটি বাইনারি ক্রেট এবং একটি লাইব্রেরি ক্রেট অন্তর্ভুক্ত ছিল। আপনার প্রজেক্ট বড় হওয়ার সাথে সাথে আপনি দেখতে পারেন যে লাইব্রেরি ক্রেটটি আরও বড় হচ্ছে এবং আপনি আপনার প্যাকেজটিকে একাধিক লাইব্রেরি ক্রেটে ভাগ করতে চান। কার্গো এই ক্ষেত্রে ওয়ার্কস্পেস (workspaces) নামে একটি ফিচার সরবরাহ করে যা একসাথে ডেভেলপ করা একাধিক সম্পর্কিত প্যাকেজ পরিচালনা করতে সাহায্য করতে পারে।
একটি ওয়ার্কস্পেস তৈরি করা
একটি ওয়ার্কস্পেস হলো এমন一组 প্যাকেজ যা একই Cargo.lock এবং আউটপুট ডিরেক্টরি শেয়ার করে। আসুন আমরা একটি ওয়ার্কস্পেস ব্যবহার করে একটি প্রজেক্ট তৈরি করি—আমরা এখানে সাধারণ কোড ব্যবহার করব যাতে আমরা ওয়ার্কস্পেসের কাঠামোর উপর মনোযোগ দিতে পারি। একটি ওয়ার্কস্পেস গঠন করার বিভিন্ন উপায় আছে, তাই আমরা শুধু একটি সাধারণ উপায় দেখাব। আমাদের একটি ওয়ার্কস্পেস থাকবে যাতে একটি বাইনারি এবং দুটি লাইব্রেরি থাকবে। বাইনারিটি, যা মূল কার্যকারিতা প্রদান করবে, দুটি লাইব্রেরির উপর নির্ভর করবে। একটি লাইব্রেরি add_one
ফাংশন এবং অন্য লাইব্রেরিটি add_two
ফাংশন প্রদান করবে। এই তিনটি ক্রেট একই ওয়ার্কস্পেসের অংশ হবে। আমরা ওয়ার্কস্পেসের জন্য একটি নতুন ডিরেক্টরি তৈরি করে শুরু করব:
$ mkdir add
$ cd add
এরপর, add ডিরেক্টরিতে, আমরা Cargo.toml ফাইল তৈরি করব যা পুরো ওয়ার্কস্পেস কনফিগার করবে। এই ফাইলে কোনো [package]
সেকশন থাকবে না। পরিবর্তে, এটি একটি [workspace]
সেকশন দিয়ে শুরু হবে যা আমাদের ওয়ার্কস্পেসে মেম্বার যোগ করার সুযোগ দেবে। আমরা আমাদের ওয়ার্কস্পেসে কার্গোর সর্বশেষ এবং সর্বশ্রেষ্ঠ রিজলভার অ্যালগরিদম ব্যবহার করার জন্য resolver
-এর মান "3"
সেট করব।
ফাইলের নাম: Cargo.toml
[workspace]
resolver = "3"
এরপরে, আমরা add ডিরেক্টরির মধ্যে cargo new
চালিয়ে adder
বাইনারি ক্রেট তৈরি করব:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
একটি ওয়ার্কস্পেসের ভিতরে cargo new
চালালে নতুন তৈরি হওয়া প্যাকেজটি স্বয়ংক্রিয়ভাবে ওয়ার্কস্পেসের Cargo.toml-এর [workspace]
ডেফিনিশনের members
কী-তে যোগ হয়ে যায়, যেমন:
[workspace]
resolver = "3"
members = ["adder"]
এই মুহূর্তে, আমরা cargo build
চালিয়ে ওয়ার্কস্পেসটি বিল্ড করতে পারি। আপনার add ডিরেক্টরির ফাইলগুলো এইরকম দেখতে হবে:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
ওয়ার্কস্পেসের টপ লেভেলে একটি target ডিরেক্টরি রয়েছে যেখানে কম্পাইল করা আর্টিফ্যাক্টগুলো রাখা হবে; adder
প্যাকেজের নিজস্ব কোনো target ডিরেক্টরি নেই। এমনকি যদি আমরা adder ডিরেক্টরির ভেতর থেকে cargo build
চালাই, কম্পাইল করা আর্টিফ্যাক্টগুলো add/adder/target এর পরিবর্তে add/target এই ডিরেক্টরিতেই জমা হবে। কার্গো একটি ওয়ার্কস্পেসের target ডিরেক্টরি এইভাবে গঠন করে কারণ একটি ওয়ার্কস্পেসের ক্রেটগুলো একে অপরের উপর নির্ভর করার জন্য তৈরি। যদি প্রতিটি ক্রেটের নিজস্ব target ডিরেক্টরি থাকত, তবে প্রতিটি ক্রেটকে ওয়ার্কস্পেসের অন্য ক্রেটগুলোকে পুনরায় কম্পাইল করতে হতো যাতে আর্টিফ্যাক্টগুলো তার নিজস্ব target ডিরেক্টরিতে রাখা যায়। একটি target ডিরেক্টরি শেয়ার করার মাধ্যমে, ক্রেটগুলো অপ্রয়োজনীয় রি-বিল্ডিং এড়াতে পারে।
ওয়ার্কস্পেসে দ্বিতীয় প্যাকেজ তৈরি করা
এরপরে, আসুন ওয়ার্কস্পেসে আরেকটি মেম্বার প্যাকেজ তৈরি করি এবং এর নাম দিই add_one
। add_one
নামে একটি নতুন লাইব্রেরি ক্রেট তৈরি করুন:
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
টপ-লেভেল Cargo.toml ফাইলটি এখন members
তালিকায় add_one পাথ অন্তর্ভুক্ত করবে:
ফাইলের নাম: Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
আপনার add ডিরেক্টরিতে এখন এই ডিরেক্টরি এবং ফাইলগুলো থাকা উচিত:
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
add_one/src/lib.rs ফাইলে, আসুন একটি add_one
ফাংশন যোগ করি:
ফাইলের নাম: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
এখন আমরা আমাদের বাইনারি সহ adder
প্যাকেজটিকে আমাদের লাইব্রেরি সহ add_one
প্যাকেজের উপর নির্ভর করাতে পারি। প্রথমে, আমাদের adder/Cargo.toml-এ add_one
-এর জন্য একটি পাথ ডিপেন্ডেন্সি যোগ করতে হবে।
ফাইলের নাম: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
কার্গো ধরে নেয় না যে একটি ওয়ার্কস্পেসের ক্রেটগুলো একে অপরের উপর নির্ভর করবে, তাই আমাদের ডিপেন্ডেন্সি সম্পর্কগুলো স্পষ্টভাবে উল্লেখ করতে হবে।
এরপর, আসুন adder
ক্রেটে (add_one
ক্রেট থেকে) add_one
ফাংশনটি ব্যবহার করি। adder/src/main.rs ফাইলটি খুলুন এবং main
ফাংশনটিকে add_one
ফাংশন কল করার জন্য পরিবর্তন করুন, যেমনটি তালিকা ১৪-৭ এ দেখানো হয়েছে।
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
আসুন টপ-লেভেল add ডিরেক্টরিতে cargo build
চালিয়ে ওয়ার্কস্পেসটি বিল্ড করি!
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
add ডিরেক্টরি থেকে বাইনারি ক্রেটটি চালানোর জন্য, আমরা -p
আর্গুমেন্ট এবং cargo run
এর সাথে প্যাকেজের নাম ব্যবহার করে ওয়ার্কস্পেসের কোন প্যাকেজটি চালাতে চাই তা নির্দিষ্ট করতে পারি:
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
এটি adder/src/main.rs এর কোডটি চালায়, যা add_one
ক্রেটের উপর নির্ভর করে।
একটি ওয়ার্কস্পেসে একটি এক্সটার্নাল প্যাকেজের উপর নির্ভর করা
লক্ষ্য করুন যে ওয়ার্কস্পেসের টপ লেভেলে শুধুমাত্র একটি Cargo.lock ফাইল আছে, প্রতিটি ক্রেটের ডিরেক্টরিতে একটি করে Cargo.lock না থাকার পরিবর্তে। এটি নিশ্চিত করে যে সমস্ত ক্রেট সমস্ত ডিপেন্ডেন্সির একই সংস্করণ ব্যবহার করছে। যদি আমরা adder/Cargo.toml এবং add_one/Cargo.toml ফাইলে rand
প্যাকেজ যোগ করি, কার্গো উভয়কেই rand
-এর একটি সংস্করণে রিজলভ করবে এবং তা একটি Cargo.lock-এ রেকর্ড করবে। ওয়ার্কস্পেসের সমস্ত ক্রেটকে একই ডিপেন্ডেন্সি ব্যবহার করতে বাধ্য করার মানে হলো ক্রেটগুলো সবসময় একে অপরের সাথে কম্প্যাটিবল থাকবে। আসুন add_one/Cargo.toml_ ফাইলের
[dependencies]সেকশনে
randক্রেট যোগ করি যাতে আমরা
add_oneক্রেটে
rand` ক্রেট ব্যবহার করতে পারি:
ফাইলের নাম: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
আমরা এখন add_one/src/lib.rs ফাইলে use rand;
যোগ করতে পারি, এবং add ডিরেক্টরিতে cargo build
চালিয়ে পুরো ওয়ার্কস্পেস বিল্ড করলে rand
ক্রেটটি নিয়ে আসা হবে এবং কম্পাইল করা হবে। আমরা একটি ওয়ার্নিং পাব কারণ আমরা স্কোপে আনা rand
-কে রেফারেন্স করছি না:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
টপ-লেভেল Cargo.lock-এ এখন add_one
-এর rand
-এর উপর নির্ভরতার তথ্য রয়েছে। যাইহোক, যদিও rand
ওয়ার্কস্পেসের কোথাও ব্যবহৃত হচ্ছে, আমরা ওয়ার্কস্পেসের অন্য ক্রেটগুলিতে এটি ব্যবহার করতে পারব না যতক্ষণ না আমরা তাদের Cargo.toml ফাইলে rand
যোগ করি। উদাহরণস্বরূপ, যদি আমরা adder
প্যাকেজের জন্য adder/src/main.rs ফাইলে use rand;
যোগ করি, আমরা একটি এরর পাব:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
এটি ঠিক করার জন্য, adder
প্যাকেজের জন্য Cargo.toml ফাইলটি এডিট করুন এবং নির্দেশ করুন যে rand
এটির জন্যও একটি ডিপেন্ডেন্সি। adder
প্যাকেজ বিল্ড করলে Cargo.lock-এ adder
-এর ডিপেন্ডেন্সি তালিকায় rand
যোগ হবে, কিন্তু rand
-এর কোনো অতিরিক্ত কপি ডাউনলোড করা হবে না। কার্গো নিশ্চিত করবে যে ওয়ার্কস্পেসের প্রতিটি প্যাকেজের প্রতিটি ক্রেট, যারা rand
প্যাকেজ ব্যবহার করছে, তারা একই সংস্করণ ব্যবহার করবে যতক্ষণ তারা rand
-এর কম্প্যাটিবল সংস্করণ নির্দিষ্ট করে, যা আমাদের স্পেস বাঁচায় এবং নিশ্চিত করে যে ওয়ার্কস্পেসের ক্রেটগুলো একে অপরের সাথে কম্প্যাটিবল হবে।
যদি ওয়ার্কস্পেসের ক্রেটগুলো একই ডিপেন্ডেন্সির ইনকম্প্যাটিবল সংস্করণ নির্দিষ্ট করে, কার্গো সেগুলোর প্রত্যেকটিকে রিজলভ করবে, কিন্তু তারপরও যত কম সম্ভব সংস্করণ রিজলভ করার চেষ্টা করবে।
একটি ওয়ার্কস্পেসে একটি টেস্ট যোগ করা
আরেকটি উন্নতির জন্য, আসুন add_one
ক্রেটের মধ্যে add_one::add_one
ফাংশনের একটি টেস্ট যোগ করি:
ফাইলের নাম: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}```
এখন টপ-লেভেল _add_ ডিরেক্টরিতে `cargo test` চালান। এই ধরনের গঠনযুক্ত একটি ওয়ার্কস্পেসে `cargo test` চালালে ওয়ার্কস্পেসের সমস্ত ক্রেটের জন্য টেস্টগুলো চলবে:
<!-- manual-regeneration
cd listings/ch14-more-about-cargo/no-listing-04-workspace-with-tests/add
cargo test
copy output below; the output updating script doesn't handle subdirectories in
paths properly
-->
```console
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
আউটপুটের প্রথম সেকশনটি দেখায় যে add_one
ক্রেটের it_works
টেস্টটি পাস করেছে। পরবর্তী সেকশনটি দেখায় যে adder
ক্রেটে শূন্যটি টেস্ট পাওয়া গেছে, এবং তারপর শেষ সেকশনটি দেখায় যে add_one
ক্রেটে শূন্যটি ডকুমেন্টেশন টেস্ট পাওয়া গেছে।
আমরা টপ-লেভেল ডিরেক্টরি থেকে একটি ওয়ার্কস্পেসের একটি নির্দিষ্ট ক্রেটের জন্য টেস্টও চালাতে পারি -p
ফ্ল্যাগ ব্যবহার করে এবং যে ক্রেটটি আমরা টেস্ট করতে চাই তার নাম উল্লেখ করে:
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
এই আউটপুটটি দেখায় যে cargo test
শুধুমাত্র add_one
ক্রেটের জন্য টেস্ট চালিয়েছে এবং adder
ক্রেটের টেস্ট চালায়নি।
আপনি যদি ওয়ার্কস্পেসের ক্রেটগুলো crates.io-তে পাবলিশ করেন, ওয়ার্কস্পেসের প্রতিটি ক্রেটকে আলাদাভাবে পাবলিশ করতে হবে। cargo test
-এর মতো, আমরা -p
ফ্ল্যাগ ব্যবহার করে এবং যে ক্রেটটি আমরা পাবলিশ করতে চাই তার নাম উল্লেখ করে আমাদের ওয়ার্কস্পেসের একটি নির্দিষ্ট ক্রেট পাবলিশ করতে পারি।
অতিরিক্ত অনুশীলনের জন্য, add_one
ক্রেটের মতোই এই ওয়ার্কস্পেসে একটি add_two
ক্রেট যোগ করুন!
আপনার প্রজেক্ট বড় হওয়ার সাথে সাথে, একটি ওয়ার্কস্পেস ব্যবহার করার কথা বিবেচনা করুন: এটি আপনাকে একটি বড় কোডের ব্লবের চেয়ে ছোট, সহজে বোঝা যায় এমন কম্পোনেন্ট নিয়ে কাজ করতে সক্ষম করে। উপরন্তু, ক্রেটগুলোকে একটি ওয়ার্কস্পেসে রাখা ক্রেটগুলোর মধ্যে সমন্বয় সহজ করতে পারে যদি সেগুলো প্রায়শই একই সময়ে পরিবর্তন করা হয়।
cargo install
দিয়ে বাইনারি ইনস্টল করা
cargo install
কমান্ড আপনাকে স্থানীয়ভাবে বাইনারি ক্রেট ইনস্টল এবং ব্যবহার করার সুযোগ দেয়। এটি সিস্টেম প্যাকেজ প্রতিস্থাপন করার জন্য তৈরি করা হয়নি; এটি Rust ডেভেলপারদের জন্য crates.io-তে অন্যদের শেয়ার করা টুলগুলো ইনস্টল করার একটি সুবিধাজনক উপায়। মনে রাখবেন, আপনি শুধুমাত্র সেইসব প্যাকেজ ইনস্টল করতে পারবেন যেগুলোতে বাইনারি টার্গেট (binary targets) আছে। একটি বাইনারি টার্গেট হলো একটি রান করা যায় এমন প্রোগ্রাম যা তৈরি হয় যদি ক্রেটটিতে একটি src/main.rs ফাইল বা বাইনারি হিসাবে নির্দিষ্ট করা অন্য কোনো ফাইল থাকে, যা লাইব্রেরি টার্গেটের বিপরীত। লাইব্রেরি টার্গেট নিজে থেকে রান করা যায় না তবে অন্য প্রোগ্রামের মধ্যে অন্তর্ভুক্ত করার জন্য উপযুক্ত। সাধারণত, ক্রেটের README ফাইলে তথ্য থাকে যে ক্রেটটি একটি লাইব্রেরি, একটি বাইনারি টার্গেট আছে, নাকি উভয়ই।
cargo install
দিয়ে ইনস্টল করা সমস্ত বাইনারি ইনস্টলেশন রুটের bin ফোল্ডারে সংরক্ষণ করা হয়। আপনি যদি rustup.rs ব্যবহার করে Rust ইনস্টল করে থাকেন এবং কোনো কাস্টম কনফিগারেশন না থাকে, তাহলে এই ডিরেক্টরিটি হবে $HOME/.cargo/bin। নিশ্চিত করুন যে ডিরেক্টরিটি আপনার $PATH
-এ রয়েছে যাতে আপনি cargo install
দিয়ে ইনস্টল করা প্রোগ্রামগুলো চালাতে পারেন।
উদাহরণস্বরূপ, অধ্যায় ১২-এ আমরা উল্লেখ করেছি যে ফাইল সার্চ করার জন্য grep
টুলের একটি Rust ইমপ্লিমেন্টেশন আছে যার নাম ripgrep
। ripgrep
ইনস্টল করতে, আমরা নিম্নলিখিত কমান্ডটি চালাতে পারি:
$ cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v14.1.1
Downloaded 1 crate (213.6 KB) in 0.40s
Installing ripgrep v14.1.1
--snip--
Compiling grep v0.3.2
Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v14.1.1` (executable `rg`)
আউটপুটের শেষের আগের লাইনটি ইনস্টল করা বাইনারির অবস্থান এবং নাম দেখায়, যা ripgrep
-এর ক্ষেত্রে rg
। যতক্ষণ পর্যন্ত ইনস্টলেশন ডিরেক্টরিটি আপনার $PATH
-এ থাকে, যেমনটি আগে উল্লেখ করা হয়েছে, আপনি rg --help
চালিয়ে ফাইল খোঁজার জন্য একটি দ্রুত, রাস্টিয়ার টুল ব্যবহার করা শুরু করতে পারেন
কাস্টম কমান্ড দিয়ে কার্গোকে এক্সটেন্ড করা
কার্গো এমনভাবে ডিজাইন করা হয়েছে যাতে আপনি এটিকে পরিবর্তন না করেই নতুন সাবকমান্ড দিয়ে এক্সটেন্ড করতে পারেন। যদি আপনার $PATH
-এ থাকা কোনো বাইনারির নাম cargo-something
হয়, তাহলে আপনি এটিকে cargo something
কমান্ড চালিয়ে কার্গোর সাবকমান্ডের মতো করে চালাতে পারেন। এই ধরনের কাস্টম কমান্ডগুলো cargo --list
চালালে তালিকাভুক্ত হয়। cargo install
ব্যবহার করে এক্সটেনশন ইনস্টল করতে পারা এবং তারপর সেগুলোকে বিল্ট-ইন কার্গো টুলের মতোই চালাতে পারা কার্গোর ডিজাইনের একটি অত্যন্ত সুবিধাজনক সুবিধা!
সারসংক্ষেপ
কার্গো এবং crates.io-এর মাধ্যমে কোড শেয়ার করা Rust ইকোসিস্টেমকে বিভিন্ন কাজের জন্য দরকারী করে তোলার একটি অংশ। Rust-এর স্ট্যান্ডার্ড লাইব্রেরি ছোট এবং স্থিতিশীল, কিন্তু ক্রেটগুলো ভাষার টাইমলাইনের থেকে ভিন্ন সময়ে সহজেই শেয়ার করা, ব্যবহার করা এবং উন্নত করা যায়। crates.io-তে আপনার জন্য দরকারী কোড শেয়ার করতে দ্বিধা করবেন না; সম্ভবত এটি অন্য কারো জন্যও দরকারী হবে
Smart Pointers
একটি pointer হলো একটি সাধারণ ধারণা, যা এমন একটি variable-কে বোঝায় যা মেমোরিতে থাকা কোনো একটি অ্যাড্রেস ধারণ করে। এই অ্যাড্রেসটি অন্য কোনো ডেটাকে নির্দেশ করে বা “point করে” থাকে। রাস্টে সবচেয়ে সাধারণ ধরনের pointer হলো reference, যা সম্পর্কে আপনি Chapter 4-এ জেনেছেন। Reference-কে &
চিহ্ন দিয়ে প্রকাশ করা হয় এবং এটি যে ভ্যালুকে point করে, তাকে borrow করে। ডেটাকে নির্দেশ করা ছাড়া এদের অন্য কোনো বিশেষ ক্ষমতা নেই, এবং এদের কোনো ওভারহেডও (overhead) নেই।
অন্যদিকে, Smart pointer হলো এমন ডেটা স্ট্রাকচার যা একটি pointer-এর মতোই কাজ করে, কিন্তু এর সাথে অতিরিক্ত মেটাডেটা (metadata) এবং কিছু বিশেষ ক্ষমতাও থাকে। Smart pointer-এর ধারণাটি শুধু রাস্টের জন্য নতুন নয়; এর প্রচলন C++ থেকে শুরু হয়েছিল এবং অন্যান্য ভাষাতেও এর وجود রয়েছে। রাস্টের স্ট্যান্ডার্ড লাইব্রেরিতে বিভিন্ন ধরনের smart pointer রয়েছে যা reference-এর চেয়েও বেশি কার্যকারিতা প্রদান করে। এই সাধারণ ধারণাটি বোঝার জন্য, আমরা কয়েকটি ভিন্ন ধরনের smart pointer-এর উদাহরণ দেখব, যার মধ্যে একটি হলো reference counting smart pointer। এই pointer একটি ডেটাকে একাধিক owner রাখার সুযোগ দেয়। এটি owner-এর সংখ্যা ট্র্যাক করে এবং যখন কোনো owner থাকে না, তখন ডেটাটি মুছে ফেলে।
রাস্টের ownership এবং borrowing-এর ধারণার কারণে reference এবং smart pointer-এর মধ্যে আরও একটি পার্থক্য রয়েছে: reference শুধু ডেটা borrow করে, কিন্তু অনেক ক্ষেত্রে smart pointer তার নির্দেশিত ডেটার own (মালিকানা) করে।
Smart pointer সাধারণত struct ব্যবহার করে ইমপ্লিমেন্ট (implement) করা হয়। সাধারণ struct-এর মতো নয়, smart pointer-গুলো Deref
এবং Drop
ট্রেইট (trait) ইমপ্লিমেন্ট করে। Deref
ট্রেইটটি smart pointer struct-এর একটি ইনস্ট্যান্সকে reference-এর মতো আচরণ করার সুযোগ দেয়, ফলে আপনি reference বা smart pointer উভয়ের জন্য কাজ করে এমন কোড লিখতে পারেন। Drop
ট্রেইটটি আপনাকে সেই কোডটি কাস্টমাইজ (customize) করার সুযোগ দেয়, যা smart pointer-এর ইনস্ট্যান্সটি স্কোপের (scope) বাইরে চলে গেলে রান হবে। এই অধ্যায়ে, আমরা এই দুটি ট্রেইট নিয়েই আলোচনা করব এবং দেখাব কেন এগুলো smart pointer-এর জন্য এত গুরুত্বপূর্ণ।
যেহেতু smart pointer প্যাটার্নটি রাস্ট-এ প্রায়শই ব্যবহৃত একটি সাধারণ ডিজাইন প্যাটার্ন, তাই এই অধ্যায়ে আমরা সব ধরনের smart pointer নিয়ে আলোচনা করব না। অনেক লাইব্রেরির নিজস্ব smart pointer রয়েছে, এবং আপনি চাইলে নিজের smart pointer তৈরি করতে পারেন। আমরা স্ট্যান্ডার্ড লাইব্রেরির সবচেয়ে সাধারণ smart pointer-গুলো নিয়ে আলোচনা করব:
Box<T>
, heap-এ ভ্যালু allocate করার জন্যRc<T>
, একটি reference counting টাইপ যা একাধিক ownership-এর সুযোগ দেয়Ref<T>
এবংRefMut<T>
, যাRefCell<T>
-এর মাধ্যমে অ্যাক্সেস করা হয়। এটি compile time-এর পরিবর্তে runtime-এ borrowing-এর নিয়মগুলো প্রয়োগ করে
এর পাশাপাশি, আমরা interior mutability প্যাটার্নটি নিয়েও আলোচনা করব, যেখানে একটি immutable টাইপ তার ভেতরের কোনো ভ্যালু পরিবর্তন করার জন্য একটি API প্রদান করে। আমরা reference cycle নিয়েও আলোচনা করব: এগুলো কীভাবে মেমোরি লিক (leak) করতে পারে এবং কীভাবে তা প্রতিরোধ করা যায়।
চলুন, শুরু করা যাক
Heap-এ ডেটা Point করার জন্য Box<T>
-এর ব্যবহার
সবচেয়ে সহজ smart pointer হলো box, যার টাইপ লেখা হয় Box<T>
। Boxes আপনাকে স্ট্যাকের (stack) পরিবর্তে হিপ-এ (heap) ডেটা সংরক্ষণ করার সুযোগ দেয়। যা স্ট্যাকে অবশিষ্ট থাকে তা হলো হিপ ডেটার একটি pointer। স্ট্যাক এবং হিপের মধ্যে পার্থক্য পর্যালোচনা করতে Chapter 4 দেখুন।
Box-এর ডেটা স্ট্যাকের পরিবর্তে হিপে সংরক্ষণ করা ছাড়া আর কোনো পারফরম্যান্স ওভারহেড নেই। তবে এদের খুব বেশি অতিরিক্ত ক্ষমতাও নেই। আপনি বেশিরভাগ সময়ে এই পরিস্থিতিগুলিতে এগুলি ব্যবহার করবেন:
- যখন আপনার কাছে এমন একটি টাইপ থাকে যার সাইজ কম্পাইল টাইমে জানা যায় না এবং আপনি সেই টাইপের একটি ভ্যালু এমন একটি কনটেক্সট-এ ব্যবহার করতে চান যেখানে একটি নির্দিষ্ট সাইজের প্রয়োজন হয়।
- যখন আপনার কাছে প্রচুর পরিমাণে ডেটা থাকে এবং আপনি ownership হস্তান্তর করতে চান কিন্তু নিশ্চিত করতে চান যে ডেটা কপি হবে না।
- যখন আপনি একটি ভ্যালুর owner হতে চান এবং আপনি শুধু চান যে এটি একটি নির্দিষ্ট ট্রেইট (trait) ইমপ্লিমেন্ট করে, কোনো নির্দিষ্ট টাইপের না হয়ে।
আমরা প্রথম পরিস্থিতিটি দেখাব "Recursive Types with Boxes" অংশে। দ্বিতীয় ক্ষেত্রে, বিপুল পরিমাণ ডেটার ownership হস্তান্তর করতে অনেক সময় লাগতে পারে কারণ ডেটা স্ট্যাকের উপর কপি করা হয়। এই পরিস্থিতিতে পারফরম্যান্স উন্নত করতে, আমরা বিপুল পরিমাণ ডেটা একটি box-এ করে হিপ-এ সংরক্ষণ করতে পারি। তারপরে, শুধুমাত্র অল্প পরিমাণ pointer ডেটা স্ট্যাকের উপর কপি করা হয়, যখন এটি যে ডেটাকে নির্দেশ করে তা হিপের এক জায়গায় থাকে। তৃতীয় ক্ষেত্রটি একটি trait object হিসাবে পরিচিত, এবং Chapter 18-এর ["Using Trait Objects That Allow for Values of Different Types,"][trait-objects] অংশটি এই বিষয়ে উৎসর্গীকৃত। সুতরাং আপনি এখানে যা শিখবেন তা সেই অংশে আবার প্রয়োগ করবেন!
Heap-এ ডেটা সংরক্ষণের জন্য Box<T>
ব্যবহার করা
Box<T>
-এর হিপ স্টোরেজ ব্যবহার নিয়ে আলোচনা করার আগে, আমরা এর সিনট্যাক্স এবং Box<T>
-এর মধ্যে সংরক্ষিত ভ্যালুগুলোর সাথে কীভাবে ইন্টারঅ্যাক্ট করতে হয় তা দেখব।
Listing 15-1 দেখাচ্ছে কীভাবে একটি box ব্যবহার করে একটি i32
ভ্যালু হিপ-এ সংরক্ষণ করা যায়।
fn main() { let b = Box::new(5); println!("b = {b}"); }
আমরা b
ভেরিয়েবলটিকে 5
ভ্যালুর একটি Box
-এর মান হিসাবে সংজ্ঞায়িত করি, যা হিপ-এ allocate করা হয়েছে। এই প্রোগ্রামটি b = 5
প্রিন্ট করবে; এক্ষেত্রে, আমরা box-এর ডেটা অ্যাক্সেস করতে পারি ঠিক সেভাবে যেভাবে আমরা করতাম যদি এই ডেটা স্ট্যাকে থাকতো। যেকোনো owned ভ্যালুর মতোই, যখন একটি box স্কোপের বাইরে চলে যায়, যেমন b
main
-এর শেষে চলে যাচ্ছে, এটি ডিঅ্যালোকেট (deallocated) হয়ে যাবে। ডিঅ্যালোকেশনটি box (যা স্ট্যাকে সংরক্ষিত) এবং এটি যে ডেটাকে নির্দেশ করে (যা হিপ-এ সংরক্ষিত) উভয়ের জন্যই ঘটে।
হিপ-এ একটিমাত্র ভ্যালু রাখা খুব একটা কাজের না, তাই আপনি সাধারণত এভাবে box ব্যবহার করবেন না। বেশিরভাগ পরিস্থিতিতে, একটি i32
এর মতো ভ্যালু স্ট্যাকের উপর রাখাই বেশি উপযুক্ত, যেখানে ডিফল্টভাবে সেগুলি সংরক্ষণ করা হয়। চলুন এমন একটি ক্ষেত্র দেখি যেখানে box আমাদের এমন টাইপ সংজ্ঞায়িত করার অনুমতি দেয় যা box ছাড়া আমরা সংজ্ঞায়িত করতে পারতাম না।
Box ব্যবহার করে Recursive Type সক্রিয় করা
একটি recursive type-এর ভ্যালু নিজের একটি অংশ হিসেবে একই টাইপের আরেকটি ভ্যালু রাখতে পারে। Recursive type একটি সমস্যা তৈরি করে কারণ রাস্টকে কম্পাইল টাইমে জানতে হয় একটি টাইপ কতটুকু জায়গা নেয়। কিন্তু, recursive type-এর ভ্যালুগুলোর নেস্টিং (nesting) তাত্ত্বিকভাবে অসীম পর্যন্ত চলতে পারে, তাই রাস্ট জানতে পারে না ভ্যালুটির জন্য কতটুকু জায়গা প্রয়োজন। যেহেতু box-এর একটি নির্দিষ্ট সাইজ আছে, তাই আমরা recursive type-এর সংজ্ঞায় একটি box যোগ করে recursive type সক্রিয় করতে পারি।
একটি recursive type-এর উদাহরণ হিসেবে, আসুন আমরা cons list দেখি। এটি একটি ডেটা টাইপ যা সাধারণত ফাংশনাল প্রোগ্রামিং ভাষায় পাওয়া যায়। আমরা যে cons list টাইপটি সংজ্ঞায়িত করব তা recursion ছাড়া খুবই সহজ; তাই, আমরা যে উদাহরণটি নিয়ে কাজ করব তার ধারণাগুলো যেকোনো সময় যখন আপনি recursive type জড়িত আরও জটিল পরিস্থিতিতে পড়বেন তখন কার্যকর হবে।
Cons List সম্পর্কে আরও তথ্য
একটি cons list হলো একটি ডেটা স্ট্রাকচার যা Lisp প্রোগ্রামিং ভাষা এবং এর উপভাষা থেকে এসেছে, এটি নেস্টেড পেয়ার (nested pairs) দ্বারা গঠিত এবং এটি লিঙ্কড লিস্টের (linked list) Lisp সংস্করণ। এর নাম Lisp-এর cons
ফাংশন (যা construct function-এর সংক্ষিপ্ত রূপ) থেকে এসেছে, যা তার দুটি আর্গুমেন্ট থেকে একটি নতুন পেয়ার তৈরি করে। একটি ভ্যালু এবং আরেকটি পেয়ার নিয়ে গঠিত একটি পেয়ারের উপর cons
কল করে, আমরা রিকার্সিভ পেয়ার দ্বারা গঠিত cons list তৈরি করতে পারি।
উদাহরণস্বরূপ, এখানে 1, 2, 3
লিস্ট ধারণকারী একটি cons list-এর একটি स्यूडोकोड (pseudocode) উপস্থাপনা রয়েছে, যেখানে প্রতিটি পেয়ার বন্ধনীতে রয়েছে:
(1, (2, (3, Nil)))
একটি cons list-এর প্রতিটি আইটেমে দুটি উপাদান থাকে: বর্তমান আইটেমের ভ্যালু এবং পরবর্তী আইটেম। লিস্টের শেষ আইটেমে শুধু Nil
নামক একটি ভ্যালু থাকে এবং কোনো পরবর্তী আইটেম থাকে না। একটি cons list রিকার্সিভভাবে cons
ফাংশন কল করে তৈরি করা হয়। রিকার্সনের বেস কেস (base case) বোঝানোর জন্য প্রমিত নাম হল Nil
। মনে রাখবেন যে এটি Chapter 6-এ আলোচিত "null" বা "nil" ধারণার মতো নয়, যা একটি অবৈধ বা অনুপস্থিত ভ্যালু।
Cons list রাস্ট-এ একটি সাধারণভাবে ব্যবহৃত ডেটা স্ট্রাকচার নয়। রাস্ট-এ যখন আপনার কাছে আইটেমের একটি তালিকা থাকে, তখন Vec<T>
ব্যবহার করা একটি ভালো পছন্দ। অন্যান্য, আরও জটিল রিকার্সিভ ডেটা টাইপ বিভিন্ন পরিস্থিতিতে কার্যকর, কিন্তু এই অধ্যায়ে cons list দিয়ে শুরু করে, আমরা দেখতে পারি কীভাবে box আমাদের খুব বেশি বিভ্রান্তি ছাড়াই একটি রিকার্সিভ ডেটা টাইপ সংজ্ঞায়িত করতে দেয়।
Listing 15-2-তে একটি cons list-এর জন্য একটি enum সংজ্ঞা রয়েছে। মনে রাখবেন যে এই কোডটি এখনও কম্পাইল হবে না কারণ List
টাইপের কোনো নির্দিষ্ট সাইজ নেই, যা আমরা দেখাব।
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
দ্রষ্টব্য: আমরা এই উদাহরণের উদ্দেশ্যে কেবল
i32
ভ্যালু ধারণকারী একটি cons list ইমপ্লিমেন্ট করছি। আমরা Chapter 10-এ আলোচনা করা জেনেরিক ব্যবহার করে এটি ইমপ্লিমেন্ট করতে পারতাম, যাতে যেকোনো টাইপের ভ্যালু সংরক্ষণ করতে পারে এমন একটি cons list টাইপ সংজ্ঞায়িত করা যায়।
1, 2, 3
লিস্ট সংরক্ষণ করার জন্য List
টাইপ ব্যবহার করা Listing 15-3-এর কোডের মতো দেখাবে।
enum List {
Cons(i32, List),
Nil,
}
// --snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
প্রথম Cons
ভ্যালুটি 1
এবং আরেকটি List
ভ্যালু ধারণ করে। এই List
ভ্যালুটি আরেকটি Cons
ভ্যালু যা 2
এবং আরেকটি List
ভ্যালু ধারণ করে। এই List
ভ্যালুটি আরও একটি Cons
ভ্যালু যা 3
এবং একটি List
ভ্যালু ধারণ করে, যা অবশেষে Nil
, নন-রিকার্সিভ ভ্যারিয়েন্ট যা লিস্টের সমাপ্তি নির্দেশ করে।
যদি আমরা Listing 15-3-এর কোডটি কম্পাইল করার চেষ্টা করি, আমরা Listing 15-4-এ দেখানো এররটি পাই।
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing when `List` needs drop
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
এররটি দেখায় যে এই টাইপের "সাইজ অসীম"। কারণ হল আমরা List
-কে একটি ভ্যারিয়েন্ট দিয়ে সংজ্ঞায়িত করেছি যা রিকার্সিভ: এটি সরাসরি নিজের আরেকটি ভ্যালু ধারণ করে। ফলস্বরূপ, রাস্ট বের করতে পারে না যে একটি List
ভ্যালু সংরক্ষণ করতে তার কতটুকু জায়গা প্রয়োজন। আসুন আমরা ভেঙে দেখি কেন আমরা এই এররটি পাই। প্রথমে আমরা দেখব কীভাবে রাস্ট সিদ্ধান্ত নেয় যে একটি নন-রিকার্সিভ টাইপের ভ্যালু সংরক্ষণ করতে তার কতটুকু জায়গা প্রয়োজন।
একটি নন-রিকার্সিভ টাইপের সাইজ গণনা করা
স্মরণ করুন Chapter 6-এ enum সংজ্ঞা নিয়ে আলোচনা করার সময় আমরা Listing 6-2-তে যে Message
enum সংজ্ঞায়িত করেছিলাম:
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
একটি Message
ভ্যালুর জন্য কতটুকু জায়গা বরাদ্দ করতে হবে তা নির্ধারণ করতে, রাস্ট প্রতিটি ভ্যারিয়েন্টের মধ্যে দিয়ে যায় তা দেখতে কোন ভ্যারিয়েন্টের সবচেয়ে বেশি জায়গা প্রয়োজন। রাস্ট দেখে যে Message::Quit
-এর কোনো জায়গার প্রয়োজন নেই, Message::Move
-এর দুটি i32
ভ্যালু সংরক্ষণ করার জন্য যথেষ্ট জায়গার প্রয়োজন, এবং আরও অনেক কিছু। যেহেতু কেবল একটি ভ্যারিয়েন্ট ব্যবহার করা হবে, একটি Message
ভ্যালুর জন্য সর্বাধিক যে জায়গার প্রয়োজন হবে তা হল এর বৃহত্তম ভ্যারিয়েন্টটি সংরক্ষণ করতে যে জায়গা লাগবে।
এর সাথে তুলনা করুন কী ঘটে যখন রাস্ট Listing 15-2-এর List
enum-এর মতো একটি রিকার্সিভ টাইপের জন্য কতটুকু জায়গা প্রয়োজন তা নির্ধারণ করার চেষ্টা করে। কম্পাইলার Cons
ভ্যারিয়েন্টটি দেখে শুরু করে, যা i32
টাইপের একটি ভ্যালু এবং List
টাইপের একটি ভ্যালু ধারণ করে। অতএব, Cons
-এর জন্য একটি i32
-এর সাইজ এবং একটি List
-এর সাইজের সমান জায়গার প্রয়োজন। List
টাইপের জন্য কত মেমরি প্রয়োজন তা বের করতে, কম্পাইলার ভ্যারিয়েন্টগুলো দেখে, Cons
ভ্যারিয়েন্ট দিয়ে শুরু করে। Cons
ভ্যারিয়েন্ট i32
টাইপের একটি ভ্যালু এবং List
টাইপের একটি ভ্যালু ধারণ করে, এবং এই প্রক্রিয়াটি অসীমভাবে চলতে থাকে, যেমনটি Figure 15-1-এ দেখানো হয়েছে।
Figure 15-1: অসীম Cons
ভ্যারিয়েন্ট নিয়ে গঠিত একটি অসীম List
একটি নির্দিষ্ট সাইজের রিকার্সিভ টাইপ পেতে Box<T>
ব্যবহার করা
যেহেতু রাস্ট রিকার্সিভভাবে সংজ্ঞায়িত টাইপের জন্য কতটুকু জায়গা বরাদ্দ করতে হবে তা বের করতে পারে না, তাই কম্পাইলার এই সহায়ক পরামর্শ সহ একটি এরর দেয়:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
এই পরামর্শে, indirection মানে হল একটি ভ্যালু সরাসরি সংরক্ষণ করার পরিবর্তে, আমাদের ডেটা স্ট্রাকচারটি পরিবর্তন করে ভ্যালুটির একটি pointer সংরক্ষণ করে পরোক্ষভাবে ভ্যালুটি সংরক্ষণ করা উচিত।
যেহেতু একটি Box<T>
একটি pointer, রাস্ট সবসময় জানে একটি Box<T>
-এর জন্য কতটুকু জায়গা প্রয়োজন: একটি pointer-এর সাইজ এটি যে পরিমাণ ডেটাকে নির্দেশ করছে তার উপর ভিত্তি করে পরিবর্তন হয় না। এর মানে হল আমরা Cons
ভ্যারিয়েন্টের ভিতরে সরাসরি আরেকটি List
ভ্যালুর পরিবর্তে একটি Box<T>
রাখতে পারি। Box<T>
পরবর্তী List
ভ্যালুটিকে নির্দেশ করবে যা Cons
ভ্যারিয়েন্টের ভিতরে না থেকে হিপ-এ থাকবে। ধারণাগতভাবে, আমাদের এখনও একটি লিস্ট আছে, যা অন্য লিস্ট ধারণকারী লিস্ট দিয়ে তৈরি, কিন্তু এই ইমপ্লিমেন্টেশনটি এখন আইটেমগুলোকে একে অপরের ভিতরে রাখার চেয়ে একে অপরের পাশে রাখার মতো।
আমরা Listing 15-2-এর List
enum-এর সংজ্ঞা এবং Listing 15-3-এর List
-এর ব্যবহার পরিবর্তন করে Listing 15-5-এর কোডে পরিণত করতে পারি, যা কম্পাইল হবে।
enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
Cons
ভ্যারিয়েন্টের জন্য একটি i32
-এর সাইজ এবং box-এর pointer ডেটা সংরক্ষণ করার জন্য জায়গার প্রয়োজন। Nil
ভ্যারিয়েন্ট কোনো ভ্যালু সংরক্ষণ করে না, তাই এটির জন্য Cons
ভ্যারিয়েন্টের চেয়ে স্ট্যাক-এ কম জায়গা প্রয়োজন। আমরা এখন জানি যে কোনো List
ভ্যালু একটি i32
-এর সাইজ এবং একটি box-এর pointer ডেটার সাইজ গ্রহণ করবে। একটি box ব্যবহার করে, আমরা অসীম, রিকার্সিভ চেইনটি ভেঙে দিয়েছি, তাই কম্পাইলার একটি List
ভ্যালু সংরক্ষণ করার জন্য প্রয়োজনীয় সাইজ বের করতে পারে। Figure 15-2 দেখাচ্ছে Cons
ভ্যারিয়েন্টটি এখন কেমন দেখায়।
Figure 15-2: একটি List
যা অসীম আকারের নয় কারণ Cons
একটি Box
ধারণ করে
Box শুধুমাত্র indirection এবং heap allocation প্রদান করে; তাদের অন্য কোনো বিশেষ ক্ষমতা নেই, যেমনটি আমরা অন্যান্য smart pointer টাইপের সাথে দেখব। তাদের সেই বিশেষ ক্ষমতাগুলির কারণে যে পারফরম্যান্স ওভারহেড হয় তাও তাদের নেই, তাই তারা cons list-এর মতো ক্ষেত্রে উপযোগী হতে পারে যেখানে indirection-ই আমাদের একমাত্র প্রয়োজন। আমরা Chapter 18-এ box-এর আরও ব্যবহারের ক্ষেত্র দেখব।
Box<T>
টাইপটি একটি smart pointer কারণ এটি Deref
ট্রেইট ইমপ্লিমেন্ট করে, যা Box<T>
ভ্যালুগুলোকে reference-এর মতো ব্যবহার করার অনুমতি দেয়। যখন একটি Box<T>
ভ্যালু স্কোপের বাইরে চলে যায়, তখন box যে হিপ ডেটাকে নির্দেশ করছে সেটিও Drop
ট্রেইট ইমপ্লিমেন্টেশনের কারণে পরিষ্কার হয়ে যায়। এই দুটি ট্রেইট এই অধ্যায়ের বাকি অংশে আমরা যে অন্যান্য smart pointer টাইপগুলো নিয়ে আলোচনা করব তাদের কার্যকারিতার জন্য আরও বেশি গুরুত্বপূর্ণ হবে। আসুন আমরা এই দুটি ট্রেইট আরও বিস্তারিতভাবে দেখি।
Deref
Trait ব্যবহার করে Smart Pointer-কে সাধারণ Reference-এর মতো ব্যবহার করা
Deref
ট্রেইট ইমপ্লিমেন্ট করার মাধ্যমে আপনি dereference operator *
(মাল্টিপ্লিকেশন বা glob অপারেটরের সাথে বিভ্রান্ত হবেন না) এর আচরণ কাস্টমাইজ করতে পারেন। Deref
এমনভাবে ইমপ্লিমেন্ট করার মাধ্যমে একটি smart pointer-কে সাধারণ reference-এর মতো ব্যবহার করা যায়, যার ফলে আপনি এমন কোড লিখতে পারবেন যা reference-এর উপর কাজ করে এবং সেই কোড smart pointer-এর সাথেও ব্যবহার করতে পারবেন।
চলুন প্রথমে দেখি dereference অপারেটর সাধারণ reference-এর সাথে কীভাবে কাজ করে। তারপর আমরা Box<T>
-এর মতো আচরণ করে এমন একটি কাস্টম টাইপ সংজ্ঞায়িত করার চেষ্টা করব এবং দেখব কেন dereference অপারেটর আমাদের নতুন সংজ্ঞায়িত টাইপের উপর reference-এর মতো কাজ করে না। আমরা দেখব কীভাবে Deref
ট্রেইট ইমপ্লিমেন্ট করা smart pointer-গুলোকে reference-এর মতো কাজ করতে সক্ষম করে। এরপর আমরা রাস্টের deref coercion ফিচারটি দেখব এবং জানব এটি কীভাবে আমাদের reference বা smart pointer উভয়ের সাথেই কাজ করতে দেয়।
Reference অনুসরণ করে ভ্যালু পর্যন্ত পৌঁছানো
একটি সাধারণ reference হলো এক ধরনের pointer, এবং একটি pointer-কে ভাবা যেতে পারে অন্য কোথাও সংরক্ষিত একটি ভ্যালুর দিকে নির্দেশকারী একটি তীর হিসাবে। Listing 15-6-এ, আমরা একটি i32
ভ্যালুর একটি reference তৈরি করেছি এবং তারপর dereference অপারেটর ব্যবহার করে reference অনুসরণ করে সেই ভ্যালু পর্যন্ত গিয়েছি।
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
x
ভেরিয়েবলটি একটি i32
ভ্যালু 5
ধারণ করে। আমরা y
-কে x
-এর একটি reference-এর সমান সেট করেছি। আমরা assert করতে পারি যে x
এর মান 5
। কিন্তু, যদি আমরা y
-এর ভ্যালু সম্পর্কে একটি assertion করতে চাই, তাহলে আমাদের *y
ব্যবহার করে reference-টিকে অনুসরণ করে তার নির্দেশিত ভ্যালু পর্যন্ত যেতে হবে (এজন্যই dereference) যাতে কম্পাইলার আসল ভ্যালুটি তুলনা করতে পারে। একবার আমরা y
-কে dereference করলে, আমরা y
-এর নির্দেশিত ইন্টিজার ভ্যালুটি অ্যাক্সেস করতে পারি এবং সেটিকে 5
-এর সাথে তুলনা করতে পারি।
যদি আমরা এর পরিবর্তে assert_eq!(5, y);
লেখার চেষ্টা করতাম, আমরা এই কম্পাইলেশন এররটি পেতাম:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
একটি সংখ্যা এবং একটি সংখ্যার reference-এর তুলনা করার অনুমতি নেই কারণ তারা ভিন্ন টাইপের। আমাদের অবশ্যই dereference অপারেটর ব্যবহার করে reference-টিকে তার নির্দেশিত ভ্যালু পর্যন্ত অনুসরণ করতে হবে।
Box<T>
-কে Reference-এর মতো ব্যবহার করা
আমরা Listing 15-6-এর কোডটি reference-এর পরিবর্তে Box<T>
ব্যবহার করে পুনরায় লিখতে পারি; Listing 15-7-এ Box<T>
-এর উপর ব্যবহৃত dereference অপারেটরটি Listing 15-6-এ reference-এর উপর ব্যবহৃত dereference অপারেটরের মতোই কাজ করে।
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
Listing 15-7 এবং Listing 15-6-এর মধ্যে প্রধান পার্থক্য হলো এখানে আমরা y
-কে x
-এর ভ্যালুকে নির্দেশকারী একটি reference-এর পরিবর্তে x
-এর একটি কপি করা ভ্যালুকে নির্দেশকারী একটি box-এর ইনস্ট্যান্স হিসাবে সেট করেছি। শেষ assertion-এ, আমরা box-এর pointer অনুসরণ করতে dereference অপারেটর ব্যবহার করতে পারি, ঠিক সেভাবেই যেভাবে আমরা y
যখন একটি reference ছিল তখন করেছিলাম। এরপর, আমরা দেখব Box<T>
-এর মধ্যে বিশেষ কী আছে যা আমাদের নিজস্ব box টাইপ সংজ্ঞায়িত করে dereference অপারেটর ব্যবহার করতে সক্ষম করে।
আমাদের নিজস্ব Smart Pointer তৈরি করা
চলুন, স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত Box<T>
টাইপের মতো একটি wrapper টাইপ তৈরি করি যাতে ডিফল্টভাবে smart pointer টাইপগুলো reference-এর থেকে কীভাবে ভিন্ন আচরণ করে তা অনুভব করা যায়। তারপর আমরা দেখব কীভাবে dereference অপারেটর ব্যবহার করার ক্ষমতা যোগ করা যায়।
দ্রষ্টব্য: আমরা যে
MyBox<T>
টাইপটি তৈরি করতে যাচ্ছি এবং আসলBox<T>
-এর মধ্যে একটি বড় পার্থক্য আছে: আমাদের সংস্করণটি তার ডেটা হিপ-এ সংরক্ষণ করবে না। আমরা এই উদাহরণেDeref
-এর উপর ফোকাস করছি, তাই ডেটা আসলে কোথায় সংরক্ষিত আছে তা pointer-এর মতো আচরণের চেয়ে কম গুরুত্বপূর্ণ।
Box<T>
টাইপটি মূলত একটি একটি উপাদান সহ একটি tuple struct হিসাবে সংজ্ঞায়িত করা হয়েছে, তাই Listing 15-8 একই ভাবে একটি MyBox<T>
টাইপ সংজ্ঞায়িত করে। আমরা Box<T>
-তে সংজ্ঞায়িত new
ফাংশনের সাথে মেলানোর জন্য একটি new
ফাংশনও সংজ্ঞায়িত করব।
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
আমরা MyBox
নামে একটি struct সংজ্ঞায়িত করি এবং একটি জেনেরিক প্যারামিটার T
ঘোষণা করি কারণ আমরা চাই আমাদের টাইপ যেকোনো টাইপের ভ্যালু ধারণ করুক। MyBox
টাইপটি T
টাইপের একটি উপাদান সহ একটি tuple struct। MyBox::new
ফাংশনটি T
টাইপের একটি প্যারামিটার নেয় এবং পাস করা ভ্যালুটি ধারণকারী একটি MyBox
ইনস্ট্যান্স রিটার্ন করে।
চলুন Listing 15-7-এর main
ফাংশনটি Listing 15-8-এ যোগ করার চেষ্টা করি এবং এটিকে Box<T>
-এর পরিবর্তে আমাদের সংজ্ঞায়িত MyBox<T>
টাইপ ব্যবহার করার জন্য পরিবর্তন করি। Listing 15-9-এর কোডটি কম্পাইল হবে না কারণ রাস্ট জানে না কীভাবে MyBox
-কে dereference করতে হয়।
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
এর ফলে এই কম্পাইলেশন এররটি আসে:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
আমাদের MyBox<T>
টাইপটি dereference করা যায় না কারণ আমরা আমাদের টাইপের উপর সেই ক্ষমতা ইমপ্লিমেন্ট করিনি। *
অপারেটর দিয়ে dereferencing সক্ষম করতে, আমরা Deref
ট্রেইট ইমপ্লিমেন্ট করি।
Deref
Trait ইমপ্লিমেন্ট করা
Chapter 10-এর ["Implementing a Trait on a Type"][impl-trait] অংশে যেমন আলোচনা করা হয়েছে, একটি ট্রেইট ইমপ্লিমেন্ট করার জন্য আমাদের ট্রেইটের প্রয়োজনীয় মেথডগুলির জন্য ইমপ্লিমেন্টেশন সরবরাহ করতে হবে। স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত Deref
ট্রেইটের জন্য আমাদের deref
নামে একটি মেথড ইমপ্লিমেন্ট করতে হবে যা self
borrow করে এবং ভেতরের ডেটার একটি reference রিটার্ন করে। Listing 15-10-এ MyBox<T>
-এর সংজ্ঞায় যোগ করার জন্য Deref
-এর একটি ইমপ্লিমেন্টেশন রয়েছে।
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
type Target = T;
সিনট্যাক্সটি Deref
ট্রেইটের ব্যবহারের জন্য একটি associated type সংজ্ঞায়িত করে। Associated type গুলো একটি জেনেরিক প্যারামিটার ঘোষণা করার একটি সামান্য ভিন্ন উপায়, কিন্তু আপনার এখন এগুলি নিয়ে চিন্তা করার দরকার নেই; আমরা Chapter 20-এ এগুলি সম্পর্কে আরও বিস্তারিতভাবে আলোচনা করব।
আমরা deref
মেথডের বডি &self.0
দিয়ে পূরণ করি যাতে deref
সেই ভ্যালুর একটি reference রিটার্ন করে যা আমরা *
অপারেটর দিয়ে অ্যাক্সেস করতে চাই; Chapter 5-এর ["Using Tuple Structs Without Named Fields to Create Different Types"][tuple-structs] থেকে স্মরণ করুন যে .0
একটি tuple struct-এর প্রথম ভ্যালু অ্যাক্সেস করে। Listing 15-9-এর main
ফাংশন যা MyBox<T>
ভ্যালুর উপর *
কল করে, তা এখন কম্পাইল হয় এবং assertion গুলো পাস করে!
Deref
ট্রেইট ছাড়া, কম্পাইলার শুধুমাত্র &
reference-গুলো dereference করতে পারে। deref
মেথড কম্পাইলারকে যেকোনো টাইপের ভ্যালু যা Deref
ইমপ্লিমেন্ট করে তা নিয়ে deref
মেথড কল করে একটি &
reference পাওয়ার ক্ষমতা দেয়, যা সে dereference করতে জানে।
যখন আমরা Listing 15-9-এ *y
লিখেছিলাম, পর্দার আড়ালে রাস্ট আসলে এই কোডটি রান করেছে:
*(y.deref())
রাস্ট *
অপারেটরটিকে deref
মেথডে একটি কল এবং তারপর একটি সাধারণ dereference দিয়ে প্রতিস্থাপন করে যাতে আমাদের deref
মেথড কল করার প্রয়োজন আছে কি না তা নিয়ে ভাবতে না হয়। রাস্টের এই ফিচারটি আমাদের এমন কোড লিখতে দেয় যা একইভাবে কাজ করে, আমাদের কাছে একটি সাধারণ reference থাকুক বা Deref
ইমপ্লিমেন্ট করা একটি টাইপ থাকুক।
deref
মেথড কেন একটি ভ্যালুর reference রিটার্ন করে, এবং *(y.deref())
-এর বন্ধনীর বাইরের সাধারণ dereference কেন এখনও প্রয়োজনীয়, তার কারণ ownership সিস্টেমের সাথে সম্পর্কিত। যদি deref
মেথড ভ্যালুর reference-এর পরিবর্তে সরাসরি ভ্যালুটি রিটার্ন করত, তাহলে ভ্যালুটি self
থেকে মুভ (move) হয়ে যেত। আমরা এই ক্ষেত্রে বা বেশিরভাগ ক্ষেত্রে যেখানে আমরা dereference অপারেটর ব্যবহার করি সেখানে MyBox<T>
-এর ভেতরের ভ্যালুর ownership নিতে চাই না।
মনে রাখবেন যে *
অপারেটরটি deref
মেথডে একটি কল এবং তারপর *
অপারেটরে একটি কল দ্বারা প্রতিস্থাপিত হয়, প্রতিবার যখন আমরা আমাদের কোডে একটি *
ব্যবহার করি। যেহেতু *
অপারেটরের প্রতিস্থাপন অসীমভাবে পুনরাবৃত্তি হয় না, তাই আমরা i32
টাইপের ডেটা পাই, যা Listing 15-9-এর assert_eq!
-তে 5
-এর সাথে মেলে।
ফাংশন এবং মেথডে স্বয়ংক্রিয় Deref Coercion
Deref coercion এমন একটি টাইপের reference-কে যা Deref
ট্রেইট ইমপ্লিমেন্ট করে, অন্য একটি টাইপের reference-এ রূপান্তরিত করে। উদাহরণস্বরূপ, deref coercion &String
-কে &str
-এ রূপান্তরিত করতে পারে কারণ String
Deref
ট্রেইট এমনভাবে ইমপ্লিমেন্ট করে যা &str
রিটার্ন করে। Deref coercion একটি সুবিধা যা রাস্ট ফাংশন এবং মেথডের আর্গুমেন্টের উপর প্রয়োগ করে এবং এটি শুধুমাত্র সেইসব টাইপের উপর কাজ করে যা Deref
ট্রেইট ইমপ্লিমেন্ট করে। এটি স্বয়ংক্রিয়ভাবে ঘটে যখন আমরা একটি নির্দিষ্ট টাইপের ভ্যালুর reference একটি ফাংশন বা মেথডের আর্গুমেন্ট হিসাবে পাস করি যা ফাংশন বা মেথড সংজ্ঞার প্যারামিটার টাইপের সাথে মেলে না। deref
মেথডে একাধিক কলের একটি ক্রম আমাদের দেওয়া টাইপটিকে প্যারামিটারের প্রয়োজনীয় টাইপে রূপান্তরিত করে।
Deref coercion রাস্ট-এ যোগ করা হয়েছিল যাতে ফাংশন এবং মেথড কল লেখার সময় প্রোগ্রামারদের &
এবং *
দিয়ে অনেক বেশি সুস্পষ্ট reference এবং dereference যোগ করার প্রয়োজন না হয়। deref coercion ফিচারটি আমাদের আরও বেশি কোড লিখতে দেয় যা reference বা smart pointer উভয়ের জন্য কাজ করতে পারে।
Deref coercion বাস্তবে দেখতে, আসুন আমরা Listing 15-8-এ সংজ্ঞায়িত MyBox<T>
টাইপ এবং Listing 15-10-এ যোগ করা Deref
-এর ইমপ্লিমেন্টেশন ব্যবহার করি। Listing 15-11 একটি ফাংশনের সংজ্ঞা দেখায় যার একটি স্ট্রিং স্লাইস প্যারামিটার রয়েছে।
fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {}
আমরা hello
ফাংশনটিকে একটি স্ট্রিং স্লাইস আর্গুমেন্ট দিয়ে কল করতে পারি, যেমন hello("Rust");
। Deref coercion hello
-কে MyBox<String>
টাইপের একটি ভ্যালুর reference দিয়ে কল করা সম্ভব করে, যেমনটি Listing 15-12-এ দেখানো হয়েছে।
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
এখানে আমরা hello
ফাংশনটিকে &m
আর্গুমেন্ট দিয়ে কল করছি, যা একটি MyBox<String>
ভ্যালুর একটি reference। যেহেতু আমরা Listing 15-10-এ MyBox<T>
-এর উপর Deref
ট্রেইট ইমপ্লিমেন্ট করেছি, তাই রাস্ট deref
কল করে &MyBox<String>
-কে &String
-এ পরিণত করতে পারে। স্ট্যান্ডার্ড লাইব্রেরি String
-এর উপর Deref
-এর একটি ইমপ্লিমেন্টেশন প্রদান করে যা একটি স্ট্রিং স্লাইস রিটার্ন করে, এবং এটি Deref
-এর API ডকুমেন্টেশনে রয়েছে। রাস্ট &String
-কে &str
-এ পরিণত করতে আবার deref
কল করে, যা hello
ফাংশনের সংজ্ঞার সাথে মেলে।
যদি রাস্ট deref coercion ইমপ্লিমেন্ট না করত, তাহলে hello
-কে &MyBox<String>
টাইপের একটি ভ্যালু দিয়ে কল করার জন্য আমাদের Listing 15-12-এর কোডের পরিবর্তে Listing 15-13-এর কোডটি লিখতে হতো।
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
(*m)
MyBox<String>
-কে dereference করে একটি String
-এ পরিণত করে। তারপর &
এবং [..]
String
-এর একটি স্ট্রিং স্লাইস নেয় যা hello
-এর সিগনেচারের সাথে মেলানোর জন্য পুরো স্ট্রিংয়ের সমান। এই সমস্ত চিহ্ন জড়িত থাকার কারণে deref coercion ছাড়া এই কোডটি পড়া, লেখা এবং বোঝা কঠিন। Deref coercion রাস্টকে এই রূপান্তরগুলি আমাদের জন্য স্বয়ংক্রিয়ভাবে পরিচালনা করার অনুমতি দেয়।
যখন জড়িত টাইপগুলির জন্য Deref
ট্রেইট সংজ্ঞায়িত করা হয়, রাস্ট টাইপগুলি বিশ্লেষণ করবে এবং প্যারামিটারের টাইপের সাথে মেলানোর জন্য একটি reference পেতে যতবার প্রয়োজন Deref::deref
ব্যবহার করবে। Deref::deref
কতবার সন্নিবেশ করা প্রয়োজন তা কম্পাইল টাইমে সমাধান করা হয়, তাই deref coercion-এর সুবিধা নেওয়ার জন্য কোনো রানটাইম পেনাল্টি নেই!
Deref Coercion এবং Mutability-র সম্পর্ক
আপনি যেভাবে immutable reference-এর উপর *
অপারেটর ওভাররাইড করতে Deref
ট্রেইট ব্যবহার করেন, সেভাবেই আপনি mutable reference-এর উপর *
অপারেটর ওভাররাইড করতে DerefMut
ট্রেইট ব্যবহার করতে পারেন।
রাস্ট তিনটি ক্ষেত্রে deref coercion করে যখন এটি টাইপ এবং ট্রেইট ইমপ্লিমেন্টেশন খুঁজে পায়:
&T
থেকে&U
যখনT: Deref<Target=U>
&mut T
থেকে&mut U
যখনT: DerefMut<Target=U>
&mut T
থেকে&U
যখনT: Deref<Target=U>
প্রথম দুটি ক্ষেত্র একই, শুধুমাত্র দ্বিতীয়টি mutability ইমপ্লিমেন্ট করে। প্রথম ক্ষেত্রটি বলে যে যদি আপনার কাছে একটি &T
থাকে, এবং T
কোনো টাইপ U
-এর জন্য Deref
ইমপ্লিমেন্ট করে, আপনি স্বচ্ছভাবে একটি &U
পেতে পারেন। দ্বিতীয় ক্ষেত্রটি বলে যে mutable reference-এর জন্য একই deref coercion ঘটে।
তৃতীয় ক্ষেত্রটি আরও জটিল: রাস্ট একটি mutable reference-কে একটি immutable reference-এও রূপান্তর করবে। কিন্তু এর বিপরীতটি সম্ভব নয়: immutable reference কখনও mutable reference-এ রূপান্তরিত হবে না। borrowing-এর নিয়ম অনুযায়ী, যদি আপনার কাছে একটি mutable reference থাকে, তবে সেই mutable reference-টি অবশ্যই সেই ডেটার একমাত্র reference হতে হবে (অন্যথায়, প্রোগ্রামটি কম্পাইল হবে না)। একটি mutable reference-কে একটি immutable reference-এ রূপান্তরিত করলে borrowing-এর নিয়ম কখনও ভাঙবে না। একটি immutable reference-কে একটি mutable reference-এ রূপান্তরিত করার জন্য প্রয়োজন হবে যে প্রাথমিক immutable reference-টি সেই ডেটার একমাত্র immutable reference, কিন্তু borrowing-এর নিয়ম তার নিশ্চয়তা দেয় না। অতএব, রাস্ট এই ধারণা করতে পারে না যে একটি immutable reference-কে একটি mutable reference-এ রূপান্তরিত করা সম্ভব।
Drop
ট্রেইট ব্যবহার করে পরিচ্ছন্নতার সময় কোড রান করা
Smart pointer প্যাটার্নের জন্য গুরুত্বপূর্ণ দ্বিতীয় ট্রেইটটি হলো Drop
, যা আপনাকে কাস্টমাইজ করতে দেয় যে একটি ভ্যালু স্কোপের বাইরে যাওয়ার সময় কী ঘটবে। আপনি যেকোনো টাইপের উপর Drop
ট্রেইটের জন্য একটি ইমপ্লিমেন্টেশন প্রদান করতে পারেন, এবং সেই কোডটি ফাইল বা নেটওয়ার্ক সংযোগের মতো রিসোর্স মুক্ত করতে ব্যবহার করা যেতে পারে।
আমরা smart pointer-এর প্রেক্ষাপটে Drop
ট্রেইটটি আলোচনা করছি কারণ একটি smart pointer ইমপ্লিমেন্ট করার সময় প্রায় সবসময়ই Drop
ট্রেইটের কার্যকারিতা ব্যবহার করা হয়। উদাহরণস্বরূপ, যখন একটি Box<T>
ড্রপ করা হয়, তখন এটি হিপ-এর সেই স্থানটি ডিঅ্যালোকেট করে যা বক্সটি নির্দেশ করে।
কিছু ভাষায়, কিছু নির্দিষ্ট টাইপের জন্য, প্রোগ্রামারকে প্রতিবার সেই টাইপের একটি ইনস্ট্যান্স ব্যবহার শেষ করার পরে মেমরি বা রিসোর্স মুক্ত করার জন্য কোড কল করতে হয়। এর উদাহরণ হলো ফাইল হ্যান্ডেল (file handles), সকেট (sockets) এবং লক (locks)। যদি তারা এটি করতে ভুলে যায়, সিস্টেম ওভারলোড হয়ে ক্র্যাশ করতে পারে। রাস্ট-এ, আপনি নির্দিষ্ট করতে পারেন যে একটি ভ্যালু স্কোপের বাইরে যাওয়ার সময় একটি নির্দিষ্ট কোড রান হবে, এবং কম্পাইলার এই কোডটি স্বয়ংক্রিয়ভাবে যোগ করে দেবে। ফলস্বরূপ, একটি প্রোগ্রামে যেখানে একটি নির্দিষ্ট টাইপের ইনস্ট্যান্সের কাজ শেষ হয়ে গেছে, সেখানে সর্বত্র পরিচ্ছন্নতার কোড রাখার বিষয়ে আপনাকে সতর্ক থাকতে হবে না—এবং আপনি রিসোর্স লিক করবেন না!
আপনি Drop
ট্রেইট ইমপ্লিমেন্ট করে স্কোপের বাইরে যাওয়ার সময় চালানোর জন্য কোড নির্দিষ্ট করেন। Drop
ট্রেইটের জন্য আপনাকে drop
নামের একটি মেথড ইমপ্লিমেন্ট করতে হবে যা self
-এর একটি mutable reference নেয়। রাস্ট কখন drop
কল করে তা দেখতে, চলুন আপাতত println!
স্টেটমেন্ট দিয়ে drop
ইমপ্লিমেন্ট করি।
Listing 15-14 একটি CustomSmartPointer
স্ট্রাকট দেখায় যার একমাত্র কাস্টম কার্যকারিতা হলো এটি Dropping CustomSmartPointer!
প্রিন্ট করবে যখন ইনস্ট্যান্সটি স্কোপের বাইরে যাবে, এটি দেখানোর জন্য যে রাস্ট কখন drop
মেথডটি রান করে।
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); }
Drop
ট্রেইটটি প্রিলিউডে (prelude) অন্তর্ভুক্ত, তাই আমাদের এটিকে স্কোপে আনার প্রয়োজন নেই। আমরা CustomSmartPointer
-এর উপর Drop
ট্রেইটটি ইমপ্লিমেন্ট করি এবং drop
মেথডের জন্য একটি ইমপ্লিমেন্টেশন প্রদান করি যা println!
কল করে। drop
মেথডের বডি হলো সেই জায়গা যেখানে আপনি আপনার টাইপের একটি ইনস্ট্যান্স স্কোপের বাইরে যাওয়ার সময় চালাতে চান এমন যেকোনো লজিক রাখবেন। আমরা এখানে কিছু টেক্সট প্রিন্ট করছি যাতে দৃশ্যমানভাবে দেখানো যায় যে রাস্ট কখন drop
কল করবে।
main
-এ, আমরা CustomSmartPointer
-এর দুটি ইনস্ট্যান্স তৈরি করি এবং তারপর CustomSmartPointers created.
প্রিন্ট করি। main
-এর শেষে, আমাদের CustomSmartPointer
-এর ইনস্ট্যান্সগুলি স্কোপের বাইরে চলে যাবে, এবং রাস্ট drop
মেথডে রাখা কোডটি কল করবে, আমাদের চূড়ান্ত বার্তাটি প্রিন্ট করবে। লক্ষ্য করুন যে আমাদের স্পষ্টভাবে drop
মেথড কল করতে হয়নি।
যখন আমরা এই প্রোগ্রামটি রান করব, আমরা নিম্নলিখিত আউটপুট দেখতে পাব:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
রাস্ট আমাদের জন্য স্বয়ংক্রিয়ভাবে drop
কল করেছে যখন আমাদের ইনস্ট্যান্সগুলো স্কোপের বাইরে চলে গেছে, এবং আমাদের নির্দিষ্ট করা কোডটি কল করেছে। ভেরিয়েবলগুলো তাদের তৈরির বিপরীত ক্রমে ড্রপ করা হয়, তাই d
কে c
-এর আগে ড্রপ করা হয়েছে। এই উদাহরণের উদ্দেশ্য হলো drop
মেথড কীভাবে কাজ করে তার একটি দৃশ্যমান ধারণা দেওয়া; সাধারণত আপনি একটি প্রিন্ট বার্তার পরিবর্তে আপনার টাইপের জন্য প্রয়োজনীয় পরিচ্ছন্নতার কোড নির্দিষ্ট করবেন।
std::mem::drop
ব্যবহার করে কোনো ভ্যালু আগে ড্রপ করা
দুর্ভাগ্যবশত, স্বয়ংক্রিয় drop
কার্যকারিতা নিষ্ক্রিয় করা সহজ নয়। drop
নিষ্ক্রিয় করা সাধারণত প্রয়োজনীয় নয়; Drop
ট্রেইটের মূল উদ্দেশ্যই হলো এটি স্বয়ংক্রিয়ভাবে যত্ন নেওয়া হয়। তবে, মাঝে মাঝে আপনি একটি ভ্যালু আগেভাগে পরিষ্কার করতে চাইতে পারেন। একটি উদাহরণ হলো যখন লক পরিচালনাকারী smart pointer ব্যবহার করা হয়: আপনি হয়তো লকটি ছেড়ে দেওয়ার জন্য drop
মেথডটিকে জোর করে কল করতে চাইতে পারেন যাতে একই স্কোপের অন্য কোড লকটি অর্জন করতে পারে। রাস্ট আপনাকে Drop
ট্রেইটের drop
মেথড ম্যানুয়ালি কল করতে দেয় না; পরিবর্তে, যদি আপনি একটি ভ্যালুকে তার স্কোপ শেষ হওয়ার আগে ড্রপ করতে বাধ্য করতে চান তবে আপনাকে স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত std::mem::drop
ফাংশনটি কল করতে হবে।
যদি আমরা Listing 15-14 থেকে main
ফাংশনটি পরিবর্তন করে Drop
ট্রেইটের drop
মেথডটি ম্যানুয়ালি কল করার চেষ্টা করি, যেমনটি Listing 15-15-এ দেখানো হয়েছে, আমরা একটি কম্পাইলার এরর পাব।
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
c.drop();
println!("CustomSmartPointer dropped before the end of main.");
}
যখন আমরা এই কোডটি কম্পাইল করার চেষ্টা করব, আমরা এই এররটি পাব:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 | drop(c);
| +++++ ~
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
এই এরর বার্তাটি বলছে যে আমাদের স্পষ্টভাবে drop
কল করার অনুমতি নেই। এরর বার্তাটিতে destructor শব্দটি ব্যবহার করা হয়েছে, যা একটি ইনস্ট্যান্স পরিষ্কার করার জন্য একটি ফাংশনের সাধারণ প্রোগ্রামিং পরিভাষা। একটি destructor একটি constructor-এর অনুরূপ, যা একটি ইনস্ট্যান্স তৈরি করে। রাস্টের drop
ফাংশন একটি নির্দিষ্ট destructor।
রাস্ট আমাদের স্পষ্টভাবে drop
কল করতে দেয় না কারণ রাস্ট main
-এর শেষে ভ্যালুটির উপর স্বয়ংক্রিয়ভাবে drop
কল করবে। এটি একটি double free এরর ঘটাবে কারণ রাস্ট একই ভ্যালু দুবার পরিষ্কার করার চেষ্টা করবে।
আমরা একটি ভ্যালু স্কোপের বাইরে যাওয়ার সময় drop
-এর স্বয়ংক্রিয় সন্নিবেশ নিষ্ক্রিয় করতে পারি না, এবং আমরা স্পষ্টভাবে drop
মেথড কল করতে পারি না। সুতরাং, যদি আমাদের একটি ভ্যালু আগেভাগে পরিষ্কার করতে বাধ্য করতে হয়, আমরা std::mem::drop
ফাংশনটি ব্যবহার করি।
std::mem::drop
ফাংশনটি Drop
ট্রেইটের drop
মেথড থেকে ভিন্ন। আমরা এটিকে আর্গুমেন্ট হিসাবে যে ভ্যালুটি জোর করে ড্রপ করতে চাই তা পাস করে কল করি। ফাংশনটি প্রিলিউডে রয়েছে, তাই আমরা Listing 15-15-এর main
পরিবর্তন করে drop
ফাংশনটি কল করতে পারি, যেমনটি Listing 15-16-এ দেখানো হয়েছে।
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); drop(c); println!("CustomSmartPointer dropped before the end of main."); }
এই কোডটি রান করলে নিম্নলিখিত প্রিন্ট হবে:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.
Dropping CustomSmartPointer with data `some data`!
লেখাটি CustomSmartPointer created.
এবং CustomSmartPointer dropped before the end of main.
লেখার মধ্যে প্রিন্ট হয়েছে, যা দেখাচ্ছে যে drop
মেথডের কোডটি সেই মুহূর্তে c
-কে ড্রপ করার জন্য কল করা হয়েছে।
আপনি পরিচ্ছন্নতাকে সুবিধাজনক এবং নিরাপদ করতে একটি Drop
ট্রেইট ইমপ্লিমেন্টেশনে নির্দিষ্ট করা কোড বিভিন্ন উপায়ে ব্যবহার করতে পারেন: উদাহরণস্বরূপ, আপনি এটি ব্যবহার করে আপনার নিজস্ব মেমরি অ্যালোকেটর তৈরি করতে পারেন! Drop
ট্রেইট এবং রাস্টের ownership সিস্টেমের সাথে, আপনাকে পরিষ্কার করার কথা মনে রাখতে হবে না কারণ রাস্ট এটি স্বয়ংক্রিয়ভাবে করে।
আপনাকে ভুলবশত এখনও ব্যবহৃত ভ্যালু পরিষ্কার করার ফলে সৃষ্ট সমস্যা নিয়েও চিন্তা করতে হবে না: ownership সিস্টেম যা নিশ্চিত করে যে reference-গুলো সর্বদা বৈধ, সেটিই নিশ্চিত করে যে drop
কেবল একবারই কল করা হয় যখন ভ্যালুটি আর ব্যবহৃত হচ্ছে না।
এখন যেহেতু আমরা Box<T>
এবং smart pointer-এর কিছু বৈশিষ্ট্য পরীক্ষা করেছি, চলুন স্ট্যান্ডার্ড লাইব্রেরিতে সংজ্ঞায়িত আরও কয়েকটি smart pointer দেখি।
Rc<T>
, রেফারেন্স কাউন্টেড স্মার্ট পয়েন্টার (Reference Counted Smart Pointer)
বেশিরভাগ ক্ষেত্রে, মালিকানা (ownership) স্পষ্ট থাকে: আপনি ঠিক জানেন কোন ভ্যারিয়েবল কোন ভ্যালুর মালিক। তবে, এমন কিছু ক্ষেত্র আছে যেখানে একটিমাত্র ভ্যালুর একাধিক মালিক থাকতে পারে। উদাহরণস্বরূপ, গ্রাফ ডেটা স্ট্রাকচারে, একাধিক এজ (edge) একই নোডকে (node) নির্দেশ করতে পারে, এবং সেই নোডটি ধারণাগতভাবে সেই সমস্ত এজের মালিকানাধীন থাকে যারা তাকে নির্দেশ করে। একটি নোড ততক্ষণ পর্যন্ত পরিষ্কার করা উচিত নয় যতক্ষণ না পর্যন্ত কোনো এজ তাকে নির্দেশ করছে এবং তার কোনো মালিক নেই।
আপনাকে রাস্টের Rc<T>
টাইপ ব্যবহার করে স্পষ্টভাবে একাধিক মালিকানা সক্রিয় করতে হবে, যা reference counting-এর সংক্ষিপ্ত রূপ। Rc<T>
টাইপটি একটি ভ্যালুর রেফারেন্সের সংখ্যা ট্র্যাক করে তা নির্ধারণ করার জন্য যে ভ্যালুটি এখনও ব্যবহৃত হচ্ছে কিনা। যদি একটি ভ্যালুর রেফারেন্স সংখ্যা শূন্য হয়, তবে কোনো রেফারেন্স অবৈধ না করেই ভ্যালুটি পরিষ্কার করা যেতে পারে।
Rc<T>
-কে একটি বসার ঘরের টিভির মতো কল্পনা করুন। যখন একজন ব্যক্তি টিভি দেখতে প্রবেশ করে, তখন সে টিভি চালু করে। অন্যরা ঘরে এসে টিভি দেখতে পারে। যখন শেষ ব্যক্তি ঘর থেকে বেরিয়ে যায়, তখন সে টিভি বন্ধ করে দেয় কারণ এটি আর ব্যবহৃত হচ্ছে না। যদি অন্য কেউ টিভি দেখার সময় টিভি বন্ধ করে দেয়, তবে বাকি টিভি দর্শকদের মধ্যে হৈচৈ পড়ে যাবে!
আমরা Rc<T>
টাইপটি ব্যবহার করি যখন আমরা আমাদের প্রোগ্রামের একাধিক অংশের জন্য হিপে কিছু ডেটা বরাদ্দ করতে চাই যা শুধু পড়া হবে এবং আমরা কম্পাইল টাইমে নির্ধারণ করতে পারি না কোন অংশটি ডেটা ব্যবহার করা শেষ করবে। যদি আমরা জানতাম কোন অংশটি শেষে শেষ করবে, আমরা কেবল সেই অংশটিকে ডেটার মালিক করতে পারতাম, এবং কম্পাইল টাইমে প্রয়োগ করা সাধারণ মালিকানার নিয়ম কার্যকর হত।
মনে রাখবেন Rc<T>
শুধুমাত্র সিঙ্গেল-থ্রেডেড (single-threaded) পরিস্থিতিতে ব্যবহারের জন্য। যখন আমরা Chapter 16-এ কনকারেন্সি (concurrency) নিয়ে আলোচনা করব, তখন আমরা মাল্টি-থ্রেডেড (multithreaded) প্রোগ্রামে কীভাবে রেফারেন্স কাউন্টিং করতে হয় তা দেখব।
Rc<T>
ব্যবহার করে ডেটা শেয়ার করা
চলুন Listing 15-5-এর আমাদের cons list-এর উদাহরণে ফিরে যাই। মনে করে দেখুন, আমরা এটি Box<T>
ব্যবহার করে সংজ্ঞায়িত করেছিলাম। এবার, আমরা দুটি লিস্ট তৈরি করব যারা উভয়েই তৃতীয় একটি লিস্টের মালিকানা শেয়ার করবে। ধারণাগতভাবে, এটি Figure 15-3-এর মতো দেখায়।
Figure 15-3: দুটি লিস্ট, b
এবং c
, তৃতীয় একটি লিস্ট a
-এর মালিকানা শেয়ার করছে
আমরা a
লিস্ট তৈরি করব যা 5
এবং তারপর 10
ধারণ করবে। তারপর আমরা আরও দুটি লিস্ট তৈরি করব: b
যা 3
দিয়ে শুরু হবে এবং c
যা 4
দিয়ে শুরু হবে। b
এবং c
উভয় লিস্টই তারপর প্রথম a
লিস্টে চলবে যা 5
এবং 10
ধারণ করে। অন্য কথায়, উভয় লিস্টই 5
এবং 10
ধারণকারী প্রথম লিস্টটি শেয়ার করবে।
Box<T>
দিয়ে আমাদের List
-এর সংজ্ঞা ব্যবহার করে এই পরিস্থিতিটি ইমপ্লিমেন্ট করার চেষ্টা করলে কাজ করবে না, যেমনটি Listing 15-17-এ দেখানো হয়েছে।
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
যখন আমরা এই কোডটি কম্পাইল করি, তখন আমরা এই এররটি পাই:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
Cons
ভ্যারিয়েন্টগুলো তাদের ধারণ করা ডেটার মালিক, তাই যখন আমরা b
লিস্ট তৈরি করি, a
কে b
-তে মুভ (move) করা হয় এবং b
a
-এর মালিক হয়ে যায়। তারপর, যখন আমরা c
তৈরি করার সময় আবার a
ব্যবহার করার চেষ্টা করি, তখন আমাদের অনুমতি দেওয়া হয় না কারণ a
মুভ হয়ে গেছে।
আমরা Cons
-এর সংজ্ঞা পরিবর্তন করে রেফারেন্স ধারণ করতে পারতাম, কিন্তু তাহলে আমাদের লাইফটাইম প্যারামিটার (lifetime parameters) নির্দিষ্ট করতে হতো। লাইফটাইম প্যারামিটার নির্দিষ্ট করার মাধ্যমে, আমরা নির্দিষ্ট করতাম যে লিস্টের প্রতিটি উপাদান অন্তত পুরো লিস্টের সমান সময়কাল বেঁচে থাকবে। Listing 15-17-এর উপাদান এবং লিস্টের ক্ষেত্রে এটি সত্য, কিন্তু সব পরিস্থিতিতে নয়।
এর পরিবর্তে, আমরা আমাদের List
-এর সংজ্ঞা পরিবর্তন করে Box<T>
-এর জায়গায় Rc<T>
ব্যবহার করব, যেমনটি Listing 15-18-এ দেখানো হয়েছে। প্রতিটি Cons
ভ্যারিয়েন্ট এখন একটি ভ্যালু এবং একটি List
-কে নির্দেশকারী একটি Rc<T>
ধারণ করবে। যখন আমরা b
তৈরি করব, তখন a
-এর মালিকানা নেওয়ার পরিবর্তে, আমরা a
-এর ধারণ করা Rc<List>
-কে ক্লোন করব, যার ফলে রেফারেন্সের সংখ্যা এক থেকে দুইয়ে বৃদ্ধি পাবে এবং a
এবং b
উভয়কেই সেই Rc<List>
-এর ডেটার মালিকানা শেয়ার করতে দেবে। আমরা c
তৈরি করার সময়ও a
কে ক্লোন করব, যার ফলে রেফারেন্সের সংখ্যা দুই থেকে তিনে বৃদ্ধি পাবে। প্রতিবার যখন আমরা Rc::clone
কল করব, Rc<List>
-এর ভেতরের ডেটার রেফারেন্স কাউন্ট বাড়বে, এবং ডেটা ততক্ষণ পর্যন্ত পরিষ্কার করা হবে না যতক্ষণ না পর্যন্ত তার রেফারেন্স সংখ্যা শূন্য হয়।
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }
আমাদের Rc<T>
-কে স্কোপে আনার জন্য একটি use
স্টেটমেন্ট যোগ করতে হবে কারণ এটি প্রিলিউডে (prelude) নেই। main
-এ, আমরা 5
এবং 10
ধারণকারী লিস্ট তৈরি করি এবং এটিকে a
-তে একটি নতুন Rc<List>
-এ সংরক্ষণ করি। তারপর, যখন আমরা b
এবং c
তৈরি করি, আমরা Rc::clone
ফাংশনটি কল করি এবং a
-এর Rc<List>
-এর একটি রেফারেন্স আর্গুমেন্ট হিসাবে পাস করি।
আমরা Rc::clone(&a)
-এর পরিবর্তে a.clone()
কল করতে পারতাম, কিন্তু রাস্টের কনভেনশন হলো এই ক্ষেত্রে Rc::clone
ব্যবহার করা। Rc::clone
-এর ইমপ্লিমেন্টেশন বেশিরভাগ টাইপের clone
ইমপ্লিমেন্টেশনের মতো সমস্ত ডেটার একটি ডিপ কপি (deep copy) তৈরি করে না। Rc::clone
-এর কল শুধুমাত্র রেফারেন্স কাউন্ট বাড়ায়, যা খুব বেশি সময় নেয় না। ডেটার ডিপ কপি অনেক সময় নিতে পারে। রেফারেন্স কাউন্টিংয়ের জন্য Rc::clone
ব্যবহার করে, আমরা ডিপ-কপি ধরনের ক্লোন এবং রেফারেন্স কাউন্ট বাড়ায় এমন ধরনের ক্লোনের মধ্যে দৃশ্যমানভাবে পার্থক্য করতে পারি। কোডে পারফরম্যান্স সমস্যা খোঁজার সময়, আমাদের কেবল ডিপ-কপি ক্লোনগুলো বিবেচনা করতে হবে এবং Rc::clone
-এর কলগুলোকে উপেক্ষা করা যেতে পারে।
একটি Rc<T>
ক্লোন করা রেফারেন্স কাউন্ট বাড়ায়
চলুন Listing 15-18-এর আমাদের কার্যকরী উদাহরণটি পরিবর্তন করি যাতে আমরা দেখতে পারি a
-তে থাকা Rc<List>
-এর রেফারেন্স তৈরি এবং ড্রপ করার সাথে সাথে রেফারেন্স কাউন্ট কীভাবে পরিবর্তিত হয়।
Listing 15-19-এ, আমরা main
-কে পরিবর্তন করব যাতে c
লিস্টের চারপাশে একটি অভ্যন্তরীণ স্কোপ থাকে; তাহলে আমরা দেখতে পাব c
স্কোপের বাইরে চলে গেলে রেফারেন্স কাউন্ট কীভাবে পরিবর্তিত হয়।
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; // --snip-- fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); }
প্রোগ্রামের প্রতিটি পয়েন্টে যেখানে রেফারেন্স কাউন্ট পরিবর্তিত হয়, আমরা রেফারেন্স কাউন্ট প্রিন্ট করি, যা আমরা Rc::strong_count
ফাংশন কল করে পাই। এই ফাংশনটির নাম strong_count
কারণ Rc<T>
টাইপের একটি weak_count
-ও আছে; আমরা [“Weak<T>
ব্যবহার করে রেফারেন্স সাইকেল প্রতিরোধ করা”][preventing-ref-cycles] অংশে দেখব weak_count
কীসের জন্য ব্যবহৃত হয়।
এই কোডটি নিম্নলিখিত প্রিন্ট করে:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
আমরা দেখতে পাচ্ছি যে a
-তে থাকা Rc<List>
-এর প্রাথমিক রেফারেন্স কাউন্ট 1; তারপর প্রতিবার যখন আমরা clone
কল করি, কাউন্ট 1 করে বেড়ে যায়। যখন c
স্কোপের বাইরে চলে যায়, কাউন্ট 1 করে কমে যায়। রেফারেন্স কাউন্ট বাড়ানোর জন্য যেমন আমাদের Rc::clone
কল করতে হয়, তেমন রেফারেন্স কাউন্ট কমানোর জন্য আমাদের কোনো ফাংশন কল করতে হয় না: Drop
ট্রেইটের ইমপ্লিমেন্টেশন স্বয়ংক্রিয়ভাবে রেফারেন্স কাউন্ট কমিয়ে দেয় যখন একটি Rc<T>
ভ্যালু স্কোপের বাইরে চলে যায়।
এই উদাহরণে আমরা যা দেখতে পাচ্ছি না তা হলো, main
-এর শেষে যখন b
এবং তারপর a
স্কোপের বাইরে চলে যায়, তখন কাউন্ট 0 হয়ে যায়, এবং Rc<List>
সম্পূর্ণরূপে পরিষ্কার হয়ে যায়। Rc<T>
ব্যবহার করে একটিমাত্র ভ্যালুর একাধিক মালিক থাকতে পারে, এবং কাউন্ট নিশ্চিত করে যে ভ্যালুটি ততক্ষণ পর্যন্ত বৈধ থাকবে যতক্ষণ পর্যন্ত কোনো মালিক বিদ্যমান থাকে।
অপরিবর্তনশীল রেফারেন্সের (immutable references) মাধ্যমে, Rc<T>
আপনাকে আপনার প্রোগ্রামের একাধিক অংশের মধ্যে শুধুমাত্র পড়ার জন্য ডেটা শেয়ার করার অনুমতি দেয়। যদি Rc<T>
আপনাকে একাধিক পরিবর্তনশীল রেফারেন্সও (mutable references) রাখার অনুমতি দিত, তাহলে আপনি Chapter 4-এ আলোচিত ধার নেওয়ার নিয়মগুলোর (borrowing rules) একটি লঙ্ঘন করতে পারতেন: একই জায়গায় একাধিক পরিবর্তনশীল ধার ডেটা রেস (data races) এবং অসামঞ্জস্যের কারণ হতে পারে। কিন্তু ডেটা পরিবর্তন করতে পারা খুবই দরকারী! পরবর্তী বিভাগে, আমরা ইন্টেরিয়র মিউটেবিলিটি (interior mutability) প্যাটার্ন এবং RefCell<T>
টাইপ নিয়ে আলোচনা করব যা আপনি এই অপরিবর্তনীয়তার সীমাবদ্ধতার সাথে কাজ করার জন্য Rc<T>
-এর সাথে একত্রে ব্যবহার করতে পারেন।
RefCell<T>
এবং ইন্টেরিয়র মিউটেবিলিটি প্যাটার্ন (Interior Mutability Pattern)
Interior mutability রাস্টের একটি ডিজাইন প্যাটার্ন যা আপনাকে ডেটা পরিবর্তন করার অনুমতি দেয়, এমনকি যখন সেই ডেটার immutable reference থাকে; সাধারণত, borrowing-এর নিয়ম অনুযায়ী এই কাজটি নিষিদ্ধ। ডেটা পরিবর্তন করার জন্য, এই প্যাটার্নটি একটি ডেটা স্ট্রাকচারের ভিতরে unsafe
কোড ব্যবহার করে রাস্টের স্বাভাবিক নিয়মাবলী, যা পরিবর্তন এবং borrowing নিয়ন্ত্রণ করে, সেগুলোকে কিছুটা বাঁকিয়ে দেয়। unsafe
কোড কম্পাইলারকে নির্দেশ করে যে আমরা নিয়মগুলো ম্যানুয়ালি পরীক্ষা করছি, কম্পাইলারের উপর নির্ভর না করে; আমরা Chapter 20-এ unsafe
কোড নিয়ে আরও আলোচনা করব।
আমরা শুধুমাত্র তখনই interior mutability প্যাটার্ন ব্যবহারকারী টাইপগুলো ব্যবহার করতে পারি যখন আমরা নিশ্চিত করতে পারি যে borrowing-এর নিয়মগুলো রানটাইমে অনুসরণ করা হবে, যদিও কম্পাইলার এর গ্যারান্টি দিতে পারে না। ব্যবহৃত unsafe
কোডটি তখন একটি নিরাপদ API-এর মধ্যে মোড়ানো থাকে, এবং বাইরের টাইপটি তখনও immutable থাকে।
চলুন, RefCell<T>
টাইপটি দেখে এই ধারণাটি অন্বেষেষণ করি, যা interior mutability প্যাটার্ন অনুসরণ করে।
RefCell<T>
দিয়ে রানটাইমে Borrowing-এর নিয়ম প্রয়োগ করা
Rc<T>
-এর মতো নয়, RefCell<T>
টাইপটি তার ধারণ করা ডেটার উপর একক মালিকানা (single ownership) প্রতিনিধিত্ব করে। তাহলে Box<T>
-এর মতো টাইপ থেকে RefCell<T>
কীভাবে আলাদা? Chapter 4-এ শেখা borrowing-এর নিয়মগুলো মনে করুন:
- যেকোনো নির্দিষ্ট সময়ে, আপনার কাছে হয় একটি mutable reference অথবা যেকোনো সংখ্যক immutable reference থাকতে পারে (কিন্তু উভয়ই নয়)।
- Reference সবসময় বৈধ (valid) হতে হবে।
Reference এবং Box<T>
-এর ক্ষেত্রে, borrowing-এর নিয়মের এই শর্তগুলো কম্পাইল টাইমে (compile time) প্রয়োগ করা হয়। RefCell<T>
-এর ক্ষেত্রে, এই শর্তগুলো রানটাইমে (runtime) প্রয়োগ করা হয়। Reference-এর সাথে, আপনি যদি এই নিয়মগুলো ভঙ্গ করেন, তাহলে আপনি একটি কম্পাইলার এরর পাবেন। RefCell<T>
-এর সাথে, আপনি যদি এই নিয়মগুলো ভঙ্গ করেন, আপনার প্রোগ্রামটি প্যানিক (panic) করবে এবং বন্ধ হয়ে যাবে।
কম্পাইল টাইমে borrowing-এর নিয়ম পরীক্ষা করার সুবিধা হলো যে এররগুলো ডেভেলপমেন্ট প্রক্রিয়ার শুরুতেই ধরা পড়ে, এবং রানটাইম পারফরম্যান্সের উপর কোনো প্রভাব পড়ে না কারণ সমস্ত বিশ্লেষণ আগে থেকেই সম্পন্ন হয়। এই কারণগুলোর জন্য, বেশিরভাগ ক্ষেত্রে কম্পাইল টাইমে borrowing-এর নিয়ম পরীক্ষা করাই সেরা পছন্দ, আর একারণেই এটি রাস্টের ডিফল্ট আচরণ।
এর পরিবর্তে রানটাইমে borrowing-এর নিয়ম পরীক্ষা করার সুবিধা হলো যে কিছু মেমরি-সেফ (memory-safe) পরিস্থিতি তখন অনুমোদিত হয়, যা কম্পাইল-টাইম চেক দ্বারা নিষিদ্ধ হতো। স্ট্যাটিক অ্যানালাইসিস (Static analysis), যেমন রাস্ট কম্পাইলার, স্বভাবতই রক্ষণশীল (conservative)। কোডের কিছু বৈশিষ্ট্য কোড বিশ্লেষণ করে সনাক্ত করা অসম্ভব: সবচেয়ে বিখ্যাত উদাহরণ হলো Halting Problem, যা এই বইয়ের আওতার বাইরে কিন্তু গবেষণার জন্য একটি আকর্ষণীয় বিষয়।
যেহেতু কিছু বিশ্লেষণ অসম্ভব, তাই যদি রাস্ট কম্পাইলার নিশ্চিত না হতে পারে যে কোডটি ownership-এর নিয়ম মেনে চলে, তবে এটি একটি সঠিক প্রোগ্রাম প্রত্যাখ্যান করতে পারে; এইভাবে, এটি রক্ষণশীল। যদি রাস্ট একটি ভুল প্রোগ্রাম গ্রহণ করত, ব্যবহারকারীরা রাস্টের দেওয়া গ্যারান্টির উপর বিশ্বাস রাখতে পারত না। তবে, যদি রাস্ট একটি সঠিক প্রোগ্রাম প্রত্যাখ্যান করে, প্রোগ্রামার অসুবিধায় পড়বে, কিন্তু কোনো বিপর্যয় ঘটবে না। RefCell<T>
টাইপটি উপযোগী যখন আপনি নিশ্চিত যে আপনার কোড borrowing-এর নিয়ম অনুসরণ করে কিন্তু কম্পাইলার তা বুঝতে এবং গ্যারান্টি দিতে অক্ষম।
Rc<T>
-এর মতো, RefCell<T>
শুধুমাত্র সিঙ্গেল-থ্রেডেড পরিস্থিতিতে ব্যবহারের জন্য এবং আপনি যদি এটি মাল্টি-থ্রেডেড কনটেক্সটে ব্যবহার করার চেষ্টা করেন তবে এটি আপনাকে একটি কম্পাইল-টাইম এরর দেবে। আমরা Chapter 16-এ একটি মাল্টি-থ্রেডেড প্রোগ্রামে RefCell<T>
-এর কার্যকারিতা কীভাবে পাওয়া যায় সে সম্পর্কে কথা বলব।
Box<T>
, Rc<T>
, বা RefCell<T>
বেছে নেওয়ার কারণগুলোর একটি সারসংক্ষেপ নিচে দেওয়া হলো:
Rc<T>
একই ডেটার একাধিক owner সক্ষম করে;Box<T>
এবংRefCell<T>
-এর একক owner থাকে।Box<T>
কম্পাইল টাইমে চেক করা immutable বা mutable borrow-এর অনুমতি দেয়;Rc<T>
শুধুমাত্র কম্পাইল টাইমে চেক করা immutable borrow-এর অনুমতি দেয়;RefCell<T>
রানটাইমে চেক করা immutable বা mutable borrow-এর অনুমতি দেয়।- যেহেতু
RefCell<T>
রানটাইমে চেক করা mutable borrow-এর অনুমতি দেয়, তাই আপনিRefCell<T>
-এর ভেতরের ভ্যালুটি পরিবর্তন করতে পারেন এমনকি যখনRefCell<T>
-টি immutable থাকে।
একটি immutable ভ্যালুর ভেতরের ভ্যালু পরিবর্তন করাই হলো interior mutability প্যাটার্ন। চলুন এমন একটি পরিস্থিতি দেখি যেখানে interior mutability উপযোগী এবং এটি কীভাবে সম্ভব তা পরীক্ষা করি।
ইন্টেরিয়র মিউটেবিলিটি: একটি Immutable ভ্যালুর জন্য Mutable Borrow
Borrowing-এর নিয়মের একটি ফলাফল হলো যখন আপনার কাছে একটি immutable ভ্যালু থাকে, আপনি এটিকে mutable-ভাবে borrow করতে পারবেন না। উদাহরণস্বরূপ, এই কোডটি কম্পাইল হবে না:
fn main() {
let x = 5;
let y = &mut x;
}
আপনি যদি এই কোডটি কম্পাইল করার চেষ্টা করতেন, আপনি নিম্নলিখিত এররটি পেতেন:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
তবে, এমন পরিস্থিতি আছে যেখানে একটি ভ্যালুর জন্য তার মেথডগুলোতে নিজেকে পরিবর্তন করা উপযোগী হবে কিন্তু অন্য কোডের কাছে এটি immutable বলে মনে হবে। ভ্যালুর মেথডগুলোর বাইরের কোড ভ্যালুটি পরিবর্তন করতে পারবে না। RefCell<T>
ব্যবহার করা interior mutability-র ক্ষমতা পাওয়ার একটি উপায়, কিন্তু RefCell<T>
borrowing-এর নিয়মগুলো পুরোপুরি এড়িয়ে যায় না: কম্পাইলারের borrow checker এই interior mutability-কে অনুমতি দেয়, এবং borrowing-এর নিয়মগুলো রানটাইমে পরীক্ষা করা হয়। যদি আপনি নিয়ম লঙ্ঘন করেন, তাহলে আপনি কম্পাইলার এররের পরিবর্তে একটি panic!
পাবেন।
চলুন একটি বাস্তব উদাহরণ দেখি যেখানে আমরা RefCell<T>
ব্যবহার করে একটি immutable ভ্যালু পরিবর্তন করতে পারি এবং দেখি কেন এটি উপযোগী।
ইন্টেরিয়র মিউটেবিলিটির একটি ব্যবহার: মক অবজেক্ট (Mock Objects)
কখনও কখনও টেস্টিংয়ের সময় একজন প্রোগ্রামার একটি নির্দিষ্ট আচরণ পর্যবেক্ষণ করতে এবং এটি সঠিকভাবে ইমপ্লিমেন্ট করা হয়েছে কিনা তা নিশ্চিত করতে অন্য একটি টাইপের জায়গায় একটি টাইপ ব্যবহার করেন। এই placeholder টাইপটিকে বলা হয় test double। এটিকে ফিল্মমেকিং-এর স্টান্ট ডাবলের মতো ভাবুন, যেখানে একজন ব্যক্তি একটি বিশেষভাবে কঠিন দৃশ্যের জন্য একজন অভিনেতার পরিবর্তে কাজ করে। আমরা যখন টেস্ট চালাই তখন টেস্ট ডাবলগুলো অন্য টাইপের জন্য দাঁড়িয়ে থাকে। Mock objects হলো বিশেষ ধরনের টেস্ট ডাবল যা একটি টেস্টের সময় কী ঘটে তা রেকর্ড করে যাতে আপনি assert করতে পারেন যে সঠিক কাজগুলো হয়েছে।
অন্যান্য ভাষায় যেমন অবজেক্ট আছে, রাস্ট-এ সেই অর্থে অবজেক্ট নেই, এবং রাস্টের স্ট্যান্ডার্ড লাইব্রেরিতে অন্য কিছু ভাষার মতো মক অবজেক্ট কার্যকারিতা বিল্ট-ইন নেই। তবে, আপনি অবশ্যই একটি struct তৈরি করতে পারেন যা একটি মক অবজেক্টের মতো একই উদ্দেশ্যে কাজ করবে।
এখানে আমরা যে পরিস্থিতিটি পরীক্ষা করব: আমরা একটি লাইব্রেরি তৈরি করব যা একটি সর্বোচ্চ মানের (maximum value) বিপরীতে একটি মান ট্র্যাক করে এবং বর্তমান মানটি সর্বোচ্চ মানের কতটা কাছাকাছি তার উপর ভিত্তি করে বার্তা পাঠায়। এই লাইব্রেরিটি একজন ব্যবহারকারীর API কল করার কোটা ট্র্যাক করতে ব্যবহার করা যেতে পারে, উদাহরণস্বরূপ।
আমাদের লাইব্রেরি শুধুমাত্র একটি মান সর্বোচ্চ মানের কতটা কাছাকাছি তা ট্র্যাক করার কার্যকারিতা এবং কোন সময়ে কী বার্তা হওয়া উচিত তা সরবরাহ করবে। আমাদের লাইব্রেরি ব্যবহারকারী অ্যাপ্লিকেশনগুলো থেকে বার্তা পাঠানোর ব্যবস্থা সরবরাহ করার আশা করা হবে: অ্যাপ্লিকেশনটি অ্যাপ্লিকেশনে একটি বার্তা রাখতে পারে, একটি ইমেল পাঠাতে পারে, একটি টেক্সট বার্তা পাঠাতে পারে, বা অন্য কিছু করতে পারে। লাইব্রেরিকে সেই বিস্তারিত জানার প্রয়োজন নেই। এটির যা প্রয়োজন তা হলো এমন কিছু যা আমরা Messenger
নামে একটি ট্রেইট সরবরাহ করব তা ইমপ্লিমেন্ট করে। Listing 15-20 লাইব্রেরি কোডটি দেখায়।
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
এই কোডের একটি গুরুত্বপূর্ণ অংশ হলো Messenger
ট্রেইটের send
নামে একটি মেথড আছে যা self
-এর একটি immutable reference এবং বার্তার টেক্সট নেয়। এই ট্রেইটটি হলো সেই ইন্টারফেস যা আমাদের মক অবজেক্টকে ইমপ্লিমেন্ট করতে হবে যাতে মকটি একটি আসল অবজেক্টের মতো একইভাবে ব্যবহার করা যায়। অন্য গুরুত্বপূর্ণ অংশ হলো আমরা LimitTracker
-এর set_value
মেথডের আচরণ পরীক্ষা করতে চাই। আমরা value
প্যারামিটারের জন্য যা পাস করি তা পরিবর্তন করতে পারি, কিন্তু set_value
আমাদের assert করার জন্য কিছু রিটার্ন করে না। আমরা বলতে চাই যে যদি আমরা Messenger
ট্রেইট ইমপ্লিমেন্ট করে এমন কিছু এবং max
-এর জন্য একটি নির্দিষ্ট মান দিয়ে একটি LimitTracker
তৈরি করি, যখন আমরা value
-এর জন্য বিভিন্ন সংখ্যা পাস করি তখন মেসেঞ্জারকে উপযুক্ত বার্তা পাঠাতে বলা হয়।
আমাদের একটি মক অবজেক্ট দরকার যা, send
কল করার সময় ইমেল বা টেক্সট বার্তা পাঠানোর পরিবর্তে, শুধুমাত্র তাকে যে বার্তাগুলো পাঠাতে বলা হয়েছে সেগুলো ট্র্যাক করবে। আমরা মক অবজেক্টের একটি নতুন ইনস্ট্যান্স তৈরি করতে পারি, মক অবজেক্ট ব্যবহার করে এমন একটি LimitTracker
তৈরি করতে পারি, LimitTracker
-এর set_value
মেথড কল করতে পারি, এবং তারপর পরীক্ষা করতে পারি যে মক অবজেক্টে আমাদের প্রত্যাশিত বার্তাগুলো আছে কিনা। Listing 15-21 একটি মক অবজেক্ট ইমপ্লিমেন্ট করার একটি প্রচেষ্টা দেখায়, কিন্তু borrow checker এটি অনুমোদন করবে না।
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
এই টেস্ট কোডটি একটি MockMessenger
struct সংজ্ঞায়িত করে যার sent_messages
নামে একটি ফিল্ড আছে যেখানে Vec<String>
মানের একটি ভেক্টর রয়েছে যা তাকে পাঠানো বার্তাগুলো ট্র্যাক করার জন্য। আমরা একটি new
নামের associated function-ও সংজ্ঞায়িত করি যাতে খালি বার্তা তালিকা দিয়ে নতুন MockMessenger
মান তৈরি করা সুবিধাজনক হয়। তারপর আমরা MockMessenger
-এর জন্য Messenger
ট্রেইট ইমপ্লিমেন্ট করি যাতে আমরা একটি MockMessenger
-কে একটি LimitTracker
-কে দিতে পারি। send
মেথডের সংজ্ঞায়, আমরা প্যারামিটার হিসাবে পাস করা বার্তাটি নিই এবং এটিকে MockMessenger
-এর sent_messages
তালিকায় সংরক্ষণ করি।
টেস্টে, আমরা পরীক্ষা করছি যে LimitTracker
-কে value
এমন কিছুতে সেট করতে বলা হলে কী হয় যা max
মানের ৭৫ শতাংশের বেশি। প্রথমে আমরা একটি নতুন MockMessenger
তৈরি করি, যা একটি খালি বার্তা তালিকা দিয়ে শুরু হবে। তারপর আমরা একটি নতুন LimitTracker
তৈরি করি এবং এটিকে নতুন MockMessenger
-এর একটি রেফারেন্স এবং 100
-এর একটি max
মান দিই। আমরা LimitTracker
-এর set_value
মেথডটি 80
মান দিয়ে কল করি, যা ১০০-এর ৭৫ শতাংশের বেশি। তারপর আমরা assert করি যে MockMessenger
যে বার্তাগুলোর তালিকা ট্র্যাক করছে তাতে এখন একটি বার্তা থাকা উচিত।
তবে, এই টেস্টে একটি সমস্যা আছে, যেমনটি এখানে দেখানো হয়েছে:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
আমরা MockMessenger
-কে বার্তাগুলোর ট্র্যাক রাখার জন্য মডিফাই করতে পারি না কারণ send
মেথডটি self
-এর একটি immutable reference নেয়। আমরা এরর টেক্সট থেকে &mut self
ব্যবহার করার পরামর্শটিও নিতে পারি না। আমরা শুধুমাত্র টেস্টিংয়ের জন্য Messenger
ট্রেইট পরিবর্তন করতে চাই না। পরিবর্তে, আমাদের বিদ্যমান ডিজাইনের সাথে আমাদের টেস্ট কোড সঠিকভাবে কাজ করার একটি উপায় খুঁজে বের করতে হবে।
এটি এমন একটি পরিস্থিতি যেখানে ইন্টেরিয়র মিউটেবিলিটি সাহায্য করতে পারে! আমরা sent_messages
-কে একটি RefCell<T>
-এর মধ্যে সংরক্ষণ করব, এবং তারপর send
মেথড sent_messages
পরিবর্তন করতে সক্ষম হবে যাতে আমরা যে বার্তাগুলো দেখেছি তা সংরক্ষণ করতে পারে। Listing 15-22 দেখাচ্ছে এটি কেমন দেখায়।
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
sent_messages
ফিল্ডটি এখন Vec<String>
-এর পরিবর্তে RefCell<Vec<String>>
টাইপের। new
ফাংশনে, আমরা খালি ভেক্টরের চারপাশে একটি নতুন RefCell<Vec<String>>
ইনস্ট্যান্স তৈরি করি।
send
মেথডের ইমপ্লিমেন্টেশনের জন্য, প্রথম প্যারামিটারটি এখনও self
-এর একটি immutable borrow, যা ট্রেইট সংজ্ঞার সাথে মেলে। আমরা self.sent_messages
-এর RefCell<Vec<String>>
-এর উপর borrow_mut
কল করি যাতে RefCell<Vec<String>>
-এর ভেতরের ভ্যালু, যা হলো ভেক্টর, তার একটি mutable reference পেতে পারি। তারপর আমরা ভেক্টরের mutable reference-এর উপর push
কল করতে পারি যাতে টেস্টের সময় পাঠানো বার্তাগুলো ট্র্যাক রাখা যায়।
শেষ যে পরিবর্তনটি আমাদের করতে হবে তা হলো assertion-এ: ভেতরের ভেক্টরে কতগুলো আইটেম আছে তা দেখতে, আমরা RefCell<Vec<String>>
-এর উপর borrow
কল করি যাতে ভেক্টরের একটি immutable reference পেতে পারি।
এখন যেহেতু আপনি RefCell<T>
কীভাবে ব্যবহার করতে হয় তা দেখেছেন, আসুন আমরা দেখি এটি কীভাবে কাজ করে!
RefCell<T>
এর মাধ্যমে রানটাইমে Borrow ট্র্যাক রাখা
Immutable এবং mutable reference তৈরি করার সময়, আমরা যথাক্রমে &
এবং &mut
সিনট্যাক্স ব্যবহার করি। RefCell<T>
-এর সাথে, আমরা borrow
এবং borrow_mut
মেথড ব্যবহার করি, যা RefCell<T>
-এর নিরাপদ API-এর অংশ। borrow
মেথডটি স্মার্ট পয়েন্টার টাইপ Ref<T>
রিটার্ন করে, এবং borrow_mut
স্মার্ট পয়েন্টার টাইপ RefMut<T>
রিটার্ন করে। উভয় টাইপই Deref
ইমপ্লিমেন্ট করে, তাই আমরা তাদের সাধারণ reference-এর মতো ব্যবহার করতে পারি।
RefCell<T>
ট্র্যাক রাখে যে বর্তমানে কতগুলো Ref<T>
এবং RefMut<T>
স্মার্ট পয়েন্টার সক্রিয় আছে। প্রতিবার যখন আমরা borrow
কল করি, RefCell<T>
তার সক্রিয় immutable borrow-এর সংখ্যা বাড়িয়ে দেয়। যখন একটি Ref<T>
ভ্যালু স্কোপের বাইরে চলে যায়, immutable borrow-এর সংখ্যা ১ কমে যায়। ঠিক কম্পাইল-টাইম borrowing নিয়মের মতোই, RefCell<T>
আমাদের যেকোনো সময়ে অনেকগুলো immutable borrow অথবা একটি mutable borrow রাখার অনুমতি দেয়।
যদি আমরা এই নিয়মগুলো লঙ্ঘন করার চেষ্টা করি, তাহলে reference-এর ক্ষেত্রে যেমন কম্পাইলার এরর পেতাম, তার পরিবর্তে RefCell<T>
-এর ইমপ্লিমেন্টেশন রানটাইমে প্যানিক করবে। Listing 15-23 Listing 15-22-এর send
-এর ইমপ্লিমেন্টেশনের একটি পরিবর্তন দেখায়। আমরা ইচ্ছাকৃতভাবে একই স্কোপের জন্য দুটি সক্রিয় mutable borrow তৈরি করার চেষ্টা করছি যাতে দেখানো যায় যে RefCell<T>
আমাদের রানটাইমে এটি করা থেকে বিরত রাখে।
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
আমরা borrow_mut
থেকে রিটার্ন করা RefMut<T>
স্মার্ট পয়েন্টারের জন্য one_borrow
নামে একটি ভ্যারিয়েবল তৈরি করি। তারপর আমরা two_borrow
ভ্যারিয়েবলে একইভাবে আরেকটি mutable borrow তৈরি করি। এটি একই স্কোপে দুটি mutable reference তৈরি করে, যা অনুমোদিত নয়। যখন আমরা আমাদের লাইব্রেরির জন্য টেস্ট চালাই, Listing 15-23-এর কোড কোনো এরর ছাড়াই কম্পাইল হবে, কিন্তু টেস্টটি ফেইল করবে:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
লক্ষ্য করুন যে কোডটি already borrowed: BorrowMutError
বার্তা দিয়ে প্যানিক করেছে। এভাবেই RefCell<T>
রানটাইমে borrowing নিয়মের লঙ্ঘন সামাল দেয়।
কম্পাইল টাইমের পরিবর্তে রানটাইমে borrowing এরর ধরার সিদ্ধান্ত নেওয়ার মানে হলো, যেমনটি আমরা এখানে করেছি, আপনি সম্ভবত ডেভেলপমেন্ট প্রক্রিয়ার পরে আপনার কোডের ভুল খুঁজে পাবেন: সম্ভবত আপনার কোড প্রোডাকশনে স্থাপন না হওয়া পর্যন্ত নয়। এছাড়াও, রানটাইমে borrow ট্র্যাক রাখার ফলে আপনার কোডে একটি ছোট রানটাইম পারফরম্যান্স পেনাল্টি হবে। তবে, RefCell<T>
ব্যবহার করে এমন একটি মক অবজেক্ট লেখা সম্ভব যা নিজেকে পরিবর্তন করে তার দেখা বার্তাগুলোর ট্র্যাক রাখতে পারে যখন আপনি এটি এমন একটি কনটেক্সটে ব্যবহার করছেন যেখানে শুধুমাত্র immutable ভ্যালু অনুমোদিত। আপনি নিয়মিত reference-এর চেয়ে বেশি কার্যকারিতা পেতে RefCell<T>
ব্যবহার করতে পারেন, এর ট্রেড-অফ থাকা সত্ত্বেও।
Rc<T>
এবং RefCell<T>
একত্রিত করে একাধিক মালিকানাধীন Mutable ডেটার অনুমতি দেওয়া
RefCell<T>
ব্যবহার করার একটি সাধারণ উপায় হলো Rc<T>
-এর সাথে সংমিশ্রণ। মনে করুন Rc<T>
আপনাকে কিছু ডেটার একাধিক owner रखने দেয়, কিন্তু এটি শুধুমাত্র সেই ডেটার immutable অ্যাক্সেস দেয়। যদি আপনার কাছে একটি Rc<T>
থাকে যা একটি RefCell<T>
ধারণ করে, আপনি এমন একটি ভ্যালু পেতে পারেন যার একাধিক owner থাকতে পারে এবং যা আপনি পরিবর্তন করতে পারেন!
উদাহরণস্বরূপ, Listing 15-18-এর cons list উদাহরণটি মনে করুন যেখানে আমরা Rc<T>
ব্যবহার করে একাধিক লিস্টকে অন্য একটি লিস্টের মালিকানা শেয়ার করার অনুমতি দিয়েছিলাম। যেহেতু Rc<T>
শুধুমাত্র immutable ভ্যালু ধারণ করে, তাই আমরা একবার লিস্ট তৈরি করার পরে লিস্টের কোনো ভ্যালু পরিবর্তন করতে পারি না। চলুন লিস্টের ভ্যালুগুলো পরিবর্তন করার ক্ষমতার জন্য RefCell<T>
যোগ করি। Listing 15-24 দেখাচ্ছে যে Cons
সংজ্ঞায় একটি RefCell<T>
ব্যবহার করে, আমরা সমস্ত লিস্টে সংরক্ষিত ভ্যালু পরিবর্তন করতে পারি।
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }
আমরা Rc<RefCell<i32>>
-এর একটি ইনস্ট্যান্স তৈরি করে value
নামের একটি ভেরিয়েবলে সংরক্ষণ করি যাতে আমরা পরে সরাসরি এটি অ্যাক্সেস করতে পারি। তারপর আমরা a
-তে একটি List
তৈরি করি যার Cons
ভ্যারিয়েন্ট value
ধারণ করে। আমাদের value
ক্লোন করতে হবে যাতে a
এবং value
উভয়েই ভেতরের 5
ভ্যালুর মালিকানা পায়, value
থেকে a
-তে মালিকানা হস্তান্তর না করে বা a
-কে value
থেকে borrow করতে না হয়।
আমরা a
লিস্টটিকে একটি Rc<T>
-তে মুড়িয়ে দিই যাতে যখন আমরা b
এবং c
লিস্ট তৈরি করি, তারা উভয়েই a
-কে নির্দেশ করতে পারে, যা আমরা Listing 15-18-এ করেছিলাম।
a
, b
, এবং c
-তে লিস্ট তৈরি করার পরে, আমরা value
-এর ভ্যালুতে 10 যোগ করতে চাই। আমরা value
-এর উপর borrow_mut
কল করে এটি করি, যা Chapter 5-এর ["Where’s the ->
Operator?"][wheres-the---operator]-এ আলোচনা করা স্বয়ংক্রিয় dereferencing ফিচার ব্যবহার করে Rc<T>
-কে ভেতরের RefCell<T>
ভ্যালুতে dereference করে। borrow_mut
মেথডটি একটি RefMut<T>
স্মার্ট পয়েন্টার রিটার্ন করে, এবং আমরা এর উপর dereference অপারেটর ব্যবহার করে ভেতরের ভ্যালু পরিবর্তন করি।
যখন আমরা a
, b
, এবং c
প্রিন্ট করি, আমরা দেখতে পাই যে তাদের সকলেরই 5
-এর পরিবর্তে পরিবর্তিত মান 15
রয়েছে:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
এই কৌশলটি বেশ চমৎকার! RefCell<T>
ব্যবহার করে, আমাদের কাছে একটি বাহ্যিকভাবে immutable List
ভ্যালু আছে। কিন্তু আমরা RefCell<T>
-এর মেথডগুলো ব্যবহার করতে পারি যা তার ইন্টেরিয়র মিউটেবিলিটিতে অ্যাক্সেস দেয় যাতে প্রয়োজনে আমরা আমাদের ডেটা পরিবর্তন করতে পারি। রানটাইমে borrowing নিয়মের চেক আমাদের ডেটা রেস থেকে রক্ষা করে, এবং আমাদের ডেটা স্ট্রাকচারে এই নমনীয়তার জন্য কখনও কখনও কিছুটা গতি বিসর্জন দেওয়া সার্থক। মনে রাখবেন RefCell<T>
মাল্টি-থ্রেডেড কোডের জন্য কাজ করে না! Mutex<T>
হলো RefCell<T>
-এর থ্রেড-সেফ সংস্করণ, এবং আমরা Chapter 16-এ Mutex<T>
নিয়ে আলোচনা করব।
রেফারেন্স সাইকেল মেমোরি লিক করতে পারে (Reference Cycles Can Leak Memory)
রাস্টের মেমোরি সেফটি গ্যারান্টি ভুলবশত এমন মেমোরি তৈরি করা কঠিন করে তোলে যা কখনও পরিষ্কার হয় না (যা মেমোরি লিক নামে পরিচিত), কিন্তু অসম্ভব নয়। সম্পূর্ণভাবে মেমোরি লিক প্রতিরোধ করা রাস্টের গ্যারান্টির মধ্যে পড়ে না, যার অর্থ হলো রাস্ট-এ মেমোরি লিক মেমোরি সেফ (memory safe)। আমরা দেখতে পারি যে রাস্ট Rc<T>
এবং RefCell<T>
ব্যবহার করে মেমোরি লিকের অনুমতি দেয়: এমন রেফারেন্স তৈরি করা সম্ভব যেখানে আইটেমগুলো একে অপরকে একটি সাইকেলে (cycle) নির্দেশ করে। এটি মেমোরি লিক তৈরি করে কারণ সাইকেলের প্রতিটি আইটেমের রেফারেন্স কাউন্ট কখনও ০-তে পৌঁছাবে না, এবং ভ্যালুগুলো কখনও ড্রপ হবে না।
একটি রেফারেন্স সাইকেল তৈরি করা
চলুন দেখি কীভাবে একটি রেফারেন্স সাইকেল ঘটতে পারে এবং কীভাবে এটি প্রতিরোধ করা যায়। এর জন্য, আমরা Listing 15-25-এ List
enum-এর সংজ্ঞা এবং একটি tail
মেথড দিয়ে শুরু করব।
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() {}
আমরা Listing 15-5 থেকে List
সংজ্ঞার আরেকটি ভিন্ন সংস্করণ ব্যবহার করছি। Cons
ভ্যারিয়েন্টের দ্বিতীয় উপাদানটি এখন RefCell<Rc<List>>
, যার মানে হলো, Listing 15-24-এর মতো i32
ভ্যালু পরিবর্তন করার ক্ষমতার পরিবর্তে, আমরা একটি Cons
ভ্যারিয়েন্ট যে List
ভ্যালুকে নির্দেশ করছে তা পরিবর্তন করতে চাই। আমরা একটি tail
মেথডও যোগ করছি যাতে আমাদের কাছে Cons
ভ্যারিয়েন্ট থাকলে দ্বিতীয় আইটেমটি অ্যাক্সেস করা সুবিধাজনক হয়।
Listing 15-26-এ, আমরা একটি main
ফাংশন যোগ করছি যা Listing 15-25-এর সংজ্ঞাগুলো ব্যবহার করে। এই কোডটি a
-তে একটি লিস্ট এবং b
-তে একটি লিস্ট তৈরি করে যা a
-এর লিস্টকে নির্দেশ করে। তারপর এটি a
-এর লিস্টকে b
-কে নির্দেশ করার জন্য পরিবর্তন করে, যার ফলে একটি রেফারেন্স সাইকেল তৈরি হয়। এই প্রক্রিয়ার বিভিন্ন পর্যায়ে রেফারেন্স কাউন্ট কত তা দেখানোর জন্য পথে println!
স্টেটমেন্ট রয়েছে।
use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!("a initial rc count = {}", Rc::strong_count(&a)); println!("a next item = {:?}", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!("a rc count after b creation = {}", Rc::strong_count(&a)); println!("b initial rc count = {}", Rc::strong_count(&b)); println!("b next item = {:?}", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); // Uncomment the next line to see that we have a cycle; // it will overflow the stack. // println!("a next item = {:?}", a.tail()); }
আমরা a
ভ্যারিয়েবলে একটি List
ভ্যালু ধারণকারী একটি Rc<List>
ইনস্ট্যান্স তৈরি করি যার প্রাথমিক লিস্ট হলো 5, Nil
। তারপর আমরা b
ভ্যারিয়েবলে আরেকটি List
ভ্যালু ধারণকারী একটি Rc<List>
ইনস্ট্যান্স তৈরি করি যা 10
ভ্যালুটি ধারণ করে এবং a
-এর লিস্টকে নির্দেশ করে।
আমরা a
-কে পরিবর্তন করি যাতে এটি Nil
-এর পরিবর্তে b
-কে নির্দেশ করে, যার ফলে একটি সাইকেল তৈরি হয়। আমরা এটি tail
মেথড ব্যবহার করে a
-এর RefCell<Rc<List>>
-এর একটি রেফারেন্স পেয়ে করি, যা আমরা link
ভ্যারিয়েবলে রাখি। তারপর আমরা RefCell<Rc<List>>
-এর উপর borrow_mut
মেথড ব্যবহার করে ভেতরের ভ্যালুটি Nil
ভ্যালু ধারণকারী একটি Rc<List>
থেকে b
-এর Rc<List>
-এ পরিবর্তন করি।
যখন আমরা এই কোডটি রান করি, শেষ println!
-টি আপাতত কমেন্ট আউট রেখে, আমরা এই আউটপুটটি পাব:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
a
এবং b
উভয়ের Rc<List>
ইনস্ট্যান্সের রেফারেন্স কাউন্ট ২ হয়ে যায় যখন আমরা a
-এর লিস্টকে b
-কে নির্দেশ করার জন্য পরিবর্তন করি। main
-এর শেষে, রাস্ট b
ভ্যারিয়েবলটি ড্রপ করে, যা b
Rc<List>
ইনস্ট্যান্সের রেফারেন্স কাউন্ট ২ থেকে ১-এ কমিয়ে দেয়। এই মুহূর্তে Rc<List>
-এর হিপে থাকা মেমোরি ড্রপ হবে না কারণ এর রেফারেন্স কাউন্ট ১, ০ নয়। তারপর রাস্ট a
-কে ড্রপ করে, যা a
Rc<List>
ইনস্ট্যান্সের রেফারেন্স কাউন্টও ২ থেকে ১-এ কমিয়ে দেয়। এই ইনস্ট্যান্সের মেমোরিও ড্রপ করা যাবে না, কারণ অন্য Rc<List>
ইনস্ট্যান্সটি এখনও এটিকে নির্দেশ করছে। লিস্টের জন্য বরাদ্দ করা মেমোরি চিরকালের জন্য সংগ্রহ করা হবে না। এই রেফারেন্স সাইকেলটি কল্পনা করার জন্য, আমরা Figure 15-4-এ একটি ডায়াগ্রাম তৈরি করেছি।
Figure 15-4: লিস্ট a
এবং b
-এর একটি রেফারেন্স সাইকেল যা একে অপরকে নির্দেশ করছে
আপনি যদি শেষ println!
-টি আনকমেন্ট করে প্রোগ্রামটি রান করেন, রাস্ট এই সাইকেলটি প্রিন্ট করার চেষ্টা করবে যেখানে a
b
-কে নির্দেশ করে, b
a
-কে নির্দেশ করে এবং এভাবে চলতে থাকবে যতক্ষণ না এটি স্ট্যাক ওভারফ্লো (stack overflow) করে।
বাস্তব জগতের একটি প্রোগ্রামের তুলনায়, এই উদাহরণে একটি রেফারেন্স সাইকেল তৈরি করার পরিণতি খুব ভয়াবহ নয়: আমরা রেফারেন্স সাইকেল তৈরি করার পরেই প্রোগ্রামটি শেষ হয়ে যায়। তবে, যদি একটি আরও জটিল প্রোগ্রাম একটি সাইকেলে প্রচুর মেমোরি বরাদ্দ করে এবং এটি দীর্ঘ সময়ের জন্য ধরে রাখে, প্রোগ্রামটি প্রয়োজনের চেয়ে বেশি মেমোরি ব্যবহার করবে এবং সিস্টেমকে অভিভূত করতে পারে, যার ফলে উপলব্ধ মেমোরি শেষ হয়ে যেতে পারে।
রেফারেন্স সাইকেল তৈরি করা সহজ নয়, কিন্তু এটি অসম্ভবও নয়। যদি আপনার কাছে Rc<T>
ভ্যালু ধারণকারী RefCell<T>
ভ্যালু বা ইন্টেরিয়র মিউটেবিলিটি এবং রেফারেন্স কাউন্টিং সহ টাইপের অনুরূপ নেস্টেড সংমিশ্রণ থাকে, আপনাকে নিশ্চিত করতে হবে যে আপনি সাইকেল তৈরি করছেন না; আপনি রাস্টের উপর নির্ভর করতে পারবেন না যে এটি সেগুলো ধরবে। একটি রেফারেন্স সাইকেল তৈরি করা আপনার প্রোগ্রামে একটি লজিক বাগ হবে যা আপনার উচিত অটোমেটেড টেস্ট, কোড রিভিউ এবং অন্যান্য সফটওয়্যার ডেভেলপমেন্ট অনুশীলন ব্যবহার করে কমিয়ে আনা।
রেফারেন্স সাইকেল এড়ানোর আরেকটি সমাধান হলো আপনার ডেটা স্ট্রাকচারগুলো এমনভাবে পুনর্গঠিত করা যাতে কিছু রেফারেন্স মালিকানা প্রকাশ করে এবং কিছু রেফারেন্স করে না। ফলস্বরূপ, আপনি কিছু মালিকানা সম্পর্ক এবং কিছু অ-মালিকানা সম্পর্ক দিয়ে গঠিত সাইকেল রাখতে পারেন, এবং শুধুমাত্র মালিকানা সম্পর্কগুলোই একটি ভ্যালু ড্রপ করা যাবে কিনা তা প্রভাবিত করে। Listing 15-25-এ, আমরা সবসময় চাই Cons
ভ্যারিয়েন্টগুলো তাদের লিস্টের মালিক হোক, তাই ডেটা স্ট্রাকচার পুনর্গঠন করা সম্ভব নয়। চলুন প্যারেন্ট নোড এবং চাইল্ড নোড দিয়ে গঠিত গ্রাফ ব্যবহার করে একটি উদাহরণ দেখি কখন অ-মালিকানা সম্পর্ক রেফারেন্স সাইকেল প্রতিরোধের একটি উপযুক্ত উপায়।
Weak<T>
ব্যবহার করে রেফারেন্স সাইকেল প্রতিরোধ করা
এখন পর্যন্ত, আমরা দেখিয়েছি যে Rc::clone
কল করা একটি Rc<T>
ইনস্ট্যান্সের strong_count
বাড়ায়, এবং একটি Rc<T>
ইনস্ট্যান্স শুধুমাত্র তখনই পরিষ্কার করা হয় যদি এর strong_count
০ হয়। আপনি একটি Rc<T>
ইনস্ট্যান্সের ভেতরের ভ্যালুর একটি দুর্বল রেফারেন্সও (weak reference) তৈরি করতে পারেন Rc::downgrade
কল করে এবং Rc<T>
-এর একটি রেফারেন্স পাস করে। Strong references হলো যেভাবে আপনি একটি Rc<T>
ইনস্ট্যান্সের মালিকানা শেয়ার করতে পারেন। Weak references কোনো মালিকানা সম্পর্ক প্রকাশ করে না, এবং তাদের কাউন্ট একটি Rc<T>
ইনস্ট্যান্স কখন পরিষ্কার করা হবে তা প্রভাবিত করে না। তারা রেফারেন্স সাইকেল তৈরি করবে না কারণ কিছু দুর্বল রেফারেন্স জড়িত কোনো সাইকেল ভেঙে যাবে যখন জড়িত ভ্যালুগুলোর strong reference count ০ হবে।
যখন আপনি Rc::downgrade
কল করেন, আপনি Weak<T>
টাইপের একটি স্মার্ট পয়েন্টার পান। Rc<T>
ইনস্ট্যান্সের strong_count
১ বাড়ানোর পরিবর্তে, Rc::downgrade
কল করা weak_count
১ বাড়ায়। Rc<T>
টাইপ weak_count
ব্যবহার করে ট্র্যাক রাখে কতগুলো Weak<T>
রেফারেন্স বিদ্যমান, strong_count
-এর মতো। পার্থক্য হলো Rc<T>
ইনস্ট্যান্সটি পরিষ্কার করার জন্য weak_count
-এর ০ হওয়ার প্রয়োজন নেই।
যেহেতু Weak<T>
যে ভ্যালুটিকে রেফারেন্স করে তা ড্রপ হয়ে যেতে পারে, তাই Weak<T>
যে ভ্যালুটিকে নির্দেশ করছে তার সাথে কিছু করার জন্য আপনাকে নিশ্চিত করতে হবে যে ভ্যালুটি এখনও বিদ্যমান আছে। এটি Weak<T>
ইনস্ট্যান্সের উপর upgrade
মেথড কল করে করুন, যা একটি Option<Rc<T>>
রিটার্ন করবে। আপনি Some
ফলাফল পাবেন যদি Rc<T>
ভ্যালুটি এখনও ড্রপ না হয়ে থাকে এবং None
ফলাফল পাবেন যদি Rc<T>
ভ্যালুটি ড্রপ হয়ে গিয়ে থাকে। যেহেতু upgrade
একটি Option<Rc<T>>
রিটার্ন করে, রাস্ট নিশ্চিত করবে যে Some
কেস এবং None
কেস উভয়ই হ্যান্ডেল করা হয়েছে, এবং কোনো অবৈধ পয়েন্টার থাকবে না।
একটি উদাহরণ হিসাবে, এমন একটি লিস্ট ব্যবহার করার পরিবর্তে যার আইটেমগুলো কেবল পরবর্তী আইটেম সম্পর্কে জানে, আমরা একটি ট্রি (tree) তৈরি করব যার আইটেমগুলো তাদের চাইল্ড আইটেম এবং তাদের প্যারেন্ট আইটেম সম্পর্কে জানে।
একটি ট্রি ডেটা স্ট্রাকচার তৈরি করা: চাইল্ড নোড সহ একটি Node
শুরুতে, আমরা এমন একটি ট্রি তৈরি করব যার নোডগুলো তাদের চাইল্ড নোড সম্পর্কে জানে। আমরা Node
নামে একটি স্ট্রাকট তৈরি করব যা তার নিজস্ব i32
ভ্যালু এবং তার চাইল্ড Node
ভ্যালুগুলোর রেফারেন্স ধারণ করে:
Filename: src/main.rs
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }
আমরা চাই একটি Node
তার চাইল্ডদের মালিক হোক, এবং আমরা সেই মালিকানা ভ্যারিয়েবলগুলোর সাথে শেয়ার করতে চাই যাতে আমরা ট্রির প্রতিটি Node
সরাসরি অ্যাক্সেস করতে পারি। এটি করার জন্য, আমরা Vec<T>
আইটেমগুলোকে Rc<Node>
টাইপের ভ্যালু হিসাবে সংজ্ঞায়িত করি। আমরা আরও পরিবর্তন করতে চাই কোন নোডগুলো অন্য নোডের চাইল্ড, তাই আমাদের children
-এ Vec<Rc<Node>>
-এর চারপাশে একটি RefCell<T>
আছে।
এরপরে, আমরা আমাদের স্ট্রাকট সংজ্ঞা ব্যবহার করব এবং 3
মান সহ এবং কোনো চাইল্ড ছাড়া leaf
নামে একটি Node
ইনস্ট্যান্স এবং 5
মান সহ এবং leaf
-কে তার একটি চাইল্ড হিসাবে branch
নামে আরেকটি ইনস্ট্যান্স তৈরি করব, যেমনটি Listing 15-27-এ দেখানো হয়েছে।
use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); }
আমরা leaf
-এর Rc<Node>
-কে ক্লোন করি এবং তা branch
-এ সংরক্ষণ করি, যার মানে হলো leaf
-এর Node
-এর এখন দুটি মালিক: leaf
এবং branch
। আমরা branch.children
-এর মাধ্যমে branch
থেকে leaf
-এ যেতে পারি, কিন্তু leaf
থেকে branch
-এ যাওয়ার কোনো উপায় নেই। কারণ হলো leaf
-এর branch
-এর কোনো রেফারেন্স নেই এবং তারা যে সম্পর্কিত তা জানে না। আমরা চাই leaf
জানুক যে branch
তার প্যারেন্ট। আমরাต่อไป এটি করব।
একটি চাইল্ড থেকে তার প্যারেন্টের একটি রেফারেন্স যোগ করা
চাইল্ড নোডকে তার প্যারেন্ট সম্পর্কে সচেতন করতে, আমাদের Node
স্ট্রাকট সংজ্ঞায় একটি parent
ফিল্ড যোগ করতে হবে। সমস্যা হলো parent
-এর টাইপ কী হবে তা নির্ধারণ করা। আমরা জানি এটি একটি Rc<T>
ধারণ করতে পারে না, কারণ এটি leaf.parent
-কে branch
-কে নির্দেশ করে এবং branch.children
-কে leaf
-কে নির্দেশ করে একটি রেফারেন্স সাইকেল তৈরি করবে, যা তাদের strong_count
মানগুলোকে কখনও ০ হতে দেবে না।
সম্পর্কগুলো অন্যভাবে চিন্তা করলে, একটি প্যারেন্ট নোডের উচিত তার চাইল্ডদের মালিক হওয়া: যদি একটি প্যারেন্ট নোড ড্রপ করা হয়, তার চাইল্ড নোডগুলোও ড্রপ করা উচিত। তবে, একটি চাইল্ডের উচিত নয় তার প্যারেন্টের মালিক হওয়া: যদি আমরা একটি চাইল্ড নোড ড্রপ করি, প্যারেন্টটি তখনও বিদ্যমান থাকা উচিত। এটি weak references-এর জন্য একটি ক্ষেত্র!
সুতরাং Rc<T>
-এর পরিবর্তে, আমরা parent
-এর টাইপ Weak<T>
ব্যবহার করব, নির্দিষ্টভাবে একটি RefCell<Weak<Node>>
। এখন আমাদের Node
স্ট্রাকট সংজ্ঞাটি এমন দেখাচ্ছে:
Filename: src/main.rs
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }
একটি নোড তার প্যারেন্ট নোডকে নির্দেশ করতে সক্ষম হবে কিন্তু তার প্যারেন্টের মালিক হবে না। Listing 15-28-এ, আমরা main
-কে এই নতুন সংজ্ঞা ব্যবহার করার জন্য আপডেট করি যাতে leaf
নোডের তার প্যারেন্ট, branch
-কে নির্দেশ করার একটি উপায় থাকে।
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); }
leaf
নোড তৈরি করা Listing 15-27-এর মতোই দেখাচ্ছে, parent
ফিল্ডটি ছাড়া: leaf
শুরুতে কোনো প্যারেন্ট ছাড়া থাকে, তাই আমরা একটি নতুন, খালি Weak<Node>
রেফারেন্স ইনস্ট্যান্স তৈরি করি।
এই মুহূর্তে, যখন আমরা upgrade
মেথড ব্যবহার করে leaf
-এর প্যারেন্টের একটি রেফারেন্স পাওয়ার চেষ্টা করি, আমরা একটি None
ভ্যালু পাই। আমরা এটি প্রথম println!
স্টেটমেন্টের আউটপুটে দেখতে পাই:
leaf parent = None
যখন আমরা branch
নোড তৈরি করি, তখন এটির parent
ফিল্ডে একটি নতুন Weak<Node>
রেফারেন্সও থাকবে কারণ branch
-এর কোনো প্যারেন্ট নোড নেই। আমাদের এখনও leaf
branch
-এর একটি চাইল্ড হিসাবে আছে। একবার আমাদের branch
-এ Node
ইনস্ট্যান্সটি থাকলে, আমরা leaf
-কে পরিবর্তন করে তার প্যারেন্টের একটি Weak<Node>
রেফারেন্স দিতে পারি। আমরা leaf
-এর parent
ফিল্ডের RefCell<Weak<Node>>
-এর উপর borrow_mut
মেথড ব্যবহার করি, এবং তারপর আমরা branch
-এর Rc<Node>
থেকে branch
-এর একটি Weak<Node>
রেফারেন্স তৈরি করতে Rc::downgrade
ফাংশন ব্যবহার করি।
যখন আমরা leaf
-এর প্যারেন্ট আবার প্রিন্ট করি, এবার আমরা branch
ধারণকারী একটি Some
ভ্যারিয়েন্ট পাব: এখন leaf
তার প্যারেন্ট অ্যাক্সেস করতে পারে! যখন আমরা leaf
প্রিন্ট করি, আমরা সেই সাইকেলটিও এড়িয়ে যাই যা অবশেষে Listing 15-26-এর মতো একটি স্ট্যাক ওভারফ্লোতে শেষ হয়েছিল; Weak<Node>
রেফারেন্সগুলো (Weak)
হিসাবে প্রিন্ট করা হয়:
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
অসীম আউটপুটের অভাব নির্দেশ করে যে এই কোডটি একটি রেফারেন্স সাইকেল তৈরি করেনি। আমরা Rc::strong_count
এবং Rc::weak_count
কল করে পাওয়া মানগুলো দেখেও এটি বলতে পারি।
strong_count
এবং weak_count
-এর পরিবর্তনগুলো কল্পনা করা
চলুন দেখি Rc<Node>
ইনস্ট্যান্সগুলোর strong_count
এবং weak_count
মানগুলো কীভাবে পরিবর্তিত হয় একটি নতুন অভ্যন্তরীণ স্কোপ তৈরি করে এবং branch
-এর তৈরিকে সেই স্কোপে সরিয়ে নিয়ে। এটি করার মাধ্যমে, আমরা দেখতে পারি branch
তৈরি হলে এবং তারপর স্কোপের বাইরে চলে গেলে কী হয়। পরিবর্তনগুলো Listing 15-29-এ দেখানো হয়েছে।
use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); { let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( "branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch), ); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); }
leaf
তৈরি করার পরে, তার Rc<Node>
-এর একটি strong count of 1 এবং একটি weak count of 0 থাকে। অভ্যন্তরীণ স্কোপে, আমরা branch
তৈরি করি এবং এটিকে leaf
-এর সাথে যুক্ত করি, সেই সময়ে যখন আমরা কাউন্টগুলো প্রিন্ট করি, branch
-এর Rc<Node>
-এর একটি strong count of 1 এবং একটি weak count of 1 থাকবে (leaf.parent
branch
-কে একটি Weak<Node>
দিয়ে নির্দেশ করার জন্য)। যখন আমরা leaf
-এর কাউন্টগুলো প্রিন্ট করি, আমরা দেখব এটির একটি strong count of 2 থাকবে কারণ branch
-এর এখন branch.children
-এ সংরক্ষিত leaf
-এর Rc<Node>
-এর একটি ক্লোন আছে, কিন্তু weak count of 0 থাকবে।
যখন অভ্যন্তরীণ স্কোপ শেষ হয়, branch
স্কোপের বাইরে চলে যায় এবং Rc<Node>
-এর strong count ০-তে কমে যায়, তাই তার Node
ড্রপ হয়ে যায়। leaf.parent
থেকে weak count of 1 Node
ড্রপ হবে কিনা তার উপর কোনো প্রভাব ফেলে না, তাই আমরা কোনো মেমোরি লিক পাই না!
যদি আমরা স্কোপের শেষের পরে leaf
-এর প্যারেন্ট অ্যাক্সেস করার চেষ্টা করি, আমরা আবার None
পাব। প্রোগ্রামের শেষে, leaf
-এর Rc<Node>
-এর একটি strong count of 1 এবং একটি weak count of 0 থাকে কারণ leaf
ভ্যারিয়েবলটি এখন Rc<Node>
-এর একমাত্র রেফারেন্স।
কাউন্ট এবং ভ্যালু ড্রপিং পরিচালনা করার সমস্ত লজিক Rc<T>
এবং Weak<T>
এবং তাদের Drop
ট্রেইটের ইমপ্লিমেন্টেশনে নির্মিত। Node
-এর সংজ্ঞায় একটি চাইল্ড থেকে তার প্যারেন্টের সম্পর্ক একটি Weak<T>
রেফারেন্স হওয়া উচিত তা নির্দিষ্ট করার মাধ্যমে, আপনি রেফারেন্স সাইকেল এবং মেমোরি লিক তৈরি না করে প্যারেন্ট নোডগুলোকে চাইল্ড নোডগুলোকে নির্দেশ করতে এবং এর বিপরীতটি করতে সক্ষম হন।
সারসংক্ষেপ (Summary)
এই অধ্যায়ে আলোচনা করা হয়েছে কীভাবে স্মার্ট পয়েন্টার ব্যবহার করে রাস্টের ডিফল্ট রেফারেন্সের থেকে ভিন্ন গ্যারান্টি এবং ট্রেড-অফ তৈরি করা যায়। Box<T>
টাইপের একটি নির্দিষ্ট সাইজ আছে এবং এটি হিপ-এ বরাদ্দ করা ডেটাকে নির্দেশ করে। Rc<T>
টাইপ হিপের ডেটার রেফারেন্স সংখ্যা ট্র্যাক করে যাতে ডেটার একাধিক মালিক থাকতে পারে। RefCell<T>
টাইপ তার ইন্টেরিয়র মিউটেবিলিটি সহ আমাদের এমন একটি টাইপ দেয় যা আমরা ব্যবহার করতে পারি যখন আমাদের একটি immutable টাইপ প্রয়োজন কিন্তু সেই টাইপের একটি ভেতরের ভ্যালু পরিবর্তন করতে হবে; এটি কম্পাইল টাইমের পরিবর্তে রানটাইমে borrowing-এর নিয়ম প্রয়োগ করে।
এছাড়াও Deref
এবং Drop
ট্রেইট নিয়ে আলোচনা করা হয়েছে, যা স্মার্ট পয়েন্টারগুলোর অনেক কার্যকারিতা সক্ষম করে। আমরা রেফারেন্স সাইকেল যা মেমোরি লিক ঘটাতে পারে এবং Weak<T>
ব্যবহার করে কীভাবে তা প্রতিরোধ করা যায় তা অন্বেষণ করেছি।
যদি এই অধ্যায়টি আপনার আগ্রহ জাগিয়ে তোলে এবং আপনি আপনার নিজস্ব স্মার্ট পয়েন্টার ইমপ্লিমেন্ট করতে চান, আরও দরকারী তথ্যের জন্য ["The Rustonomicon"][nomicon] দেখুন।
এরপরে, আমরা রাস্ট-এ কনকারেন্সি (concurrency) নিয়ে কথা বলব। আপনি এমনকি কয়েকটি নতুন স্মার্ট পয়েন্টার সম্পর্কেও শিখবেন।
নির্ভীক কনকারেন্সি
কনকারেন্ট প্রোগ্রামিং নিরাপদে এবং দক্ষতার সাথে পরিচালনা করা Rust-এর অন্যতম প্রধান লক্ষ্য। কনকারেন্ট প্রোগ্রামিং (Concurrent programming), যেখানে একটি প্রোগ্রামের বিভিন্ন অংশ স্বাধীনভাবে এক্সিকিউট হয়, এবং প্যারালাল প্রোগ্রামিং (parallel programming), যেখানে একটি প্রোগ্রামের বিভিন্ন অংশ একই সময়ে এক্সিকিউট হয়, এই দুটি বিষয় ক্রমশ গুরুত্বপূর্ণ হয়ে উঠছে কারণ আরও বেশি কম্পিউটার তাদের একাধিক প্রসেসরের সুবিধা নিচ্ছে। ঐতিহাসিকভাবে, এই প্রেক্ষাপটে প্রোগ্রামিং করা কঠিন এবং ভুল-প্রবণ ছিল। Rust সেই ধারণা পরিবর্তন করার আশা রাখে।
প্রথমদিকে, Rust টিম ভেবেছিল যে মেমরি সেফটি নিশ্চিত করা এবং কনকারেন্সি সমস্যা প্রতিরোধ করা দুটি ভিন্ন চ্যালেঞ্জ যা ভিন্ন পদ্ধতিতে সমাধান করতে হবে। সময়ের সাথে সাথে, টিম আবিষ্কার করে যে ওনারশিপ এবং টাইপ সিস্টেম মেমরি সেফটি এবং কনকারেন্সি সমস্যা উভয়ই পরিচালনা করতে সাহায্য করার জন্য একটি শক্তিশালী টুল সেট! ওনারশিপ এবং টাইপ চেকিং ব্যবহার করে, অনেক কনকারেন্সি এরর Rust-এ রানটাইম এররের পরিবর্তে কম্পাইল-টাইম এরর হিসেবে ধরা পড়ে। সুতরাং, একটি রানটাইম কনকারেন্সি বাগ ঠিক কোন পরিস্থিতিতে ঘটছে তা পুনরুৎপাদন করার জন্য অনেক সময় ব্যয় করার পরিবর্তে, ভুল কোড কম্পাইল হতেই拒绝 করবে এবং সমস্যা ব্যাখ্যা করে একটি এরর দেখাবে। ফলস্বরূপ, আপনি আপনার কোড প্রোডাকশনে পাঠানোর পরে ঠিক করার পরিবর্তে, কাজ করার সময়ই এটি ঠিক করতে পারেন। আমরা Rust-এর এই দিকটির ডাকনাম দিয়েছি নির্ভীক কনকারেন্সি (fearless concurrency)। নির্ভীক কনকারেন্সি আপনাকে এমন কোড লিখতে দেয় যা সূক্ষ্ম বাগমুক্ত এবং নতুন বাগ তৈরি না করেই রিফ্যাক্টর করা সহজ।
নোট: সরলতার জন্য, আমরা অনেক সমস্যাকে আরও সুনির্দিষ্টভাবে কনকারেন্ট এবং/অথবা প্যারালাল না বলে শুধু কনকারেন্ট হিসাবে উল্লেখ করব। এই অধ্যায়ের জন্য, আমরা যখনই কনকারেন্ট শব্দটি ব্যবহার করব, অনুগ্রহ করে মানসিকভাবে এটিকে কনকারেন্ট এবং/অথবা প্যারালাল দিয়ে প্রতিস্থাপন করবেন। পরবর্তী অধ্যায়ে, যেখানে এই পার্থক্যটি আরও গুরুত্বপূর্ণ, আমরা আরও সুনির্দিষ্ট হব।
অনেক ল্যাঙ্গুয়েজ কনকারেন্ট সমস্যা সমাধানের জন্য তাদের দেওয়া সমাধান সম্পর্কে একরোখা। উদাহরণস্বরূপ, Erlang-এর মেসেজ-পাসিং কনকারেন্সির জন্য চমৎকার কার্যকারিতা রয়েছে তবে থ্রেডগুলোর মধ্যে স্টেট শেয়ার করার জন্য শুধুমাত্র অস্পষ্ট উপায় রয়েছে। সম্ভাব্য সমাধানের শুধুমাত্র একটি উপসেট সমর্থন করা হায়ার-লেভেল ল্যাঙ্গুয়েজগুলোর জন্য একটি যুক্তিসঙ্গত কৌশল কারণ একটি হায়ার-লেভেল ল্যাঙ্গুয়েজ অ্যাবস্ট্রাকশন পাওয়ার জন্য কিছু নিয়ন্ত্রণ ছেড়ে দেওয়ার মাধ্যমে সুবিধা দেওয়ার প্রতিশ্রুতি দেয়। যাইহোক, লোয়ার-লেভেল ল্যাঙ্গুয়েজগুলো থেকে আশা করা হয় যে কোনো পরিস্থিতিতে সেরা পারফরম্যান্সের সমাধান দেবে এবং হার্ডওয়্যারের উপর কম অ্যাবস্ট্রাকশন থাকবে। তাই, Rust আপনার পরিস্থিতি এবং প্রয়োজনীয়তার জন্য উপযুক্ত যেকোনো উপায়ে সমস্যা মডেল করার জন্য বিভিন্ন টুল সরবরাহ করে।
এই অধ্যায়ে আমরা যে বিষয়গুলো আলোচনা করব তা এখানে দেওয়া হলো:
- একই সময়ে কোডের একাধিক অংশ চালানোর জন্য কীভাবে থ্রেড তৈরি করতে হয়
- মেসেজ-পাসিং কনকারেন্সি, যেখানে চ্যানেলগুলো থ্রেডের মধ্যে মেসেজ পাঠায়
- শেয়ারড-স্টেট কনকারেন্সি, যেখানে একাধিক থ্রেডের কিছু ডেটাতে অ্যাক্সেস থাকে
Sync
এবংSend
ট্রেইট, যা Rust-এর কনকারেন্সি গ্যারান্টিকে ইউজার-ডিফাইন্ড টাইপের পাশাপাশি স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত টাইপের ক্ষেত্রেও প্রসারিত করে
একই সাথে কোড চালানোর জন্য থ্রেড ব্যবহার করা
বেশিরভাগ আধুনিক অপারেটিং সিস্টেমে, একটি প্রোগ্রামের কোড একটি প্রসেস (process) এর মধ্যে চলে এবং অপারেটিং সিস্টেম একসাথে একাধিক প্রসেস পরিচালনা করে। একটি প্রোগ্রামের মধ্যেও, আপনার স্বাধীন অংশ থাকতে পারে যা একই সাথে চলে। এই স্বাধীন অংশগুলো চালানোর ফিচারগুলোকে থ্রেড (threads) বলা হয়। উদাহরণস্বরূপ, একটি ওয়েব সার্ভারে একাধিক থ্রেড থাকতে পারে যাতে এটি একই সময়ে একাধিক অনুরোধের উত্তর দিতে পারে।
আপনার প্রোগ্রামের কম্পিউটেশনকে একাধিক থ্রেডে বিভক্ত করে একই সময়ে একাধিক কাজ চালানো পারফরম্যান্স উন্নত করতে পারে, তবে এটি জটিলতাও বাড়ায়। কারণ থ্রেডগুলো একই সাথে চলতে পারে, আপনার কোডের বিভিন্ন অংশের কোন অংশ কোন ক্রমে চলবে তার কোনো অন্তর্নিহিত গ্যারান্টি নেই। এটি বিভিন্ন সমস্যার কারণ হতে পারে, যেমন:
- রেস কন্ডিশন (Race conditions), যেখানে থ্রেডগুলো অসংগত ক্রমে ডেটা বা রিসোর্স অ্যাক্সেস করে।
- ডেডলক (Deadlocks), যেখানে দুটি থ্রেড একে অপরের জন্য অপেক্ষা করে, উভয় থ্রেডকে চলতে বাধা দেয়।
- এমন বাগ যা শুধুমাত্র নির্দিষ্ট পরিস্থিতিতে ঘটে এবং নির্ভরযোগ্যভাবে পুনরুৎপাদন এবং ঠিক করা কঠিন।
Rust থ্রেড ব্যবহারের নেতিবাচক প্রভাবগুলো কমানোর চেষ্টা করে, কিন্তু একটি মাল্টিথ্রেডেড প্রেক্ষাপটে প্রোগ্রামিং করার জন্য এখনও সতর্ক চিন্তাভাবনা এবং একটি কোড কাঠামোর প্রয়োজন যা একটি একক থ্রেডে চলা প্রোগ্রামগুলোর থেকে ভিন্ন।
প্রোগ্রামিং ল্যাঙ্গুয়েজগুলো বিভিন্ন উপায়ে থ্রেড ইমপ্লিমেন্ট করে, এবং অনেক অপারেটিং সিস্টেম একটি API সরবরাহ করে যা প্রোগ্রামিং ল্যাঙ্গুয়েজ নতুন থ্রেড তৈরি করার জন্য কল করতে পারে। Rust স্ট্যান্ডার্ড লাইব্রেরি থ্রেড ইমপ্লিমেন্টেশনের জন্য একটি 1:1 মডেল ব্যবহার করে, যেখানে একটি প্রোগ্রাম প্রতিটি ল্যাঙ্গুয়েজ থ্রেডের জন্য একটি অপারেটিং সিস্টেম থ্রেড ব্যবহার করে। এমন কিছু ক্রেট আছে যা থ্রেডিংয়ের অন্যান্য মডেল ইমপ্লিমেন্ট করে যা 1:1 মডেলের থেকে ভিন্ন ট্রেড-অফ করে। (Rust-এর অ্যাসিঙ্ক সিস্টেম, যা আমরা পরবর্তী অধ্যায়ে দেখব, কনকারেন্সির জন্য আরেকটি পদ্ধতি প্রদান করে।)
spawn
দিয়ে একটি নতুন থ্রেড তৈরি করা
একটি নতুন থ্রেড তৈরি করতে, আমরা thread::spawn
ফাংশনটি কল করি এবং এটিকে একটি ক্লোজার (closure) পাস করি (আমরা অধ্যায় ১৩-এ ক্লোজার নিয়ে আলোচনা করেছি) যেখানে আমরা নতুন থ্রেডে যে কোডটি চালাতে চাই তা থাকে। তালিকা ১৬-১ এর উদাহরণটি একটি প্রধান থ্রেড থেকে কিছু টেক্সট এবং একটি নতুন থ্রেড থেকে অন্য টেক্সট প্রিন্ট করে।
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } }
লক্ষ্য করুন যে যখন একটি Rust প্রোগ্রামের প্রধান থ্রেড শেষ হয়ে যায়, তখন সমস্ত স্পন (spawned) করা থ্রেড বন্ধ হয়ে যায়, তারা তাদের কাজ শেষ করুক বা না করুক। এই প্রোগ্রামের আউটপুট প্রতিবার কিছুটা ভিন্ন হতে পারে, তবে এটি নিম্নলিখিতর মতো দেখাবে:
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
thread::sleep
এর কলগুলো একটি থ্রেডকে তার এক্সিকিউশন অল্প সময়ের জন্য থামাতে বাধ্য করে, যা অন্য একটি থ্রেডকে চলার সুযোগ দেয়। থ্রেডগুলো সম্ভবত পর্যায়ক্রমে চলবে, তবে এর কোনো নিশ্চয়তা নেই: এটি আপনার অপারেটিং সিস্টেম কীভাবে থ্রেডগুলোকে সময় নির্ধারণ করে তার উপর নির্ভর করে। এই রানে, প্রধান থ্রেডটি প্রথমে প্রিন্ট করেছে, যদিও স্পন করা থ্রেডের প্রিন্ট স্টেটমেন্টটি কোডে প্রথমে দেখা যায়। এবং যদিও আমরা স্পন করা থ্রেডটিকে i
9
না হওয়া পর্যন্ত প্রিন্ট করতে বলেছিলাম, প্রধান থ্রেড বন্ধ হয়ে যাওয়ার আগে এটি কেবল 5
পর্যন্ত পৌঁছেছে।
আপনি যদি এই কোডটি চালান এবং শুধুমাত্র প্রধান থ্রেডের আউটপুট দেখতে পান, বা কোনো ওভারল্যাপ না দেখেন, তাহলে থ্রেডগুলোর মধ্যে অপারেটিং সিস্টেমকে স্যুইচ করার জন্য আরও সুযোগ তৈরি করতে রেঞ্জের সংখ্যাগুলো বাড়ানোর চেষ্টা করুন।
join
হ্যান্ডেল ব্যবহার করে সমস্ত থ্রেডের শেষ হওয়ার জন্য অপেক্ষা করা
তালিকা ১৬-১ এর কোডটি কেবল প্রধান থ্রেড শেষ হওয়ার কারণে বেশিরভাগ সময় স্পন করা থ্রেডটিকে অকালে থামিয়ে দেয় না, বরং থ্রেডগুলো কোন ক্রমে চলবে তার কোনো গ্যারান্টি না থাকায়, আমরা এটাও গ্যারান্টি দিতে পারি না যে স্পন করা থ্রেডটি überhaupt চলার সুযোগ পাবে!
আমরা thread::spawn
এর রিটার্ন ভ্যালু একটি ভ্যারিয়েবলে সংরক্ষণ করে স্পন করা থ্রেডটি না চলার বা অকালে শেষ হয়ে যাওয়ার সমস্যাটি সমাধান করতে পারি। thread::spawn
এর রিটার্ন টাইপ হলো JoinHandle<T>
। একটি JoinHandle<T>
হলো একটি ওনড ভ্যালু যা, যখন আমরা এর উপর join
মেথড কল করি, তখন তার থ্রেড শেষ হওয়ার জন্য অপেক্ষা করবে। তালিকা ১৬-২ দেখায় কীভাবে আমরা তালিকা ১৬-১ এ তৈরি করা থ্রেডের JoinHandle<T>
ব্যবহার করতে পারি এবং main
এক্সিট করার আগে স্পন করা থ্রেডটি শেষ হয়েছে তা নিশ্চিত করতে join
কল করতে পারি।
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
হ্যান্ডেলের উপর join
কল করা বর্তমানে চলমান থ্রেডটিকে ব্লক করে যতক্ষণ না হ্যান্ডেল দ্বারা প্রতিনিধিত্ব করা থ্রেডটি শেষ হয়। একটি থ্রেডকে ব্লক করার অর্থ হলো সেই থ্রেডটিকে কাজ করা বা এক্সিট করা থেকে বিরত রাখা। যেহেতু আমরা join
এর কলটি প্রধান থ্রেডের for
লুপের পরে রেখেছি, তালিকা ১৬-২ চালালে এই ধরনের আউটপুট তৈরি হওয়া উচিত:
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
দুটি থ্রেড পর্যায়ক্রমে চলতে থাকে, কিন্তু প্রধান থ্রেডটি handle.join()
কলের কারণে অপেক্ষা করে এবং স্পন করা থ্রেডটি শেষ না হওয়া পর্যন্ত শেষ হয় না।
কিন্তু আসুন দেখি কী হয় যখন আমরা handle.join()
কে main
এর for
লুপের আগে নিয়ে যাই, এইভাবে:
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {i} from the spawned thread!"); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!("hi number {i} from the main thread!"); thread::sleep(Duration::from_millis(1)); } }
প্রধান থ্রেডটি স্পন করা থ্রেডটি শেষ হওয়ার জন্য অপেক্ষা করবে এবং তারপর তার for
লুপ চালাবে, তাই আউটপুট আর ইন্টারলিভড হবে না, যেমনটি এখানে দেখানো হয়েছে:
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
ছোটখাটো বিবরণ, যেমন join
কোথায় কল করা হয়েছে, তা আপনার থ্রেডগুলো একই সাথে চলে কিনা তা প্রভাবিত করতে পারে।
থ্রেডের সাথে move
ক্লোজার ব্যবহার করা
আমরা প্রায়ই thread::spawn
-এ পাস করা ক্লোজারের সাথে move
কীওয়ার্ড ব্যবহার করব কারণ ক্লোজারটি তখন পরিবেশ থেকে ব্যবহৃত ভ্যালুগুলোর ওনারশিপ নিয়ে নেবে, এইভাবে সেই ভ্যালুগুলোর ওনারশিপ এক থ্রেড থেকে অন্য থ্রেডে স্থানান্তর করবে। অধ্যায় ১৩ এর "Capturing References or Moving Ownership" বিভাগে, আমরা ক্লোজারের প্রেক্ষাপটে move
নিয়ে আলোচনা করেছি। এখন আমরা move
এবং thread::spawn
-এর মধ্যেকার মিথস্ক্রিয়ার উপর আরও বেশি মনোযোগ দেব।
তালিকা ১৬-১ এ লক্ষ্য করুন যে আমরা thread::spawn
-এ যে ক্লোজারটি পাস করি তা কোনো আর্গুমেন্ট নেয় না: আমরা প্রধান থ্রেড থেকে কোনো ডেটা স্পন করা থ্রেডের কোডে ব্যবহার করছি না। প্রধান থ্রেড থেকে ডেটা স্পন করা থ্রেডে ব্যবহার করার জন্য, স্পন করা থ্রেডের ক্লোজারটিকে প্রয়োজনীয় ভ্যালুগুলো ক্যাপচার করতে হবে। তালিকা ১৬-৩ প্রধান থ্রেডে একটি ভেক্টর তৈরি করে এবং এটি স্পন করা থ্রেডে ব্যবহার করার একটি প্রচেষ্টা দেখায়। যাইহোক, এটি এখনও কাজ করবে না, যেমনটি আপনি এক মুহূর্তের মধ্যে দেখতে পাবেন।
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
ক্লোজারটি v
ব্যবহার করে, তাই এটি v
কে ক্যাপচার করবে এবং এটিকে ক্লোজারের পরিবেশের অংশ করে তুলবে। যেহেতু thread::spawn
এই ক্লোজারটিকে একটি নতুন থ্রেডে চালায়, তাই আমাদের সেই নতুন থ্রেডের ভিতরে v
অ্যাক্সেস করতে সক্ষম হওয়া উচিত। কিন্তু যখন আমরা এই উদাহরণটি কম্পাইল করি, আমরা নিম্নলিখিত এররটি পাই:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust অনুমান (infers) করে কীভাবে v
কে ক্যাপচার করতে হবে, এবং যেহেতু println!
শুধুমাত্র v
-এর একটি রেফারেন্সের প্রয়োজন, ক্লোজারটি v
-কে ধার করার চেষ্টা করে। যাইহোক, একটি সমস্যা আছে: Rust বলতে পারে না যে স্পন করা থ্রেডটি কতক্ষণ চলবে, তাই এটি জানে না যে v
-এর রেফারেন্সটি সর্বদা বৈধ থাকবে কিনা।
তালিকা ১৬-৪ এমন একটি পরিস্থিতি প্রদান করে যেখানে v
-এর একটি রেফারেন্স অবৈধ হওয়ার সম্ভাবনা বেশি।
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v); // oh no!
handle.join().unwrap();
}
যদি Rust আমাদের এই কোডটি চালানোর অনুমতি দিত, তবে একটি সম্ভাবনা ছিল যে স্পন করা থ্রেডটি überhaupt না চালিয়েই অবিলম্বে ব্যাকগ্রাউন্ডে রাখা হবে। স্পন করা থ্রেডের ভিতরে v
-এর একটি রেফারেন্স রয়েছে, কিন্তু প্রধান থ্রেডটি অবিলম্বে v
-কে ড্রপ করে দেয়, drop
ফাংশনটি ব্যবহার করে যা আমরা অধ্যায় ১৫-এ আলোচনা করেছি। তারপর, যখন স্পন করা থ্রেডটি এক্সিকিউট করা শুরু করে, v
আর বৈধ থাকে না, তাই এটির একটি রেফারেন্সও অবৈধ। ওহ নো!
তালিকা ১৬-৩ এর কম্পাইলার এররটি ঠিক করার জন্য, আমরা এরর মেসেজের পরামর্শ ব্যবহার করতে পারি:
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
ক্লোজারের আগে move
কীওয়ার্ড যোগ করে, আমরা ক্লোজারটিকে তার ব্যবহৃত ভ্যালুগুলোর ওনারশিপ নিতে বাধ্য করি, Rust-কে অনুমান করতে দেওয়ার পরিবর্তে যে এটি ভ্যালুগুলো ধার করবে। তালিকা ১৬-৩ এর পরিবর্তন যা তালিকা ১৬-৫ এ দেখানো হয়েছে, তা কম্পাইল হবে এবং আমাদের উদ্দেশ্য অনুযায়ী চলবে।
use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {v:?}"); }); handle.join().unwrap(); }
আমরা হয়তো তালিকা ১৬-৪ এর কোডটি ঠিক করার জন্য একই জিনিস চেষ্টা করতে প্রলুব্ধ হতে পারি যেখানে প্রধান থ্রেড drop
কল করেছিল একটি move
ক্লোজার ব্যবহার করে। যাইহোক, এই সমাধানটি কাজ করবে না কারণ তালিকা ১৬-৪ যা করার চেষ্টা করছে তা একটি ভিন্ন কারণে নিষিদ্ধ। যদি আমরা ক্লোজারে move
যোগ করি, আমরা v
-কে ক্লোজারের পরিবেশে নিয়ে যাব, এবং আমরা আর প্রধান থ্রেডে এটির উপর drop
কল করতে পারব না। আমরা পরিবর্তে এই কম্পাইলার এররটি পাব:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust-এর ওনারশিপের নিয়ম আমাদের আবার বাঁচিয়েছে! আমরা তালিকা ১৬-৩ এর কোড থেকে একটি এরর পেয়েছিলাম কারণ Rust রক্ষণশীল ছিল এবং থ্রেডের জন্য শুধুমাত্র v
কে ধার করছিল, যার মানে প্রধান থ্রেড তাত্ত্বিকভাবে স্পন করা থ্রেডের রেফারেন্সকে অবৈধ করতে পারত। Rust-কে v
-এর ওনারশিপ স্পন করা থ্রেডে স্থানান্তর করতে বলে, আমরা Rust-কে গ্যারান্টি দিচ্ছি যে প্রধান থ্রেড আর v
ব্যবহার করবে না। যদি আমরা তালিকা ১৬-৪ কে একই ভাবে পরিবর্তন করি, তাহলে আমরা প্রধান থ্রেডে v
ব্যবহার করার চেষ্টা করার সময় ওনারশিপের নিয়ম লঙ্ঘন করছি। move
কীওয়ার্ড Rust-এর রক্ষণশীল ডিফল্ট ধার করাকে ওভাররাইড করে; এটি আমাদের ওনারশিপের নিয়ম লঙ্ঘন করতে দেয় না।
এখন যেহেতু আমরা থ্রেড কী এবং থ্রেড API দ্বারা সরবরাহ করা মেথডগুলো আলোচনা করেছি, আসুন কিছু পরিস্থিতি দেখি যেখানে আমরা থ্রেড ব্যবহার করতে পারি।
থ্রেডের মধ্যে ডেটা স্থানান্তরের জন্য মেসেজ পাসিং ব্যবহার করা
নিরাপদ কনকারেন্সি নিশ্চিত করার একটি ক্রমবর্ধমান জনপ্রিয় পদ্ধতি হলো মেসেজ পাসিং (message passing), যেখানে থ্রেড বা অ্যাক্টররা ডেটা সহ মেসেজ একে অপরকে পাঠিয়ে যোগাযোগ করে। গো ল্যাঙ্গুয়েজের ডকুমেন্টেশন থেকে একটি স্লোগান দিয়ে ধারণাটি এখানে দেওয়া হলো: "মেমরি শেয়ার করে যোগাযোগ করবেন না; পরিবর্তে, যোগাযোগের মাধ্যমে মেমরি শেয়ার করুন।"
মেসেজ-সেন্ডিং কনকারেন্সি সম্পন্ন করার জন্য, Rust-এর স্ট্যান্ডার্ড লাইব্রেরি চ্যানেলগুলোর একটি ইমপ্লিমেন্টেশন সরবরাহ করে। একটি চ্যানেল (channel) হলো একটি সাধারণ প্রোগ্রামিং ধারণা যার মাধ্যমে ডেটা এক থ্রেড থেকে অন্য থ্রেডে পাঠানো হয়।
আপনি প্রোগ্রামিংয়ে একটি চ্যানেলকে জলের একটি দিকনির্দেশক চ্যানেলের মতো কল্পনা করতে পারেন, যেমন একটি স্রোত বা নদী। আপনি যদি একটি রাবারের হাঁসের মতো কিছু একটি নদীতে রাখেন, তবে এটি স্রোতের সাথে জলপথের শেষ পর্যন্ত ভ্রমণ করবে।
একটি চ্যানেলের দুটি অর্ধেক থাকে: একটি ট্রান্সমিটার এবং একটি রিসিভার। ট্রান্সমিটারের অর্ধেকটি হলো উজানের অবস্থান যেখানে আপনি রাবারের হাঁসটিকে নদীতে রাখেন, এবং রিসিভারের অর্ধেকটি হলো যেখানে রাবারের হাঁসটি স্রোতের শেষে পৌঁছায়। আপনার কোডের একটি অংশ আপনি যে ডেটা পাঠাতে চান তা দিয়ে ট্রান্সমিটারের মেথড কল করে, এবং অন্য একটি অংশ আগত মেসেজের জন্য রিসিভিং প্রান্তটি পরীক্ষা করে। যদি ট্রান্সমিটার বা রিসিভারের অর্ধেকটি ড্রপ করা হয় তবে একটি চ্যানেলকে বন্ধ (closed) বলা হয়।
এখানে, আমরা এমন একটি প্রোগ্রামের দিকে কাজ করব যেখানে একটি থ্রেড ভ্যালু তৈরি করে এবং সেগুলোকে একটি চ্যানেলের মাধ্যমে পাঠায়, এবং অন্য একটি থ্রেড সেই ভ্যালুগুলো গ্রহণ করে এবং সেগুলো প্রিন্ট করে। আমরা ফিচারটি চিত্রিত করার জন্য একটি চ্যানেল ব্যবহার করে থ্রেডগুলোর মধ্যে সহজ ভ্যালু পাঠাব। একবার আপনি এই কৌশলের সাথে পরিচিত হয়ে গেলে, আপনি একে অপরের সাথে যোগাযোগ করার প্রয়োজন এমন যেকোনো থ্রেডের জন্য চ্যানেল ব্যবহার করতে পারেন, যেমন একটি চ্যাট সিস্টেম বা এমন একটি সিস্টেম যেখানে অনেক থ্রেড একটি গণনার অংশ সম্পাদন করে এবং ফলাফলগুলো একত্রিত করে এমন একটি থ্রেডে অংশগুলো পাঠায়।
প্রথমে, তালিকা ১৬-৬-এ, আমরা একটি চ্যানেল তৈরি করব কিন্তু এটি দিয়ে কিছুই করব না। মনে রাখবেন যে এটি এখনও কম্পাইল হবে না কারণ Rust বলতে পারে না যে আমরা চ্যানেলের মাধ্যমে কোন ধরনের ভ্যালু পাঠাতে চাই।
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
আমরা mpsc::channel
ফাংশন ব্যবহার করে একটি নতুন চ্যানেল তৈরি করি; mpsc
মানে হলো multiple producer, single consumer (একাধিক উৎপাদক, একক ভোক্তা)। সংক্ষেপে, Rust-এর স্ট্যান্ডার্ড লাইব্রেরি যেভাবে চ্যানেলগুলো প্রয়োগ করে তার মানে হলো একটি চ্যানেলের একাধিক প্রেরণকারী প্রান্ত থাকতে পারে যা ভ্যালু তৈরি করে কিন্তু শুধুমাত্র একটি গ্রহণকারী প্রান্ত থাকে যা সেই ভ্যালুগুলো গ্রহণ করে। কল্পনা করুন একাধিক স্রোত একসাথে একটি বড় নদীতে প্রবাহিত হচ্ছে: যেকোনো স্রোতে পাঠানো সবকিছু শেষে একটি নদীতেই শেষ হবে। আমরা আপাতত একটি একক প্রডিউসার দিয়ে শুরু করব, কিন্তু এই উদাহরণটি কাজ করলে আমরা একাধিক প্রডিউসার যোগ করব।
mpsc::channel
ফাংশনটি একটি টাপল (tuple) রিটার্ন করে, যার প্রথম উপাদানটি হলো প্রেরক প্রান্ত—ট্রান্সমিটার—এবং দ্বিতীয় উপাদানটি হলো প্রাপক প্রান্ত—রিসিভার। tx
এবং rx
সংক্ষেপণগুলো ঐতিহ্যগতভাবে অনেক ক্ষেত্রে যথাক্রমে ট্রান্সমিটার (transmitter) এবং রিসিভার (receiver) এর জন্য ব্যবহৃত হয়, তাই আমরা প্রতিটি প্রান্ত নির্দেশ করার জন্য আমাদের ভ্যারিয়েবলগুলোর নাম সেভাবেই রাখি। আমরা একটি let
স্টেটমেন্ট একটি প্যাটার্নসহ ব্যবহার করছি যা টাপলটিকে ডিস্ট্রাকচার (destructures) করে; আমরা অধ্যায় ১৯-এ let
স্টেটমেন্টে প্যাটার্নের ব্যবহার এবং ডিস্ট্রাকচারিং নিয়ে আলোচনা করব। আপাতত, জেনে রাখুন যে mpsc::channel
দ্বারা রিটার্ন করা টাপলের অংশগুলো বের করার জন্য এই পদ্ধতিতে একটি let
স্টেটমেন্ট ব্যবহার করা একটি সুবিধাজনক উপায়।
আসুন প্রেরক প্রান্তটিকে একটি স্পনড থ্রেডে منتقل করি এবং এটি একটি স্ট্রিং পাঠাক যাতে স্পনড থ্রেডটি মূল থ্রেডের সাথে যোগাযোগ করে, যেমনটি তালিকা ১৬-৭-এ দেখানো হয়েছে। এটি নদীর উজানে একটি রাবারের হাঁস রাখার মতো বা এক থ্রেড থেকে অন্য থ্রেডে একটি চ্যাট বার্তা পাঠানোর মতো।
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); }
আবারও, আমরা একটি নতুন থ্রেড তৈরি করতে thread::spawn
ব্যবহার করছি এবং তারপর tx
-কে ক্লোজারে منتقل করতে move
ব্যবহার করছি যাতে স্পনড থ্রেড tx
-এর মালিক হয়। স্পনড থ্রেডটিকে চ্যানেলের মাধ্যমে বার্তা পাঠাতে সক্ষম হওয়ার জন্য ট্রান্সমিটারের মালিক হতে হবে।
ট্রান্সমিটারের একটি send
মেথড আছে যা আমরা যে ভ্যালুটি পাঠাতে চাই তা নেয়। send
মেথডটি একটি Result<T, E>
টাইপ রিটার্ন করে, তাই যদি রিসিভারটি ইতিমধ্যে ড্রপ করা হয়ে থাকে এবং একটি ভ্যালু পাঠানোর কোনো জায়গা না থাকে, তবে সেন্ড অপারেশনটি একটি এরর রিটার্ন করবে। এই উদাহরণে, আমরা একটি এররের ক্ষেত্রে প্যানিক করার জন্য unwrap
কল করছি। কিন্তু একটি বাস্তব অ্যাপ্লিকেশনে, আমরা এটি সঠিকভাবে হ্যান্ডেল করব: সঠিক এরর হ্যান্ডলিংয়ের কৌশলগুলো পর্যালোচনা করতে অধ্যায় ৯-এ ফিরে যান।
তালিকা ১৬-৮-এ, আমরা মূল থ্রেডে রিসিভার থেকে ভ্যালুটি পাব। এটি নদীর শেষে জল থেকে রাবারের হাঁসটি উদ্ধার করার মতো বা একটি চ্যাট বার্তা গ্রহণ করার মতো।
use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Got: {received}"); }
রিসিভারের দুটি দরকারী মেথড আছে: recv
এবং try_recv
। আমরা recv
ব্যবহার করছি, যা receive এর সংক্ষিপ্ত রূপ, যা মূল থ্রেডের এক্সিকিউশন ব্লক করবে এবং চ্যানেলের মাধ্যমে একটি ভ্যালু পাঠানো না হওয়া পর্যন্ত অপেক্ষা করবে। একবার একটি ভ্যালু পাঠানো হলে, recv
এটি একটি Result<T, E>
-এ রিটার্ন করবে। যখন ট্রান্সমিটার বন্ধ হয়ে যায়, recv
একটি এরর রিটার্ন করে সংকেত দেবে যে আর কোনো ভ্যালু আসছে না।
try_recv
মেথডটি ব্লক করে না, বরং অবিলম্বে একটি Result<T, E>
রিটার্ন করবে: একটি Ok
ভ্যালু যাতে একটি বার্তা থাকে যদি একটি উপলব্ধ থাকে এবং একটি Err
ভ্যালু যদি এই সময়ে কোনো বার্তা না থাকে। try_recv
ব্যবহার করা দরকারী যদি এই থ্রেডের বার্তাগুলোর জন্য অপেক্ষা করার সময় অন্য কাজ করার থাকে: আমরা একটি লুপ লিখতে পারি যা প্রতি এতক্ষণ try_recv
কল করে, একটি বার্তা উপলব্ধ থাকলে তা হ্যান্ডেল করে, এবং অন্যথায় অন্য কাজ করে কিছুক্ষণ যতক্ষণ না আবার পরীক্ষা করা হয়।
আমরা এই উদাহরণে সরলতার জন্য recv
ব্যবহার করেছি; আমাদের মূল থ্রেডের জন্য বার্তাগুলোর জন্য অপেক্ষা করা ছাড়া অন্য কোনো কাজ নেই, তাই মূল থ্রেডটিকে ব্লক করা উপযুক্ত।
যখন আমরা তালিকা ১৬-৮-এর কোডটি চালাই, আমরা মূল থ্রেড থেকে প্রিন্ট করা ভ্যালুটি দেখতে পাব:
Got: hi
পারফেক্ট!
চ্যানেল এবং মালিকানা স্থানান্তর (Channels and Ownership Transference)
মালিকানার নিয়মগুলো মেসেজ পাঠানোর ক্ষেত্রে একটি গুরুত্বপূর্ণ ভূমিকা পালন করে কারণ এগুলো আপনাকে নিরাপদ, কনকারেন্ট কোড লিখতে সাহায্য করে। আপনার Rust প্রোগ্রামজুড়ে মালিকানা নিয়ে চিন্তা করার সুবিধা হলো কনকারেন্ট প্রোগ্রামিংয়ে ভুল প্রতিরোধ করা। আসুন একটি পরীক্ষা করি যাতে চ্যানেল এবং মালিকানা কীভাবে সমস্যা প্রতিরোধ করতে একসাথে কাজ করে তা দেখানো যায়: আমরা চ্যানেলের মাধ্যমে পাঠানোর পরে স্পনড থ্রেডে একটি val
ভ্যালু ব্যবহার করার চেষ্টা করব। তালিকা ১৬-৯-এর কোডটি কম্পাইল করার চেষ্টা করুন যাতে দেখা যায় কেন এই কোডটি অনুমোদিত নয়।
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
এখানে, আমরা tx.send
-এর মাধ্যমে চ্যানেলে পাঠানোর পর val
প্রিন্ট করার চেষ্টা করছি। এটি অনুমোদন করা একটি খারাপ ধারণা হবে: একবার ভ্যালুটি অন্য থ্রেডে পাঠানো হয়ে গেলে, সেই থ্রেডটি আমরা আবার ভ্যালুটি ব্যবহার করার চেষ্টা করার আগে এটিকে পরিবর্তন বা ড্রপ করতে পারে। সম্ভবত, অন্য থ্রেডের পরিবর্তনগুলো অসংগত বা অস্তিত্বহীন ডেটার কারণে ত্রুটি বা অপ্রত্যাশিত ফলাফলের কারণ হতে পারে। যাইহোক, যদি আমরা তালিকা ১৬-৯-এর কোডটি কম্পাইল করার চেষ্টা করি তবে Rust আমাদের একটি এরর দেয়:
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:26
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
আমাদের কনকারেন্সি ভুল একটি কম্পাইল-টাইম এররের কারণ হয়েছে। send
ফাংশনটি তার প্যারামিটারের মালিকানা নেয়, এবং যখন ভ্যালুটি সরানো হয় তখন রিসিভার এটির মালিকানা নেয়। এটি আমাদের পাঠানোর পরে ঘটনাক্রমে আবার ভ্যালুটি ব্যবহার করা থেকে বিরত রাখে; মালিকানা সিস্টেম পরীক্ষা করে যে সবকিছু ঠিক আছে।
একাধিক ভ্যালু পাঠানো এবং রিসিভারকে অপেক্ষা করতে দেখা
তালিকা ১৬-৮-এর কোডটি কম্পাইল এবং রান হয়েছে, কিন্তু এটি স্পষ্টভাবে দেখায়নি যে দুটি পৃথক থ্রেড চ্যানেলের মাধ্যমে একে অপরের সাথে কথা বলছে।
তালিকা ১৬-১০-এ আমরা কিছু পরিবর্তন করেছি যা প্রমাণ করবে যে তালিকা ১৬-৮-এর কোডটি কনকারেন্টলি চলছে: স্পনড থ্রেডটি এখন একাধিক মেসেজ পাঠাবে এবং প্রতিটি মেসেজের মধ্যে এক সেকেন্ডের জন্য বিরতি দেবে।
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
এইবার, স্পনড থ্রেডে স্ট্রিংগুলোর একটি ভেক্টর আছে যা আমরা মূল থ্রেডে পাঠাতে চাই। আমরা সেগুলোর উপর ইটারেট করি, প্রতিটি পৃথকভাবে পাঠাই, এবং প্রতিটির মধ্যে এক সেকেন্ডের Duration
ভ্যালু দিয়ে thread::sleep
ফাংশন কল করে বিরতি দিই।
মূল থ্রেডে, আমরা আর স্পষ্টভাবে recv
ফাংশন কল করছি না: পরিবর্তে, আমরা rx
-কে একটি ইটারেটর হিসাবে ব্যবহার করছি। প্রতিটি প্রাপ্ত ভ্যালুর জন্য, আমরা এটি প্রিন্ট করছি। যখন চ্যানেলটি বন্ধ হয়ে যাবে, ইটারেশন শেষ হয়ে যাবে।
তালিকা ১৬-১০-এর কোডটি চালানোর সময়, আপনার প্রতিটি লাইনের মধ্যে এক-সেকেন্ডের বিরতিসহ নিম্নলিখিত আউটপুটটি দেখতে পাওয়া উচিত:
Got: hi
Got: from
Got: the
Got: thread
যেহেতু আমাদের মূল থ্রেডের for
লুপে কোনো কোড নেই যা বিরতি বা বিলম্ব করে, আমরা বলতে পারি যে মূল থ্রেডটি স্পনড থ্রেড থেকে ভ্যালু গ্রহণ করার জন্য অপেক্ষা করছে।
ট্রান্সমিটার ক্লোন করে একাধিক প্রডিউসার তৈরি করা
আগে আমরা উল্লেখ করেছি যে mpsc
হলো multiple producer, single consumer এর সংক্ষিপ্ত রূপ। আসুন mpsc
-কে কাজে লাগাই এবং তালিকা ১৬-১০-এর কোডটি প্রসারিত করে একাধিক থ্রেড তৈরি করি যা সবাই একই রিসিভারে ভ্যালু পাঠায়। আমরা ট্রান্সমিটার ক্লোন করে এটি করতে পারি, যেমনটি তালিকা ১৬-১১-এ দেখানো হয়েছে।
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// --snip--
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
// --snip--
}
এইবার, আমরা প্রথম স্পনড থ্রেড তৈরি করার আগে, আমরা ট্রান্সমিটারের উপর clone
কল করি। এটি আমাদের একটি নতুন ট্রান্সমিটার দেবে যা আমরা প্রথম স্পনড থ্রেডে পাস করতে পারি। আমরা আসল ট্রান্সমিটারটি একটি দ্বিতীয় স্পনড থ্রেডে পাস করি। এটি আমাদের দুটি থ্রেড দেয়, প্রতিটি একটি রিসিভারে ভিন্ন ভিন্ন মেসেজ পাঠায়।
যখন আপনি কোডটি চালান, আপনার আউটপুটটি এইরকম কিছু দেখতে হবে:
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
আপনার সিস্টেমের উপর নির্ভর করে আপনি ভ্যালুগুলো অন্য ক্রমে দেখতে পারেন। এটিই কনকারেন্সি কে আকর্ষণীয় এবং কঠিন করে তোলে। আপনি যদি thread::sleep
নিয়ে পরীক্ষা করেন, বিভিন্ন থ্রেডে বিভিন্ন ভ্যালু দেন, প্রতিটি রান আরও নন-ডিটারমিনিস্টিক হবে এবং প্রতিবার ভিন্ন আউটপুট তৈরি করবে।
এখন যেহেতু আমরা দেখেছি চ্যানেলগুলো কীভাবে কাজ করে, আসুন কনকারেন্সির একটি ভিন্ন পদ্ধতি দেখি।
শেয়ারড-স্টেট কনকারেন্সি (Shared-State Concurrency)
মেসেজ পাসিং কনকারেন্সি পরিচালনা করার একটি চমৎকার উপায়, কিন্তু এটিই একমাত্র উপায় নয়। আরেকটি পদ্ধতি হলো একাধিক থ্রেডের একই শেয়ারড ডেটা অ্যাক্সেস করা। গো ল্যাঙ্গুয়েজের ডকুমেন্টেশনের স্লোগানের এই অংশটি আবার বিবেচনা করুন: "মেমরি শেয়ার করে যোগাযোগ করবেন না।"
মেমরি শেয়ার করে যোগাযোগ করা দেখতে কেমন হবে? এছাড়াও, মেসেজ পাসিংয়ের উৎসাহীরা কেন মেমরি শেয়ারিং ব্যবহার না করার জন্য সাবধান করে?
একভাবে, যেকোনো প্রোগ্রামিং ল্যাঙ্গুয়েজে চ্যানেলগুলো একক মালিকানার (single ownership) মতো কারণ একবার আপনি একটি চ্যানেলের মাধ্যমে একটি ভ্যালু স্থানান্তর করলে, আপনার আর সেই ভ্যালুটি ব্যবহার করা উচিত নয়। শেয়ারড-মেমরি কনকারেন্সি একাধিক মালিকানার (multiple ownership) মতো: একাধিক থ্রেড একই সময়ে একই মেমরি লোকেশন অ্যাক্সেস করতে পারে। যেমনটি আপনি অধ্যায় ১৫-এ দেখেছেন, যেখানে স্মার্ট পয়েন্টার একাধিক মালিকানা সম্ভব করেছিল, একাধিক মালিকানা জটিলতা বাড়াতে পারে কারণ এই বিভিন্ন মালিকদের পরিচালনা করার প্রয়োজন হয়। Rust-এর টাইপ সিস্টেম এবং মালিকানার নিয়মগুলো এই পরিচালনা সঠিকভাবে করতে ব্যাপকভাবে সহায়তা করে। একটি উদাহরণ হিসাবে, আসুন আমরা মিউটেক্স (mutexes) দেখি, যা শেয়ারড মেমোরির জন্য অন্যতম সাধারণ কনকারেন্সি প্রিমিটিভ।
এক সময়ে একটি থ্রেড থেকে ডেটা অ্যাক্সেসের অনুমতি দেওয়ার জন্য মিউটেক্স ব্যবহার করা
মিউটেক্স (Mutex) হলো মিউচুয়াল এক্সক্লুশন (mutual exclusion) এর একটি সংক্ষিপ্ত রূপ, যেমন একটি মিউটেক্স যেকোনো সময়ে শুধুমাত্র একটি থ্রেডকে কিছু ডেটা অ্যাক্সেস করার অনুমতি দেয়। একটি মিউটেক্সের ডেটা অ্যাক্সেস করার জন্য, একটি থ্রেডকে প্রথমে মিউটেক্সের লক (lock) অর্জন করার জন্য অনুরোধ করে অ্যাক্সেস চাওয়ার সংকেত দিতে হয়। লক হলো একটি ডেটা স্ট্রাকচার যা মিউটেক্সের অংশ এবং এটি ট্র্যাক রাখে যে বর্তমানে কার ডেটাতে একচেটিয়া অ্যাক্সেস রয়েছে। অতএব, মিউটেক্সকে লকিং সিস্টেমের মাধ্যমে তার ধারণ করা ডেটা গার্ড (guarding) করছে বলে বর্ণনা করা হয়।
মিউটেক্স ব্যবহার করা কঠিন বলে একটি খ্যাতি আছে কারণ আপনাকে দুটি নিয়ম মনে রাখতে হবে:
১. ডেটা ব্যবহার করার আগে আপনাকে অবশ্যই লক অর্জন করার চেষ্টা করতে হবে। ২. যখন মিউটেক্স দ্বারা সুরক্ষিত ডেটার সাথে আপনার কাজ শেষ হয়ে যায়, তখন আপনাকে অবশ্যই ডেটা আনলক করতে হবে যাতে অন্য থ্রেডগুলো লক অর্জন করতে পারে।
মিউটেক্সের একটি বাস্তব-জগতের উপমা হিসাবে, একটি সম্মেলনের প্যানেল আলোচনার কথা কল্পনা করুন যেখানে শুধুমাত্র একটি মাইক্রোফোন রয়েছে। একজন প্যানেলিস্ট কথা বলার আগে, তাকে মাইক্রোফোন ব্যবহার করতে চাওয়ার জন্য অনুরোধ বা সংকেত দিতে হবে। যখন সে মাইক্রোফোন পায়, তখন সে যতক্ষণ চায় কথা বলতে পারে এবং তারপর পরবর্তী প্যানেলিস্টকে মাইক্রোফোনটি হস্তান্তর করে যে কথা বলতে অনুরোধ করে। যদি একজন প্যানেলিস্ট কথা শেষ করে মাইক্রোফোনটি হস্তান্তর করতে ভুলে যায়, তবে অন্য কেউ কথা বলতে পারে না। যদি শেয়ারড মাইক্রোফোনের ব্যবস্থাপনায় ভুল হয়, প্যানেলটি পরিকল্পনা অনুযায়ী কাজ করবে না!
মিউটেক্সের ব্যবস্থাপনা সঠিকভাবে করা অবিশ্বাস্যভাবে কঠিন হতে পারে, যে কারণে এত লোক চ্যানেল সম্পর্কে উত্সাহী। যাইহোক, Rust-এর টাইপ সিস্টেম এবং মালিকানার নিয়মগুলোর জন্য ধন্যবাদ, আপনি লকিং এবং আনলকিংয়ে ভুল করতে পারবেন না।
Mutex<T>
এর API
একটি মিউটেক্স কীভাবে ব্যবহার করতে হয় তার একটি উদাহরণ হিসাবে, আসুন আমরা একটি একক-থ্রেডেড প্রেক্ষাপটে একটি মিউটেক্স ব্যবহার করে শুরু করি, যেমনটি তালিকা ১৬-১২-এ দেখানো হয়েছে।
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {m:?}"); }
অনেক টাইপের মতোই, আমরা অ্যাসোসিয়েটেড ফাংশন new
ব্যবহার করে একটি Mutex<T>
তৈরি করি। মিউটেক্সের ভিতরের ডেটা অ্যাক্সেস করার জন্য, আমরা লক অর্জন করতে lock
মেথড ব্যবহার করি। এই কলটি বর্তমান থ্রেডটিকে ব্লক করবে যাতে এটি কোনো কাজ করতে না পারে যতক্ষণ না আমাদের লক পাওয়ার পালা আসে।
lock
এর কলটি ব্যর্থ হবে যদি লক ধরে রাখা অন্য কোনো থ্রেড প্যানিক করে। সেক্ষেত্রে, কেউ কখনও লকটি পেতে পারবে না, তাই আমরা unwrap
করতে বেছে নিয়েছি এবং যদি আমরা সেই পরিস্থিতিতে থাকি তবে এই থ্রেডটি প্যানিক করবে।
আমরা লকটি অর্জন করার পরে, আমরা রিটার্ন ভ্যালুটিকে, এই ক্ষেত্রে যার নাম num
, ভিতরের ডেটার একটি মিউটেবল রেফারেন্স (mutable reference) হিসাবে ব্যবহার করতে পারি। টাইপ সিস্টেম নিশ্চিত করে যে m
-এর ভ্যালু ব্যবহার করার আগে আমরা একটি লক অর্জন করি। m
-এর টাইপ হলো Mutex<i32>
, i32
নয়, তাই i32
ভ্যালুটি ব্যবহার করতে সক্ষম হওয়ার জন্য আমাদের অবশ্যই lock
কল করতে হবে। আমরা ভুলতে পারি না; টাইপ সিস্টেম অন্যথায় আমাদের ভিতরের i32
অ্যাক্সেস করতে দেবে না।
lock
এর কলটি MutexGuard
নামের একটি টাইপ রিটার্ন করে, যা একটি LockResult
-এ মোড়ানো থাকে যা আমরা unwrap
-এর কল দিয়ে হ্যান্ডেল করেছি। MutexGuard
টাইপটি আমাদের ভিতরের ডেটার দিকে নির্দেশ করতে Deref
ইমপ্লিমেন্ট করে; এই টাইপের একটি Drop
ইমপ্লিমেন্টেশনও রয়েছে যা MutexGuard
স্কোপের বাইরে চলে গেলে স্বয়ংক্রিয়ভাবে লকটি ছেড়ে দেয়, যা ভিতরের স্কোপের শেষে ঘটে। ফলস্বরূপ, আমরা লকটি ছেড়ে দিতে ভুলে যাওয়ার এবং মিউটেক্সটিকে অন্য থ্রেড দ্বারা ব্যবহৃত হতে ব্লক করার ঝুঁকি নিই না কারণ লক রিলিজ স্বয়ংক্রিয়ভাবে ঘটে।
লকটি ড্রপ করার পরে, আমরা মিউটেক্সের ভ্যালু প্রিন্ট করতে পারি এবং দেখতে পারি যে আমরা ভিতরের i32
কে 6
-এ পরিবর্তন করতে সক্ষম হয়েছি।
একাধিক থ্রেডের মধ্যে একটি Mutex<T>
শেয়ার করা
এখন আসুন একাধিক থ্রেডের মধ্যে Mutex<T>
ব্যবহার করে একটি ভ্যালু শেয়ার করার চেষ্টা করি। আমরা ১০টি থ্রেড চালু করব এবং তাদের প্রত্যেককে একটি কাউন্টার ভ্যালু ১ করে বাড়াতে বলব, যাতে কাউন্টার ০ থেকে ১০ পর্যন্ত যায়। তালিকা ১৬-১৩-এর উদাহরণটিতে একটি কম্পাইলার এরর থাকবে, এবং আমরা সেই এররটি Mutex<T>
ব্যবহার সম্পর্কে এবং Rust কীভাবে আমাদের এটি সঠিকভাবে ব্যবহার করতে সাহায্য করে সে সম্পর্কে আরও জানতে ব্যবহার করব।
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
আমরা একটি counter
ভ্যারিয়েবল তৈরি করি যা একটি Mutex<T>
-এর ভিতরে একটি i32
ধারণ করে, যেমনটি আমরা তালিকা ১৬-১২-এ করেছি। এরপর, আমরা একটি সংখ্যার পরিসরের উপর ইটারেট করে ১০টি থ্রেড তৈরি করি। আমরা thread::spawn
ব্যবহার করি এবং সমস্ত থ্রেডকে একই ক্লোজার দিই: একটি যা কাউন্টারটিকে থ্রেডে منتقل করে, lock
মেথড কল করে Mutex<T>
-এর উপর একটি লক অর্জন করে, এবং তারপর মিউটেক্সের ভ্যালুতে ১ যোগ করে। যখন একটি থ্রেড তার ক্লোজার চালানো শেষ করে, num
স্কোপের বাইরে চলে যাবে এবং লকটি ছেড়ে দেবে যাতে অন্য থ্রেড এটি অর্জন করতে পারে।
মূল থ্রেডে, আমরা সমস্ত জয়েন হ্যান্ডেল সংগ্রহ করি। তারপর, যেমনটি আমরা তালিকা ১৬-২-এ করেছি, আমরা প্রতিটি হ্যান্ডেলের উপর join
কল করি যাতে নিশ্চিত করা যায় যে সমস্ত থ্রেড শেষ হয়েছে। সেই সময়ে, মূল থ্রেডটি লক অর্জন করবে এবং এই প্রোগ্রামের ফলাফল প্রিন্ট করবে।
আমরা ইঙ্গিত দিয়েছিলাম যে এই উদাহরণটি কম্পাইল হবে না। এখন আসুন জেনে নেওয়া যাক কেন!
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
এরর মেসেজটি বলছে যে counter
ভ্যালুটি লুপের আগের ইটারেশনে সরানো হয়েছে। Rust আমাদের বলছে যে আমরা লক counter
-এর মালিকানা একাধিক থ্রেডে منتقل করতে পারি না। আসুন আমরা অধ্যায় ১৫-এ আলোচনা করা একাধিক মালিকানার পদ্ধতি দিয়ে কম্পাইলার এররটি ঠিক করি।
একাধিক থ্রেডের সাথে একাধিক মালিকানা
অধ্যায় ১৫-এ, আমরা একটি রেফারেন্স কাউন্টেড ভ্যালু তৈরি করতে স্মার্ট পয়েন্টার Rc<T>
ব্যবহার করে একটি ভ্যালুকে একাধিক মালিক দিয়েছিলাম। আসুন এখানে একই কাজ করি এবং দেখি কী হয়। আমরা তালিকা ১৬-১৪-এ Mutex<T>
কে Rc<T>
-তে মোড়াব এবং থ্রেডে মালিকানা منتقل করার আগে Rc<T>
ক্লোন করব।
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
আবারও, আমরা কম্পাইল করি এবং... ভিন্ন এরর পাই! কম্পাইলার আমাদের অনেক কিছু শেখাচ্ছে।
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/std/src/thread/mod.rs:728:1
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
বাহ, এই এরর মেসেজটি খুব শব্দবহুল! এখানে মনোযোগ দেওয়ার গুরুত্বপূর্ণ অংশটি হলো: `Rc<Mutex<i32>>` cannot be sent between threads safely
। কম্পাইলার আমাদের কারণটিও বলছে: the trait `Send` is not implemented for `Rc<Mutex<i32>>`
। আমরা পরবর্তী বিভাগে Send
সম্পর্কে কথা বলব: এটি এমন একটি ট্রেইট যা নিশ্চিত করে যে আমরা থ্রেডের সাথে যে টাইপগুলো ব্যবহার করি তা কনকারেন্ট পরিস্থিতিতে ব্যবহারের জন্য তৈরি।
দুর্ভাগ্যবশত, Rc<T>
থ্রেড জুড়ে শেয়ার করার জন্য নিরাপদ নয়। যখন Rc<T>
রেফারেন্স কাউন্ট পরিচালনা করে, তখন এটি clone
-এর প্রতিটি কলের জন্য কাউন্ট যোগ করে এবং প্রতিটি ক্লোন ড্রপ করা হলে কাউন্ট থেকে বিয়োগ করে। কিন্তু এটি কোনো কনকারেন্সি প্রিমিটিভ ব্যবহার করে না যাতে নিশ্চিত করা যায় যে কাউন্টের পরিবর্তনগুলো অন্য কোনো থ্রেড দ্বারা বাধাগ্রস্ত হতে না পারে। এটি ভুল গণনার কারণ হতে পারে—সূক্ষ্ম বাগ যা ফলস্বরূপ মেমরি লিক বা আমাদের কাজ শেষ হওয়ার আগে একটি ভ্যালু ড্রপ হয়ে যাওয়ার কারণ হতে পারে। আমাদের যা প্রয়োজন তা হলো এমন একটি টাইপ যা হুবহু Rc<T>
-এর মতো, কিন্তু যা রেফারেন্স কাউন্টের পরিবর্তনগুলো একটি থ্রেড-সেফ উপায়ে করে।
Arc<T>
এর সাথে অ্যাটমিক রেফারেন্স কাউন্টিং
ভাগ্যক্রমে, Arc<T>
হলো Rc<T>
-এর মতো একটি টাইপ যা কনকারেন্ট পরিস্থিতিতে ব্যবহার করার জন্য নিরাপদ। a এর অর্থ হলো অ্যাটমিক, যার মানে এটি একটি অ্যাটমিকালি রেফারেন্স-কাউন্টেড টাইপ। অ্যাটমিক্স হলো অতিরিক্ত এক ধরনের কনকারেন্সি প্রিমিটিভ যা আমরা এখানে বিস্তারিতভাবে আলোচনা করব না: আরও বিস্তারিত জানার জন্য স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশন std::sync::atomic
দেখুন। এই মুহূর্তে, আপনাকে শুধু জানতে হবে যে অ্যাটমিক্স প্রিমিটিভ টাইপের মতো কাজ করে কিন্তু থ্রেড জুড়ে শেয়ার করার জন্য নিরাপদ।
আপনি তখন ভাবতে পারেন কেন সমস্ত প্রিমিটিভ টাইপ অ্যাটমিক নয় এবং কেন স্ট্যান্ডার্ড লাইব্রেরি টাইপগুলো ডিফল্টরূপে Arc<T>
ব্যবহার করার জন্য ইমপ্লিমেন্ট করা হয় না। কারণ হলো থ্রেড সেফটির সাথে একটি পারফরম্যান্স পেনাল্টি আসে যা আপনি শুধুমাত্র যখন সত্যিই প্রয়োজন তখনই দিতে চান। আপনি যদি শুধুমাত্র একটি একক থ্রেডের মধ্যে ভ্যালুগুলোর উপর অপারেশন করেন, তবে আপনার কোড দ্রুত চলতে পারে যদি এটিকে অ্যাটমিক্স যে গ্যারান্টি প্রদান করে তা প্রয়োগ করতে না হয়।
আসুন আমাদের উদাহরণে ফিরে যাই: Arc<T>
এবং Rc<T>
-এর একই API রয়েছে, তাই আমরা use
লাইন, new
-এর কল এবং clone
-এর কল পরিবর্তন করে আমাদের প্রোগ্রামটি ঠিক করি। তালিকা ১৬-১৫-এর কোডটি অবশেষে কম্পাইল হবে এবং চলবে।
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
এই কোডটি নিম্নলিখিতটি প্রিন্ট করবে:
Result: 10
আমরা এটা করেছি! আমরা ০ থেকে ১০ পর্যন্ত গণনা করেছি, যা খুব চিত্তাকর্ষক মনে নাও হতে পারে, কিন্তু এটি আমাদের Mutex<T>
এবং থ্রেড সেফটি সম্পর্কে অনেক কিছু শিখিয়েছে। আপনি এই প্রোগ্রামের কাঠামোটি শুধু একটি কাউন্টার বাড়ানোর চেয়ে আরও জটিল অপারেশন করার জন্যও ব্যবহার করতে পারেন। এই কৌশলটি ব্যবহার করে, আপনি একটি গণনাকাজকে স্বাধীন অংশে ভাগ করতে পারেন, সেই অংশগুলোকে থ্রেড জুড়ে বিভক্ত করতে পারেন, এবং তারপর প্রতিটি থ্রেডকে তার অংশ দিয়ে চূড়ান্ত ফলাফল আপডেট করার জন্য একটি Mutex<T>
ব্যবহার করতে পারেন।
মনে রাখবেন যে আপনি যদি সহজ সাংখ্যিক অপারেশন করেন, তবে স্ট্যান্ডার্ড লাইব্রেরির std::sync::atomic
মডিউল দ্বারা প্রদত্ত Mutex<T>
টাইপের চেয়ে সহজ টাইপ রয়েছে। এই টাইপগুলো প্রিমিটিভ টাইপগুলোতে নিরাপদ, কনকারেন্ট, অ্যাটমিক অ্যাক্সেস প্রদান করে। আমরা এই উদাহরণের জন্য একটি প্রিমিটিভ টাইপের সাথে Mutex<T>
ব্যবহার করতে বেছে নিয়েছি যাতে আমরা Mutex<T>
কীভাবে কাজ করে তার উপর মনোযোগ দিতে পারি।
RefCell<T>
/Rc<T>
এবং Mutex<T>
/Arc<T>
এর মধ্যে সাদৃশ্য
আপনি হয়তো লক্ষ্য করেছেন যে counter
অপরিবর্তনীয় কিন্তু আমরা এর ভিতরের ভ্যালুতে একটি মিউটেবল রেফারেন্স পেতে পারি; এর মানে হলো Mutex<T>
ইন্টেরিয়র মিউটেবিলিটি (interior mutability) প্রদান করে, যেমন Cell
পরিবার করে। অধ্যায় ১৫-এ আমরা যেভাবে Rc<T>
-এর ভিতরের বিষয়বস্তু মিউটেট করার অনুমতি দেওয়ার জন্য RefCell<T>
ব্যবহার করেছি, সেভাবেই আমরা Arc<T>
-এর ভিতরের বিষয়বস্তু মিউটেট করার জন্য Mutex<T>
ব্যবহার করি।
আরেকটি বিষয় লক্ষ্য করার মতো হলো যে আপনি যখন Mutex<T>
ব্যবহার করেন তখন Rust আপনাকে সব ধরনের লজিক এরর থেকে রক্ষা করতে পারে না। অধ্যায় ১৫ থেকে মনে করুন যে Rc<T>
ব্যবহার করার সাথে রেফারেন্স সাইকেল (reference cycles) তৈরি করার ঝুঁকি ছিল, যেখানে দুটি Rc<T>
ভ্যালু একে অপরকে রেফার করে, যা মেমরি লিকের কারণ হয়। একইভাবে, Mutex<T>
-এর সাথে ডেডলক (deadlocks) তৈরি করার ঝুঁকি রয়েছে। এটি তখন ঘটে যখন একটি অপারেশনের জন্য দুটি রিসোর্স লক করার প্রয়োজন হয় এবং দুটি থ্রেড প্রত্যেকে একটি করে লক অর্জন করে, যার ফলে তারা একে অপরের জন্য চিরকাল অপেক্ষা করে। আপনি যদি ডেডলকে আগ্রহী হন, তবে একটি Rust প্রোগ্রাম তৈরি করার চেষ্টা করুন যাতে একটি ডেডলক আছে; তারপর যেকোনো ল্যাঙ্গুয়েজে মিউটেক্সের জন্য ডেডলক প্রশমন কৌশল নিয়ে গবেষণা করুন এবং Rust-এ সেগুলো ইমপ্লিমেন্ট করার চেষ্টা করুন। Mutex<T>
এবং MutexGuard
-এর জন্য স্ট্যান্ডার্ড লাইব্রেরি API ডকুমেন্টেশন দরকারী তথ্য প্রদান করে।
আমরা Send
এবং Sync
ট্রেইট এবং কীভাবে আমরা কাস্টম টাইপের সাথে সেগুলো ব্যবহার করতে পারি সে সম্পর্কে কথা বলে এই অধ্যায়টি শেষ করব।
Send
এবং Sync
ট্রেইট দিয়ে এক্সটেনসিবল কনকারেন্সি
মজার ব্যাপার হলো, এই অধ্যায়ে আমরা এখন পর্যন্ত যে সমস্ত কনকারেন্সি ফিচার নিয়ে আলোচনা করেছি, তার প্রায় সবগুলোই স্ট্যান্ডার্ড লাইব্রেরির অংশ ছিল, ল্যাঙ্গুয়েজের নয়। আপনার কনকারেন্সি পরিচালনা করার বিকল্পগুলো ল্যাঙ্গুয়েজ বা স্ট্যান্ডার্ড লাইব্রেরির মধ্যে সীমাবদ্ধ নয়; আপনি আপনার নিজের কনকারেন্সি ফিচার লিখতে পারেন বা অন্যদের লেখা ফিচার ব্যবহার করতে পারেন।
তবে, যে মূল কনকারেন্সি ধারণাগুলো স্ট্যান্ডার্ড লাইব্রেরির পরিবর্তে ল্যাঙ্গুয়েজে এম্বেড করা আছে, তার মধ্যে রয়েছে std::marker
ট্রেইট Send
এবং Sync
।
Send
দিয়ে থ্রেডের মধ্যে মালিকানা স্থানান্তরের অনুমতি দেওয়া
Send
মার্কার ট্রেইটটি নির্দেশ করে যে Send
ইমপ্লিমেন্ট করা টাইপের ভ্যালুগুলোর মালিকানা থ্রেডের মধ্যে স্থানান্তর করা যেতে পারে। প্রায় প্রতিটি Rust টাইপ Send
ইমপ্লিমেন্ট করে, তবে কিছু ব্যতিক্রম আছে, যার মধ্যে Rc<T>
অন্তর্ভুক্ত: এটি Send
ইমপ্লিমেন্ট করতে পারে না কারণ আপনি যদি একটি Rc<T>
ভ্যালু ক্লোন করেন এবং ক্লোনটির মালিকানা অন্য থ্রেডে স্থানান্তর করার চেষ্টা করেন, তবে উভয় থ্রেড একই সাথে রেফারেন্স কাউন্ট আপডেট করতে পারে। এই কারণে, Rc<T>
একক-থ্রেডেড পরিস্থিতিতে ব্যবহারের জন্য ইমপ্লিমেন্ট করা হয়েছে যেখানে আপনি থ্রেড-সেফ পারফরম্যান্স পেনাল্টি দিতে চান না।
অতএব, Rust-এর টাইপ সিস্টেম এবং ট্রেইট বাউন্ডস নিশ্চিত করে যে আপনি কখনও ভুলবশত একটি Rc<T>
ভ্যালু অনিরাপদভাবে থ্রেডের মধ্যে পাঠাতে পারবেন না। যখন আমরা তালিকা ১৬-১৪-এ এটি করার চেষ্টা করেছিলাম, তখন আমরা the trait `Send` is not implemented for `Rc<Mutex<i32>>`
এই এররটি পেয়েছিলাম। যখন আমরা Arc<T>
-এ স্যুইচ করেছিলাম, যা Send
ইমপ্লিমেন্ট করে, কোডটি কম্পাইল হয়েছিল।
সম্পূর্ণরূপে Send
টাইপ দ্বারা গঠিত যেকোনো টাইপ স্বয়ংক্রিয়ভাবে Send
হিসাবে চিহ্নিত হয়। কাঁচা পয়েন্টার (raw pointers) ছাড়া প্রায় সব প্রিমিটিভ টাইপই Send
, যা আমরা অধ্যায় ২০-এ আলোচনা করব।
Sync
দিয়ে একাধিক থ্রেড থেকে অ্যাক্সেসের অনুমতি দেওয়া
Sync
মার্কার ট্রেইটটি নির্দেশ করে যে Sync
ইমপ্লিমেন্ট করা টাইপটিকে একাধিক থ্রেড থেকে রেফারেন্স করা নিরাপদ। অন্য কথায়, যেকোনো টাইপ T
Sync
ইমপ্লিমেন্ট করে যদি &T
(T
-এর একটি অপরিবর্তনীয় রেফারেন্স) Send
ইমপ্লিমেন্ট করে, যার মানে রেফারেন্সটি নিরাপদে অন্য থ্রেডে পাঠানো যেতে পারে। Send
-এর মতো, প্রিমিটিভ টাইপগুলো সবই Sync
ইমপ্লিমেন্ট করে, এবং যে টাইপগুলো সম্পূর্ণরূপে Sync
ইমপ্লিমেন্ট করা টাইপ দ্বারা গঠিত সেগুলোও Sync
ইমপ্লিমেন্ট করে।
স্মার্ট পয়েন্টার Rc<T>
ও Sync
ইমপ্লিমেন্ট করে না সেই একই কারণে যে এটি Send
ইমপ্লিমেন্ট করে না। RefCell<T>
টাইপ (যা আমরা অধ্যায় ১৫-এ আলোচনা করেছি) এবং সম্পর্কিত Cell<T>
টাইপের পরিবার Sync
ইমপ্লিমেন্ট করে না। RefCell<T>
রানটাইমে যে ধার পরীক্ষা (borrow checking) প্রয়োগ করে তা থ্রেড-সেফ নয়। স্মার্ট পয়েন্টার Mutex<T>
Sync
ইমপ্লিমেন্ট করে এবং একাধিক থ্রেডের সাথে অ্যাক্সেস শেয়ার করতে ব্যবহার করা যেতে পারে, যেমনটি আপনি "Sharing a Mutex<T>
Between Multiple Threads" বিভাগে দেখেছেন।
Send
এবং Sync
ম্যানুয়ালি ইমপ্লিমেন্ট করা অনিরাপদ
যেহেতু যে টাইপগুলো সম্পূর্ণরূপে Send
এবং Sync
ট্রেইট ইমপ্লিমেন্ট করা অন্য টাইপ দ্বারা গঠিত সেগুলোও স্বয়ংক্রিয়ভাবে Send
এবং Sync
ইমপ্লিমেন্ট করে, তাই আমাদের সেই ট্রেইটগুলো ম্যানুয়ালি ইমপ্লিমেন্ট করতে হয় না। মার্কার ট্রেইট হিসাবে, তাদের ইমপ্লিমেন্ট করার জন্য কোনো মেথডও নেই। তারা শুধু কনকারেন্সি সম্পর্কিত ইনভ্যারিয়েন্টগুলো প্রয়োগ করার জন্য দরকারি।
এই ট্রেইটগুলো ম্যানুয়ালি ইমপ্লিমেন্ট করার জন্য অনিরাপদ (unsafe) Rust কোড ইমপ্লিমেন্ট করতে হয়। আমরা অধ্যায় ২০-এ অনিরাপদ Rust কোড ব্যবহার করার বিষয়ে কথা বলব; আপাতত, গুরুত্বপূর্ণ তথ্য হলো Send
এবং Sync
অংশ দ্বারা গঠিত নয় এমন নতুন কনকারেন্ট টাইপ তৈরি করার জন্য সেফটি গ্যারান্টি বজায় রাখার জন্য সতর্ক চিন্তাভাবনার প্রয়োজন। "The Rustonomicon" এই গ্যারান্টিগুলো এবং কীভাবে সেগুলো বজায় রাখতে হয় সে সম্পর্কে আরও তথ্য রয়েছে।
সারসংক্ষেপ
এই বইয়ে আপনি কনকারেন্সি নিয়ে শেষবারের মতো দেখছেন না: পরবর্তী অধ্যায়টি অ্যাসিঙ্ক প্রোগ্রামিংয়ের উপর আলোকপাত করে, এবং অধ্যায় ২১-এর প্রকল্পটি এই অধ্যায়ের ধারণাগুলো এখানে আলোচনা করা ছোট উদাহরণগুলোর চেয়ে আরও বাস্তবসম্মত পরিস্থিতিতে ব্যবহার করবে।
যেমন আগে উল্লেখ করা হয়েছে, যেহেতু Rust যেভাবে কনকারেন্সি পরিচালনা করে তার খুব কম অংশই ল্যাঙ্গুয়েজের অংশ, তাই অনেক কনকারেন্সি সলিউশন ক্রেট হিসাবে ইমপ্লিমেন্ট করা হয়। এগুলো স্ট্যান্ডার্ড লাইব্রেরির চেয়ে দ্রুত বিকশিত হয়, তাই মাল্টিথ্রেডেড পরিস্থিতিতে ব্যবহারের জন্য বর্তমান, অত্যাধুনিক ক্রেটগুলোর জন্য অনলাইনে অনুসন্ধান করতে ভুলবেন না।
Rust স্ট্যান্ডার্ড লাইব্রেরি মেসেজ পাসিংয়ের জন্য চ্যানেল এবং স্মার্ট পয়েন্টার টাইপ, যেমন Mutex<T>
এবং Arc<T>
, সরবরাহ করে যা কনকারেন্ট প্রেক্ষাপটে ব্যবহার করা নিরাপদ। টাইপ সিস্টেম এবং বোরো চেকার নিশ্চিত করে যে এই সলিউশনগুলো ব্যবহার করা কোডে ডেটা রেস বা অবৈধ রেফারেন্স থাকবে না। একবার আপনি আপনার কোড কম্পাইল করতে পারলে, আপনি নিশ্চিন্ত থাকতে পারেন যে এটি একাধিক থ্রেডে সুখে চলবে এবং অন্য ল্যাঙ্গুয়েজে সাধারণ এমন কঠিন-থেকে-ট্র্যাক-ডাউন করা বাগগুলো থাকবে না। কনকারেন্ট প্রোগ্রামিং আর ভয়ের কোনো ধারণা নয়: নির্ভীকভাবে এগিয়ে যান এবং আপনার প্রোগ্রামগুলোকে কনকারেন্ট করুন!
অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের মূলনীতি: Async, Await, Futures, এবং Streams
আমরা কম্পিউটারকে এমন অনেক কাজ করতে বলি যা শেষ হতে বেশ কিছুটা সময় নিতে পারে। এই দীর্ঘ সময় ধরে চলা প্রসেসগুলো শেষ হওয়ার জন্য অপেক্ষা করার সময় যদি আমরা অন্য কিছু করতে পারতাম, তাহলে খুব ভালো হতো। আধুনিক কম্পিউটারগুলো একই সময়ে একাধিক অপারেশন নিয়ে কাজ করার জন্য দুটি কৌশল প্রদান করে: প্যারালালিসম (parallelism) এবং কনকারেন্সি (concurrency)। যখন আমরা এমন প্রোগ্রাম লিখতে শুরু করি যেখানে প্যারালাল বা কনকারেন্ট অপারেশন জড়িত থাকে, তখন আমরা দ্রুতই অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের কিছু নতুন চ্যালেঞ্জের মুখোমুখি হই, যেখানে অপারেশনগুলো যে ক্রমে শুরু হয়েছিল সেই ক্রমে শেষ নাও হতে পারে। এই অধ্যায়টি চ্যাপ্টার ১৬-তে প্যারালালিসম এবং কনকারেন্সির জন্য থ্রেডের ব্যবহারের উপর ভিত্তি করে তৈরি হয়েছে এবং অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের জন্য একটি বিকল্প পদ্ধতির সাথে পরিচয় করিয়ে দেবে: রাস্টের Futures, Streams, তাদের সমর্থনকারী async
এবং await
সিনট্যাক্স, এবং অ্যাসিঙ্ক্রোনাস অপারেশনগুলো পরিচালনা ও সমন্বয় করার টুলস।
আসুন একটি উদাহরণ বিবেচনা করা যাক। ধরুন আপনি একটি পারিবারিক অনুষ্ঠানের ভিডিও এক্সপোর্ট করছেন, এই কাজটি শেষ হতে মিনিট থেকে ঘণ্টা পর্যন্ত সময় লাগতে পারে। ভিডিও এক্সপোর্টটি তার সাধ্যমতো CPU এবং GPU পাওয়ার ব্যবহার করবে। যদি আপনার কেবল একটি CPU কোর থাকত এবং আপনার অপারেটিং সিস্টেম এক্সপোর্টটি শেষ না হওয়া পর্যন্ত থামিয়ে না রাখত—অর্থাৎ, যদি এটি এক্সপোর্টটি সিঙ্ক্রোনাসভাবে (synchronously) চালাত—তাহলে ঐ টাস্ক চলাকালীন আপনি আপনার কম্পিউটারে অন্য কিছুই করতে পারতেন না। এটি একটি বেশ হতাশাজনক অভিজ্ঞতা হতো। ভাগ্যক্রমে, আপনার কম্পিউটারের অপারেটিং সিস্টেম এক্সপোর্টটিকে প্রায়শই অদৃশ্যভাবে বাধাগ্রস্ত করতে পারে যাতে আপনি একই সাথে অন্যান্য কাজ করতে পারেন।
এখন ধরুন আপনি অন্য কারো শেয়ার করা একটি ভিডিও ডাউনলোড করছেন, যা শেষ হতেও বেশ সময় লাগতে পারে কিন্তু এটি ততটা CPU সময় নেয় না। এক্ষেত্রে, নেটওয়ার্ক থেকে ডেটা আসার জন্য CPU-কে অপেক্ষা করতে হয়। ডেটা আসা শুরু হলে আপনি ডেটা পড়া শুরু করতে পারলেও, সব ডেটা আসতে কিছুটা সময় লাগতে পারে। এমনকি সব ডেটা উপস্থিত থাকলেও, যদি ভিডিওটি বেশ বড় হয়, তবে এটি লোড করতে অন্তত এক বা দুই সেকেন্ড সময় লাগতে পারে। এটি হয়তো খুব বেশি সময় মনে নাও হতে পারে, কিন্তু একটি আধুনিক প্রসেসরের জন্য এটি অনেক দীর্ঘ সময়, যা প্রতি সেকেন্ডে বিলিয়ন অপারেশন করতে পারে। আবারও, আপনার অপারেটিং সিস্টেম আপনার প্রোগ্রামকে অদৃশ্যভাবে বাধাগ্রস্ত করবে যাতে নেটওয়ার্ক কল শেষ হওয়ার জন্য অপেক্ষা করার সময় CPU অন্য কাজ করতে পারে।
ভিডিও এক্সপোর্ট হলো একটি CPU-বাউন্ড বা কম্পিউট-বাউন্ড অপারেশনের উদাহরণ। এটি কম্পিউটারের CPU বা GPU-এর ডেটা প্রসেসিং স্পিডের উপর সীমাবদ্ধ, এবং সেই স্পিডের কতটা অংশ এটি অপারেশনে উৎসর্গ করতে পারে তার উপর নির্ভরশীল। ভিডিও ডাউনলোড হলো একটি IO-বাউন্ড অপারেশনের উদাহরণ, কারণ এটি কম্পিউটারের ইনপুট এবং আউটপুট এর গতির দ্বারা সীমাবদ্ধ; এটি কেবল তত দ্রুত চলতে পারে যত দ্রুত নেটওয়ার্কের মাধ্যমে ডেটা পাঠানো যায়।
এই উভয় উদাহরণে, অপারেটিং সিস্টেমের অদৃশ্য ইন্টারাপ্টগুলো এক ধরনের কনকারেন্সি প্রদান করে। তবে এই কনকারেন্সি কেবল পুরো প্রোগ্রামের স্তরে ঘটে: অপারেটিং সিস্টেম একটি প্রোগ্রামকে বাধাগ্রস্ত করে অন্য প্রোগ্রামগুলোকে কাজ করার সুযোগ দেয়। অনেক ক্ষেত্রে, যেহেতু আমরা আমাদের প্রোগ্রামগুলোকে অপারেটিং সিস্টেমের চেয়ে অনেক বেশি বিস্তারিত স্তরে বুঝি, তাই আমরা কনকারেন্সির এমন সুযোগগুলো চিহ্নিত করতে পারি যা অপারেটিং সিস্টেম দেখতে পায় না।
উদাহরণস্বরূপ, যদি আমরা ফাইল ডাউনলোড পরিচালনা করার জন্য একটি টুল তৈরি করি, আমাদের প্রোগ্রামটি এমনভাবে লেখা উচিত যাতে একটি ডাউনলোড শুরু করলে UI লক হয়ে না যায়, এবং ব্যবহারকারীরা একই সাথে একাধিক ডাউনলোড শুরু করতে পারেন। নেটওয়ার্কের সাথে ইন্টারঅ্যাক্ট করার জন্য অনেক অপারেটিং সিস্টেম এপিআই (API) ব্লকিং (blocking) হয়ে থাকে; অর্থাৎ, তারা যে ডেটা প্রসেস করছে তা সম্পূর্ণ প্রস্তুত না হওয়া পর্যন্ত প্রোগ্রামের অগ্রগতি আটকে রাখে।
দ্রষ্টব্য: আপনি যদি ভেবে দেখেন, তবে বেশিরভাগ ফাংশন কল এভাবেই কাজ করে। তবে, ব্লকিং শব্দটি সাধারণত সেই ফাংশন কলগুলোর জন্য সংরক্ষিত যা ফাইল, নেটওয়ার্ক বা কম্পিউটারের অন্যান্য রিসোর্সের সাথে ইন্টারঅ্যাক্ট করে, কারণ এই ক্ষেত্রগুলিতে একটি স্বতন্ত্র প্রোগ্রাম অপারেশনটি নন-ব্লকিং (non-blocking) হলে উপকৃত হবে।
আমরা প্রতিটি ফাইল ডাউনলোড করার জন্য একটি ডেডিকেটেড থ্রেড তৈরি করে আমাদের প্রধান থ্রেডকে ব্লক করা এড়াতে পারতাম। তবে, সেই থ্রেডগুলোর ওভারহেড অবশেষে একটি সমস্যা হয়ে দাঁড়াবে। বরং ভালো হতো যদি কলটি প্রথম স্থানেই ব্লক না করত। আরও ভালো হতো যদি আমরা ব্লকিং কোডের মতো একই সরাসরি স্টাইলে লিখতে পারতাম, যেমনটা নিচে দেখানো হয়েছে:
let data = fetch_data_from(url).await;
println!("{data}");
রাস্টের async (যা asynchronous এর সংক্ষিপ্ত রূপ) অ্যাবস্ট্র্যাকশন ঠিক এটাই আমাদের দেয়। এই অধ্যায়ে, আপনি async সম্পর্কে সবকিছু শিখবেন যখন আমরা নিম্নলিখিত বিষয়গুলি নিয়ে আলোচনা করব:
- রাস্টের
async
এবংawait
সিনট্যাক্স কীভাবে ব্যবহার করবেন - চ্যাপ্টার ১৬-তে আমরা যে চ্যালেঞ্জগুলো দেখেছিলাম তার কিছু সমাধান করতে async মডেল কীভাবে ব্যবহার করবেন
- কীভাবে মাল্টিথ্রেডিং এবং async পরিপূরক সমাধান প্রদান করে, যা আপনি অনেক ক্ষেত্রে একত্রিত করতে পারেন
তবে async বাস্তবে কীভাবে কাজ করে তা দেখার আগে, আমাদের প্যারালালিসম এবং কনকারেন্সির মধ্যে পার্থক্য নিয়ে আলোচনা করার জন্য একটি সংক্ষিপ্ত পথ পাড়ি দিতে হবে।
প্যারালালিসম এবং কনকারেন্সি (Parallelism and Concurrency)
এখন পর্যন্ত আমরা প্যারালালিসম এবং কনকারেন্সিকে মূলত একই জিনিস হিসেবে গণ্য করেছি। এখন আমাদের এদের মধ্যে আরও স্পষ্টভাবে পার্থক্য করতে হবে, কারণ আমরা কাজ শুরু করার সাথে সাথে এই পার্থক্যগুলো প্রকাশ পাবে।
একটি সফটওয়্যার প্রকল্পে একটি দল কীভাবে কাজ ভাগ করে নিতে পারে তার বিভিন্ন উপায় বিবেচনা করুন। আপনি একজন সদস্যকে একাধিক কাজ দিতে পারেন, প্রতিটি সদস্যকে একটি করে কাজ দিতে পারেন, অথবা দুটি পদ্ধতির মিশ্রণ ব্যবহার করতে পারেন।
যখন একজন ব্যক্তি একাধিক ভিন্ন কাজ সম্পন্ন করার আগে সেগুলোর উপর কাজ করে, তখন এটি হলো কনকারেন্সি (concurrency)। হয়তো আপনার কম্পিউটারে দুটি ভিন্ন প্রজেক্ট চেক আউট করা আছে, এবং যখন আপনি একটি প্রকল্পে বিরক্ত বা আটকে যান, তখন আপনি অন্যটিতে চলে যান। আপনি কেবল একজন ব্যক্তি, তাই আপনি একই সময়ে উভয় কাজে অগ্রগতি করতে পারবেন না, কিন্তু আপনি মাল্টি-টাস্ক করতে পারেন, একটি থেকে অন্যটিতে সুইচ করে একবারে একটিতে অগ্রগতি করতে পারেন (চিত্র ১৭-১ দেখুন)।
যখন দলটি প্রতিটি সদস্যকে একটি করে কাজ দিয়ে এবং একা একা কাজ করতে বলে কাজের একটি গ্রুপ ভাগ করে নেয়, তখন এটি হলো প্যারালালিসম (parallelism)। দলের প্রত্যেক ব্যক্তি একই সময়ে অগ্রগতি করতে পারে (চিত্র ১৭-২ দেখুন)।
এই উভয় ওয়ার্কফ্লোতে, আপনাকে বিভিন্ন কাজের মধ্যে সমন্বয় করতে হতে পারে। হয়তো আপনি ভেবেছিলেন যে একজন ব্যক্তিকে দেওয়া কাজটি অন্যদের কাজ থেকে সম্পূর্ণ স্বাধীন, কিন্তু আসলে এটি দলের অন্য একজন ব্যক্তির কাজ শেষ করার উপর নির্ভরশীল। কিছু কাজ প্যারালালি করা যেত, কিন্তু কিছু কাজ আসলে সিরিয়াল (serial) ছিল: এটি কেবল একটি সিরিজের মতো, একের পর এক টাস্ক হিসেবে হতে পারত, যেমনটি চিত্র ১৭-৩ এ দেখানো হয়েছে।
একইভাবে, আপনি হয়তো বুঝতে পারেন যে আপনার নিজের একটি কাজ আপনার অন্য একটি কাজের উপর নির্ভরশীল। এখন আপনার কনকারেন্ট কাজও সিরিয়াল হয়ে গেছে।
প্যারালালিসম এবং কনকারেন্সি একে অপরের সাথে ছেদ করতে পারে। যদি আপনি জানতে পারেন যে একজন সহকর্মী আপনার একটি কাজ শেষ না করা পর্যন্ত আটকে আছেন, তাহলে আপনি সম্ভবত আপনার সহকর্মীকে "আনব্লক" করার জন্য সেই কাজের উপর আপনার সমস্ত প্রচেষ্টা কেন্দ্রীভূত করবেন। আপনি এবং আপনার সহকর্মী আর প্যারালালি কাজ করতে পারছেন না, এবং আপনি আর আপনার নিজের কাজগুলিতে কনকারেন্টলি কাজ করতে পারছেন না।
একই মৌলিক গতিবিদ্যা সফটওয়্যার এবং হার্ডওয়্যারের ক্ষেত্রেও প্রযোজ্য। একটি একক CPU কোর সহ একটি মেশিনে, CPU একবারে কেবল একটি অপারেশন করতে পারে, কিন্তু এটি এখনও কনকারেন্টলি কাজ করতে পারে। থ্রেড, প্রসেস এবং async-এর মতো টুল ব্যবহার করে, কম্পিউটার একটি কার্যকলাপকে থামিয়ে অন্যগুলিতে সুইচ করতে পারে এবং অবশেষে সেই প্রথম কার্যকলাপে আবার ফিরে আসতে পারে। একাধিক CPU কোর সহ একটি মেশিনে, এটি প্যারালালিও কাজ করতে পারে। একটি কোর একটি কাজ করতে পারে যখন অন্য একটি কোর একটি সম্পূর্ণ সম্পর্কহীন কাজ করে, এবং সেই অপারেশনগুলি আসলে একই সময়ে ঘটে।
রাস্টে async নিয়ে কাজ করার সময়, আমরা সবসময় কনকারেন্সি নিয়ে কাজ করি। হার্ডওয়্যার, অপারেটিং সিস্টেম এবং আমরা যে async রানটাইম ব্যবহার করছি তার উপর নির্ভর করে (async রানটাইম সম্পর্কে শীঘ্রই আরও আলোচনা করা হবে), সেই কনকারেন্সি পর্দার আড়ালে প্যারালালিসমও ব্যবহার করতে পারে।
এখন, চলুন রাস্টের অ্যাসিঙ্ক্রোনাস প্রোগ্রামিং আসলে কীভাবে কাজ করে তা নিয়ে আলোচনা করা যাক।
Futures এবং Async সিনট্যাক্স
রাস্টে অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের মূল উপাদানগুলো হলো futures এবং রাস্টের async
ও await
কিওয়ার্ড।
একটি future হলো এমন একটি মান যা এখন প্রস্তুত নাও থাকতে পারে তবে ভবিষ্যতে কোনো এক সময়ে প্রস্তুত হবে। (এই একই ধারণাটি অনেক ভাষায় দেখা যায়, কখনও কখনও task বা promise এর মতো অন্য নামেও পরিচিত।) রাস্ট একটি Future
trait প্রদান করে যা একটি বিল্ডিং ব্লক হিসেবে কাজ করে, যাতে বিভিন্ন ডেটা স্ট্রাকচার দিয়ে বিভিন্ন অ্যাসিঙ্ক্রোনাস অপারেশন প্রয়োগ করা যায় কিন্তু একটি সাধারণ ইন্টারফেসের মাধ্যমে। রাস্টে, futures হলো সেইসব টাইপ যা Future
trait প্রয়োগ করে। প্রতিটি future তার নিজের অগ্রগতি এবং "প্রস্তুত" হওয়ার অর্থ কী সে সম্পর্কে তথ্য ধরে রাখে।
ব্লক এবং ফাংশনগুলোতে async
কিওয়ার্ড প্রয়োগ করে আপনি নির্দিষ্ট করতে পারেন যে সেগুলোকে বাধা দেওয়া এবং পুনরায় চালু করা যেতে পারে। একটি async ব্লক বা async ফাংশনের মধ্যে, আপনি একটি future-এর জন্য অপেক্ষা করতে (অর্থাৎ, এটি প্রস্তুত হওয়ার জন্য অপেক্ষা করতে) await
কিওয়ার্ডটি ব্যবহার করতে পারেন। একটি async ব্লক বা ফাংশনের মধ্যে যেখানেই আপনি একটি future-এর জন্য await করেন, সেটি সেই async ব্লক বা ফাংশনের জন্য থামা এবং পুনরায় চালু হওয়ার একটি সম্ভাব্য স্থান। একটি future-এর মান উপলব্ধ হয়েছে কিনা তা পরীক্ষা করার প্রক্রিয়াকে পোলিং (polling) বলা হয়।
কিছু অন্য ভাষা, যেমন C# এবং JavaScript, অ্যাসিঙ্ক্রোনাস প্রোগ্রামিংয়ের জন্য async
এবং await
কিওয়ার্ড ব্যবহার করে। আপনি যদি সেই ভাষাগুলির সাথে পরিচিত হন, তবে রাস্ট কীভাবে কাজ করে, সিনট্যাক্স কীভাবে পরিচালনা করে সহ কিছু গুরুত্বপূর্ণ পার্থক্য লক্ষ্য করতে পারেন। এর পেছনে সঙ্গত কারণ রয়েছে, যা আমরা দেখব!
async রাস্ট লেখার সময়, আমরা বেশিরভাগ সময় async
এবং await
কিওয়ার্ড ব্যবহার করি। রাস্ট সেগুলোকে Future
trait ব্যবহার করে সমতুল্য কোডে কম্পাইল করে, যেমনটি এটি for
লুপকে Iterator
trait ব্যবহার করে সমতুল্য কোডে কম্পাইল করে। তবে, যেহেতু রাস্ট Future
trait প্রদান করে, তাই প্রয়োজনে আপনি আপনার নিজের ডেটা টাইপের জন্য এটি প্রয়োগ করতে পারেন। এই অধ্যায়ে আমরা যে ফাংশনগুলো দেখব তার মধ্যে অনেকগুলিই তাদের নিজস্ব Future
এর ইমপ্লিমেন্টেশন সহ টাইপ রিটার্ন করে। আমরা অধ্যায়ের শেষে trait-টির সংজ্ঞায় ফিরে আসব এবং এটি কীভাবে কাজ করে সে সম্পর্কে আরও গভীরে যাব, কিন্তু আপাতত এগিয়ে যাওয়ার জন্য এইটুকুই যথেষ্ট।
এই সবকিছু কিছুটা বিমূর্ত মনে হতে পারে, তাই চলুন আমাদের প্রথম async প্রোগ্রামটি লিখি: একটি ছোট ওয়েব স্ক্র্যাপার। আমরা কমান্ড লাইন থেকে দুটি ইউআরএল (URL) নেব, উভয়ই কনকারেন্টলি ফেচ করব, এবং যেটি প্রথমে শেষ হবে তার ফলাফল ফেরত দেব। এই উদাহরণে বেশ কিছু নতুন সিনট্যাক্স থাকবে, কিন্তু চিন্তা করবেন না—আমরা যেতে যেতে আপনার যা যা জানা দরকার তার সবকিছু ব্যাখ্যা করব।
আমাদের প্রথম অ্যাসিঙ্ক্রোনাস প্রোগ্রাম
এই অধ্যায়ের ফোকাস async শেখার উপর রাখতে এবং ইকোসিস্টেমের বিভিন্ন অংশ নিয়ে মাথা না ঘামানোর জন্য, আমরা trpl
crate তৈরি করেছি (trpl
হলো “The Rust Programming Language” এর সংক্ষিপ্ত রূপ)। এটি আপনার প্রয়োজনীয় সমস্ত টাইপ, ট্রেইট এবং ফাংশন পুনরায় এক্সপোর্ট করে, প্রধানত futures
এবং tokio
crate থেকে। futures
crate হলো async কোডের জন্য রাস্টের পরীক্ষামূলক কাজের একটি অফিসিয়াল স্থান, এবং এখানেই মূলত Future
trait ডিজাইন করা হয়েছিল। Tokio বর্তমানে রাস্টের সবচেয়ে বহুল ব্যবহৃত async runtime, বিশেষ করে ওয়েব অ্যাপ্লিকেশনের জন্য। আরও অনেক ভালো রানটাইম রয়েছে এবং সেগুলো আপনার প্রয়োজনের জন্য আরও উপযুক্ত হতে পারে। আমরা trpl
-এর জন্য পর্দার আড়ালে tokio
crate ব্যবহার করি কারণ এটি ভালোভাবে পরীক্ষিত এবং ব্যাপকভাবে ব্যবহৃত।
কিছু ক্ষেত্রে, trpl
মূল API-গুলিকে পুনঃনামকরণ বা র্যাপ (wrap) করে যাতে আপনি এই অধ্যায়ের প্রাসঙ্গিক বিবরণগুলিতে মনোনিবেশ করতে পারেন। যদি আপনি বুঝতে চান crate-টি কী করে, আমরা আপনাকে এর সোর্স কোড দেখতে উৎসাহিত করি। আপনি দেখতে পারবেন প্রতিটি রি-এক্সপোর্ট কোন crate থেকে আসে, এবং আমরা crate-টি কী করে তা ব্যাখ্যা করার জন্য বিশদ মন্তব্য রেখেছি।
hello-async
নামে একটি নতুন বাইনারি প্রজেক্ট তৈরি করুন এবং trpl
crate-কে একটি ডিপেন্ডেন্সি হিসেবে যোগ করুন:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
এখন আমরা আমাদের প্রথম async প্রোগ্রাম লেখার জন্য trpl
দ্বারা প্রদত্ত বিভিন্ন অংশ ব্যবহার করতে পারি। আমরা একটি ছোট কমান্ড লাইন টুল তৈরি করব যা দুটি ওয়েব পেজ ফেচ করে, প্রতিটির <title>
এলিমেন্ট বের করে এবং যে পেজটি এই পুরো প্রক্রিয়াটি প্রথমে শেষ করবে তার টাইটেল প্রিন্ট করবে।
page_title ফাংশনটি সংজ্ঞায়িত করা
আসুন একটি ফাংশন লেখার মাধ্যমে শুরু করি যা একটি পেজের URL প্যারামিটার হিসেবে নেয়, সেটিতে একটি রিকোয়েস্ট করে, এবং টাইটেল এলিমেন্টের টেক্সট রিটার্ন করে (দেখুন লিস্টিং ১৭-১)।
extern crate trpl; // required for mdbook test fn main() { // TODO: we'll add this next! } use trpl::Html; async fn page_title(url: &str) -> Option<String> { let response = trpl::get(url).await; let response_text = response.text().await; Html::parse(&response_text) .select_first("title") .map(|title| title.inner_html()) }
প্রথমে, আমরা page_title
নামে একটি ফাংশন সংজ্ঞায়িত করি এবং এটিকে async
কিওয়ার্ড দিয়ে চিহ্নিত করি। তারপর আমরা পাস করা যেকোনো URL ফেচ করার জন্য trpl::get
ফাংশনটি ব্যবহার করি এবং রেসপন্সের জন্য অপেক্ষা করতে await
কিওয়ার্ড যোগ করি। রেসপন্সের টেক্সট পেতে, আমরা এর text
মেথড কল করি, এবং আবারও await
কিওয়ার্ড দিয়ে এটির জন্য অপেক্ষা করি। এই দুটি ধাপই অ্যাসিঙ্ক্রোনাস। get
ফাংশনের জন্য, আমাদের server-এর রেসপন্সের প্রথম অংশ পাঠানোর জন্য অপেক্ষা করতে হবে, যার মধ্যে HTTP হেডার, কুকি ইত্যাদি থাকবে এবং যা রেসপন্স বডি থেকে আলাদাভাবে সরবরাহ করা যেতে পারে। বিশেষ করে যদি বডি খুব বড় হয়, তবে এর সবটা আসতে কিছুটা সময় লাগতে পারে। যেহেতু আমাদের রেসপন্সের সম্পূর্ণটা আসার জন্য অপেক্ষা করতে হবে, তাই text
মেথডটিও async।
আমাদের এই দুটি future-কেই স্পষ্টভাবে await করতে হবে, কারণ রাস্টে future-গুলি lazy: আপনি await
কিওয়ার্ড দিয়ে তাদের কাজ করতে না বলা পর্যন্ত তারা কিছুই করে না। (আসলে, আপনি যদি একটি future ব্যবহার না করেন তবে রাস্ট একটি কম্পাইলার ওয়ার্নিং দেখাবে।) এটি আপনাকে অধ্যায় ১৩-এর Processing a Series of Items With Iterators বিভাগে ইটারেটর (iterator) আলোচনার কথা মনে করিয়ে দিতে পারে। ইটারেটররা তাদের next
মেথড কল না করা পর্যন্ত কিছুই করে না—সেটি সরাসরি হোক বা for
লুপ বা map
-এর মতো মেথড ব্যবহার করে যা পর্দার আড়ালে next
ব্যবহার করে। একইভাবে, future-গুলিও আপনি স্পষ্টভাবে তাদের কাজ করতে না বলা পর্যন্ত কিছুই করে না। এই অলসতা রাস্টকে async কোড প্রয়োজন না হওয়া পর্যন্ত চালানো থেকে বিরত রাখতে সাহায্য করে।
দ্রষ্টব্য: এটি Creating a New Thread with spawn-এ
thread::spawn
ব্যবহার করার সময় আমরা আগের অধ্যায়ে যে আচরণ দেখেছিলাম তার থেকে ভিন্ন, যেখানে আমরা অন্য থ্রেডে পাস করা ক্লোজারটি অবিলম্বে চলতে শুরু করেছিল। এটি অন্যান্য অনেক ভাষা যেভাবে async-এর সাথে কাজ করে তার থেকেও ভিন্ন। কিন্তু রাস্টের জন্য তার পারফরম্যান্স গ্যারান্টি প্রদান করতে পারাটা গুরুত্বপূর্ণ, ঠিক যেমনটি ইটারেটরের ক্ষেত্রে।
একবার আমাদের কাছে response_text
এসে গেলে, আমরা Html::parse
ব্যবহার করে এটিকে Html
টাইপের একটি ইন্সট্যান্সে পার্স করতে পারি। একটি কাঁচা স্ট্রিংয়ের পরিবর্তে, আমাদের কাছে এখন একটি ডেটা টাইপ রয়েছে যা আমরা HTML-এর সাথে আরও সমৃদ্ধ ডেটা স্ট্রাকচার হিসাবে কাজ করতে ব্যবহার করতে পারি। বিশেষ করে, আমরা একটি প্রদত্ত CSS সিলেক্টরের প্রথম ইন্সট্যান্স খুঁজে পেতে select_first
মেথডটি ব্যবহার করতে পারি। "title"
স্ট্রিংটি পাস করে, আমরা ডকুমেন্টের প্রথম <title>
এলিমেন্টটি পাব, যদি একটি থাকে। যেহেতু কোনো ম্যাচিং এলিমেন্ট নাও থাকতে পারে, select_first
একটি Option<ElementRef>
রিটার্ন করে। অবশেষে, আমরা Option::map
মেথড ব্যবহার করি, যা আমাদের Option
-এর আইটেমটি উপস্থিত থাকলে তার সাথে কাজ করতে দেয়, এবং যদি না থাকে তবে কিছুই না করতে দেয়। (আমরা এখানে একটি match
এক্সপ্রেশনও ব্যবহার করতে পারতাম, কিন্তু map
বেশি ইডিয়ম্যাটিক বা প্রচলিত।) map
-কে আমরা যে ফাংশনটি সরবরাহ করি তার বডিতে, আমরা title
-এর উপর inner_html
কল করে এর কনটেন্ট পাই, যা একটি String
। সবশেষে, আমাদের কাছে একটি Option<String>
থাকে।
লক্ষ্য করুন যে রাস্টের await
কিওয়ার্ডটি আপনি যে এক্সপ্রেশনের জন্য অপেক্ষা করছেন তার পরে বসে, আগে নয়। অর্থাৎ, এটি একটি পোস্টফিক্স (postfix) কিওয়ার্ড। আপনি যদি অন্য ভাষায় async
ব্যবহার করে থাকেন তবে এটি আপনার অভ্যস্ততার থেকে ভিন্ন হতে পারে, তবে রাস্টে এটি মেথডের চেইনগুলির সাথে কাজ করা অনেক সুন্দর করে তোলে। ফলস্বরূপ, আমরা page_title
-এর বডি পরিবর্তন করে trpl::get
এবং text
ফাংশন কলগুলিকে তাদের মধ্যে await
দিয়ে একসাথে চেইন করতে পারি, যেমনটি লিস্টিং ১৭-২ এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use trpl::Html; fn main() { // TODO: we'll add this next! } async fn page_title(url: &str) -> Option<String> { let response_text = trpl::get(url).await.text().await; Html::parse(&response_text) .select_first("title") .map(|title| title.inner_html()) }
এর সাথে, আমরা সফলভাবে আমাদের প্রথম async ফাংশন লিখে ফেলেছি! এটিকে কল করার জন্য main
-এ কিছু কোড যোগ করার আগে, চলুন আমরা যা লিখেছি এবং এর অর্থ কী তা নিয়ে আরও একটু কথা বলি।
যখন রাস্ট async
কিওয়ার্ড দিয়ে চিহ্নিত একটি ব্লক দেখে, তখন এটি এটিকে একটি অনন্য, নামহীন ডেটা টাইপে কম্পাইল করে যা Future
trait প্রয়োগ করে। যখন রাস্ট async
দিয়ে চিহ্নিত একটি ফাংশন দেখে, তখন এটি এটিকে একটি নন-async ফাংশনে কম্পাইল করে যার বডি একটি async ব্লক। একটি async ফাংশনের রিটার্ন টাইপ হলো কম্পাইলার সেই async ব্লকের জন্য যে নামহীন ডেটা টাইপ তৈরি করে তার টাইপ।
সুতরাং, async fn
লেখা একটি ফাংশন লেখার সমতুল্য যা রিটার্ন টাইপের একটি future রিটার্ন করে। কম্পাইলারের কাছে, লিস্টিং ১৭-১-এর async fn page_title
-এর মতো একটি ফাংশন সংজ্ঞা একটি নন-async ফাংশনের সমতুল্য যা এইভাবে সংজ্ঞায়িত:
#![allow(unused)] fn main() { extern crate trpl; // required for mdbook test use std::future::Future; use trpl::Html; fn page_title(url: &str) -> impl Future<Output = Option<String>> { async move { let text = trpl::get(url).await.text().await; Html::parse(&text) .select_first("title") .map(|title| title.inner_html()) } } }
আসুন রূপান্তরিত সংস্করণের প্রতিটি অংশ পর্যালোচনা করি:
- এটি
impl Trait
সিনট্যাক্স ব্যবহার করে যা আমরা অধ্যায় ১০-এর "Traits as Parameters" বিভাগে আলোচনা করেছি। - রিটার্ন করা trait টি হলো একটি
Future
যার একটি অ্যাসোসিয়েটেড টাইপOutput
রয়েছে। লক্ষ্য করুন যেOutput
টাইপটি হলোOption<String>
, যাpage_title
-এরasync fn
সংস্করণ থেকে মূল রিটার্ন টাইপের সমান। - মূল ফাংশনের বডিতে কল করা সমস্ত কোড একটি
async move
ব্লকে মোড়ানো হয়েছে। মনে রাখবেন যে ব্লকগুলি এক্সপ্রেশন। এই পুরো ব্লকটিই ফাংশন থেকে রিটার্ন করা এক্সপ্রেশন। - এই async ব্লকটি
Option<String>
টাইপের একটি মান তৈরি করে, যেমনটি এইমাত্র বর্ণনা করা হয়েছে। সেই মানটি রিটার্ন টাইপেরOutput
টাইপের সাথে মেলে। এটি আপনার দেখা অন্যান্য ব্লকের মতোই। - নতুন ফাংশন বডিটি একটি
async move
ব্লক কারণ এটিurl
প্যারামিটারটি যেভাবে ব্যবহার করে তার জন্য। (আমরা অধ্যায়ের পরবর্তীতেasync
বনামasync move
সম্পর্কে আরও অনেক কিছু আলোচনা করব।)
এখন আমরা main
-এ page_title
কল করতে পারি।
একটিমাত্র পেজের টাইটেল নির্ধারণ করা
শুরু করার জন্য, আমরা কেবল একটি পেজের টাইটেল আনব। লিস্টিং ১৭-৩ এ, আমরা Accepting Command Line Arguments বিভাগে কমান্ড লাইন আর্গুমেন্ট পাওয়ার জন্য অধ্যায় ১২-তে ব্যবহৃত একই প্যাটার্ন অনুসরণ করি। তারপর আমরা প্রথম URL টি page_title
-কে পাস করি এবং ফলাফলের জন্য await করি। যেহেতু future দ্বারা উৎপাদিত মানটি একটি Option<String>
, তাই পেজটিতে <title>
ছিল কিনা তা বিবেচনা করে বিভিন্ন বার্তা প্রিন্ট করার জন্য আমরা একটি match
এক্সপ্রেশন ব্যবহার করি।
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
দুর্ভাগ্যবশত, এই কোডটি কম্পাইল হয় না। একমাত্র যে জায়গায় আমরা await
কিওয়ার্ড ব্যবহার করতে পারি তা হলো async ফাংশন বা ব্লকে, এবং রাস্ট আমাদের বিশেষ main
ফাংশনকে async
হিসাবে চিহ্নিত করতে দেবে না।
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
main
-কে async
হিসাবে চিহ্নিত করা যায় না কারণ async কোডের একটি রানটাইম (runtime) প্রয়োজন: একটি রাস্ট crate যা অ্যাসিঙ্ক্রোনাস কোড চালানোর বিবরণ পরিচালনা করে। একটি প্রোগ্রামের main
ফাংশন একটি রানটাইম ইনিশিয়ালাইজ করতে পারে, কিন্তু এটি নিজে একটি রানটাইম নয়। (আমরা কিছুক্ষণ পরে দেখব কেন এটি এমন)। প্রতিটি রাস্ট প্রোগ্রাম যা async কোড চালায় তার অন্তত একটি জায়গা থাকে যেখানে এটি একটি রানটাইম সেট আপ করে এবং future-গুলি চালায়।
বেশিরভাগ ভাষা যা async সমর্থন করে তারা একটি রানটাইম বান্ডিল করে, কিন্তু রাস্ট তা করে না। পরিবর্তে, অনেক বিভিন্ন async রানটাইম উপলব্ধ রয়েছে, যার প্রত্যেকটি তাদের লক্ষ্য করা ব্যবহারের ক্ষেত্রে উপযুক্ত বিভিন্ন ট্রেড-অফ করে। উদাহরণস্বরূপ, অনেক সিপিইউ কোর এবং প্রচুর পরিমাণে র্যাম সহ একটি উচ্চ-থ্রুপুট ওয়েব সার্ভারের চাহিদা একটি একক কোর, অল্প পরিমাণে র্যাম এবং কোনো হিপ অ্যালোকেশন ক্ষমতা ছাড়াই একটি মাইক্রোকন্ট্রোলারের থেকে খুব আলাদা। যে crate-গুলি সেই রানটাইমগুলি সরবরাহ করে সেগুলি প্রায়শই ফাইল বা নেটওয়ার্ক I/O-এর মতো সাধারণ কার্যকারিতার async সংস্করণ সরবরাহ করে।
এখানে, এবং এই অধ্যায়ের বাকি অংশে, আমরা trpl
crate থেকে run
ফাংশনটি ব্যবহার করব, যা একটি আর্গুমেন্ট হিসাবে একটি future নেয় এবং এটিকে সম্পূর্ণ না হওয়া পর্যন্ত চালায়। পর্দার আড়ালে, run
কল করা একটি রানটাইম সেট আপ করে যা পাস করা future টি চালানোর জন্য ব্যবহৃত হয়। একবার future টি সম্পূর্ণ হলে, run
future টি যে মানটি তৈরি করেছে তা রিটার্ন করে।
আমরা page_title
দ্বারা রিটার্ন করা future টি সরাসরি run
-কে পাস করতে পারতাম, এবং এটি সম্পূর্ণ হলে, আমরা ফলাফলের Option<String>
-এর উপর ম্যাচ করতে পারতাম, যেমনটি আমরা লিস্টিং ১৭-৩-এ করার চেষ্টা করেছি। যাইহোক, অধ্যায়ের বেশিরভাগ উদাহরণের জন্য (এবং বাস্তব বিশ্বের বেশিরভাগ async কোডের জন্য), আমরা কেবল একটি async ফাংশন কলের চেয়ে বেশি কিছু করব, তাই পরিবর্তে আমরা একটি async
ব্লক পাস করব এবং page_title
কলের ফলাফলটি স্পষ্টভাবে await করব, যেমনটি লিস্টিং ১৭-৪-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
যখন আমরা এই কোডটি চালাই, তখন আমরা প্রাথমিকভাবে যে আচরণটি আশা করেছিলাম তা পাই:
$ cargo run -- https://www.rust-lang.org
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
যাক বাবা—আমরা অবশেষে কিছু কার্যকরী async কোড পেয়েছি! কিন্তু দুটি সাইটকে একে অপরের বিরুদ্ধে রেস করানোর কোড যোগ করার আগে, আসুন future-গুলি কীভাবে কাজ করে সেদিকে সংক্ষেপে আমাদের মনোযোগ ফিরিয়ে আনি।
প্রতিটি await পয়েন্ট—অর্থাৎ, প্রতিটি জায়গা যেখানে কোড await
কিওয়ার্ড ব্যবহার করে—একটি এমন স্থানকে প্রতিনিধিত্ব করে যেখানে নিয়ন্ত্রণ রানটাইমের কাছে ফিরিয়ে দেওয়া হয়। এটি কাজ করানোর জন্য, রাস্টকে async ব্লকের সাথে জড়িত অবস্থার ট্র্যাক রাখতে হবে যাতে রানটাইম অন্য কোনো কাজ শুরু করতে পারে এবং তারপর যখন এটি প্রথমটিকে আবার এগিয়ে নিয়ে যাওয়ার জন্য প্রস্তুত হয় তখন ফিরে আসতে পারে। এটি একটি অদৃশ্য স্টেট মেশিন, যেন আপনি প্রতিটি await পয়েন্টে বর্তমান অবস্থা সংরক্ষণ করার জন্য এইরকম একটি enum লিখেছেন:
#![allow(unused)] fn main() { extern crate trpl; // required for mdbook test enum PageTitleFuture<'a> { Initial { url: &'a str }, GetAwaitPoint { url: &'a str }, TextAwaitPoint { response: trpl::Response }, } }
প্রতিটি অবস্থার মধ্যে রূপান্তরের জন্য হাতে করে কোড লেখা ক্লান্তিকর এবং ত্রুটিপূর্ণ হবে, বিশেষ করে যখন আপনাকে পরে কোডে আরও কার্যকারিতা এবং আরও অবস্থা যোগ করতে হবে। ভাগ্যক্রমে, রাস্ট কম্পাইলার স্বয়ংক্রিয়ভাবে async কোডের জন্য স্টেট মেশিন ডেটা স্ট্রাকচার তৈরি এবং পরিচালনা করে। ডেটা স্ট্রাকচারের আশেপাশে স্বাভাবিক borrowing এবং ownership নিয়মগুলি সবই এখনও প্রযোজ্য, এবং আনন্দের বিষয়, কম্পাইলার সেগুলি আমাদের জন্য পরীক্ষা করে এবং দরকারী ত্রুটি বার্তা প্রদান করে। আমরা অধ্যায়ের পরে সেগুলির কয়েকটি নিয়ে কাজ করব।
শেষ পর্যন্ত, কিছু একটাকে এই স্টেট মেশিনটি চালাতে হবে, এবং সেই কিছু একটা হলো একটি রানটাইম। (এই কারণেই আপনি রানটাইম নিয়ে খোঁজ করার সময় এক্সিকিউটর (executors) এর উল্লেখ পেতে পারেন: একটি এক্সিকিউটর হলো একটি রানটাইমের অংশ যা async কোড চালানোর জন্য দায়ী।)
এখন আপনি দেখতে পাচ্ছেন কেন কম্পাইলার আমাদের লিস্টিং ১৭-৩-এ main
-কে নিজে একটি async ফাংশন তৈরি করতে বাধা দিয়েছিল। যদি main
একটি async ফাংশন হতো, তবে main
যে future টি রিটার্ন করত তার স্টেট মেশিন পরিচালনা করার জন্য অন্য কিছুর প্রয়োজন হতো, কিন্তু main
হলো প্রোগ্রামের সূচনা বিন্দু! পরিবর্তে, আমরা main
-এ trpl::run
ফাংশনটি কল করেছি একটি রানটাইম সেট আপ করতে এবং async
ব্লকের দ্বারা রিটার্ন করা future টি শেষ না হওয়া পর্যন্ত চালাতে।
দ্রষ্টব্য: কিছু রানটাইম ম্যাক্রো সরবরাহ করে যাতে আপনি একটি async
main
ফাংশন লিখতে পারেন। সেই ম্যাক্রোগুলিasync fn main() { ... }
-কে একটি সাধারণfn main
-এ পুনর্লিখন করে, যা আমরা লিস্টিং ১৭-৪-এ হাতে করে যা করেছি তাই করে: একটি ফাংশন কল করে যা একটি future-কেtrpl::run
-এর মতো সম্পূর্ণ না হওয়া পর্যন্ত চালায়।
এখন আসুন এই অংশগুলি একসাথে রাখি এবং দেখি আমরা কীভাবে কনকারেন্ট কোড লিখতে পারি।
আমাদের দুটি URL-কে একে অপরের বিরুদ্ধে রেস করানো
লিস্টিং ১৭-৫-এ, আমরা কমান্ড লাইন থেকে পাস করা দুটি ভিন্ন URL দিয়ে page_title
কল করি এবং তাদের রেস করাই।
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::run(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::race(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
আমরা ব্যবহারকারী-প্রদত্ত প্রতিটি URL-এর জন্য page_title
কল করে শুরু করি। আমরা ফলাফলস্বরূপ future-গুলিকে title_fut_1
এবং title_fut_2
হিসাবে সংরক্ষণ করি। মনে রাখবেন, এগুলি এখনও কিছুই করে না, কারণ future-গুলি অলস এবং আমরা এখনও তাদের await করিনি। তারপর আমরা future-গুলিকে trpl::race
-এ পাস করি, যা একটি মান রিটার্ন করে নির্দেশ করে যে পাস করা future-গুলির মধ্যে কোনটি প্রথমে শেষ হয়।
দ্রষ্টব্য: পর্দার আড়ালে,
race
একটি আরও সাধারণ ফাংশন,select
-এর উপর নির্মিত, যা আপনি বাস্তব-বিশ্বের রাস্ট কোডে আরও প্রায়ই দেখতে পাবেন। একটিselect
ফাংশন এমন অনেক কিছু করতে পারে যাtrpl::race
ফাংশনটি করতে পারে না, তবে এর কিছু অতিরিক্ত জটিলতাও রয়েছে যা আমরা আপাতত এড়িয়ে যেতে পারি।
যেকোনো future আইনসম্মতভাবে "জিততে" পারে, তাই একটি Result
রিটার্ন করার কোনো মানে হয় না। পরিবর্তে, race
এমন একটি টাইপ রিটার্ন করে যা আমরা আগে দেখিনি, trpl::Either
। Either
টাইপটি একটি Result
-এর সাথে কিছুটা সাদৃশ্যপূর্ণ কারণ এর দুটি কেস রয়েছে। Result
-এর মতো নয়, Either
-এ সাফল্য বা ব্যর্থতার কোনো ধারণা নেই। পরিবর্তে, এটি "একটি বা অন্যটি" নির্দেশ করতে Left
এবং Right
ব্যবহার করে:
#![allow(unused)] fn main() { enum Either<A, B> { Left(A), Right(B), } }
race
ফাংশনটি Left
রিটার্ন করে প্রথম ফিউচার আর্গুমেন্টের আউটপুট সহ যদি সেটি প্রথমে শেষ হয়, অথবা Right
রিটার্ন করে দ্বিতীয় ফিউচার আর্গুমেন্টের আউটপুট সহ যদি সেটি প্রথমে শেষ হয়। এটি ফাংশন কল করার সময় আর্গুমেন্টগুলির ক্রমের সাথে মিলে যায়: প্রথম আর্গুমেন্টটি দ্বিতীয় আর্গুমেন্টের বাম দিকে থাকে।
আমরা page_title
-কে এমনভাবে আপডেট করি যাতে এটি পাস করা একই URL রিটার্ন করে। এইভাবে, যদি প্রথমে রিটার্ন করা পেজটির কোনো <title>
না থাকে যা আমরা সমাধান করতে পারি, আমরা তবুও একটি অর্থপূর্ণ বার্তা প্রিন্ট করতে পারি। সেই তথ্য উপলব্ধ থাকায়, আমরা আমাদের println!
আউটপুট আপডেট করে শেষ করি যাতে কোন URL প্রথমে শেষ হয়েছে এবং সেই URL-এর ওয়েব পেজের <title>
কী, যদি থাকে, তা উভয়ই নির্দেশ করা যায়।
আপনি এখন একটি ছোট কার্যকরী ওয়েব স্ক্র্যাপার তৈরি করেছেন! কয়েকটি URL বেছে নিন এবং কমান্ড লাইন টুলটি চালান। আপনি আবিষ্কার করতে পারেন যে কিছু সাইট ধারাবাহিকভাবে অন্যদের চেয়ে দ্রুত, আবার অন্য ক্ষেত্রে দ্রুততর সাইটটি রান থেকে রানে পরিবর্তিত হয়। আরও গুরুত্বপূর্ণভাবে, আপনি future-এর সাথে কাজ করার মূল বিষয়গুলি শিখেছেন, তাই এখন আমরা async দিয়ে কী করতে পারি সে সম্পর্কে আরও গভীরে যেতে পারি।
Async দিয়ে কনকারেন্সি প্রয়োগ
এই বিভাগে, আমরা চ্যাপ্টার ১৬-তে থ্রেড (thread) দিয়ে সমাধান করা কিছু কনকারেন্সি চ্যালেঞ্জের জন্য async প্রয়োগ করব। যেহেতু আমরা সেখানে অনেক মূল ধারণা নিয়ে ইতিমধ্যে আলোচনা করেছি, তাই এই বিভাগে আমরা থ্রেড এবং future-এর মধ্যে কী কী পার্থক্য রয়েছে তার উপর মনোযোগ দেব।
অনেক ক্ষেত্রে, async ব্যবহার করে কনকারেন্সির সাথে কাজ করার জন্য API-গুলো থ্রেড ব্যবহারের জন্য থাকা API-গুলোর মতোই। অন্য ক্ষেত্রে, সেগুলো বেশ ভিন্ন হয়ে যায়। এমনকি যখন API-গুলো থ্রেড এবং async-এর মধ্যে দেখতে একই রকম মনে হয়, তখনও তাদের আচরণ প্রায়শই ভিন্ন হয়—এবং তাদের পারফরম্যান্স বৈশিষ্ট্যগুলি প্রায় সবসময়ই ভিন্ন থাকে।
spawn_task
দিয়ে নতুন টাস্ক তৈরি করা
Creating a New Thread with Spawn-এ আমরা প্রথম যে অপারেশনটি নিয়ে কাজ করেছিলাম তা হলো দুটি পৃথক থ্রেডে গণনা করা। আসুন async ব্যবহার করে একই কাজ করি। trpl
crate একটি spawn_task
ফাংশন সরবরাহ করে যা দেখতে thread::spawn
API-এর মতোই, এবং একটি sleep
ফাংশন যা thread::sleep
API-এর একটি async সংস্করণ। আমরা এই দুটিকে একসাথে ব্যবহার করে গণনার উদাহরণটি বাস্তবায়ন করতে পারি, যেমনটি লিস্টিং ১৭-৬-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { trpl::spawn_task(async { for i in 1..10 { println!("hi number {i} from the first task!"); trpl::sleep(Duration::from_millis(500)).await; } }); for i in 1..5 { println!("hi number {i} from the second task!"); trpl::sleep(Duration::from_millis(500)).await; } }); }
আমাদের সূচনা বিন্দু হিসাবে, আমরা আমাদের main
ফাংশনটি trpl::run
দিয়ে সেট আপ করি যাতে আমাদের টপ-লেভেল ফাংশনটি async হতে পারে।
দ্রষ্টব্য: এই অধ্যায়ের এই পয়েন্ট থেকে, প্রতিটি উদাহরণে
main
-এtrpl::run
সহ এই একই র্যাপিং কোড অন্তর্ভুক্ত থাকবে, তাই আমরা প্রায়শই এটি এড়িয়ে যাব যেমনটি আমরাmain
-এর ক্ষেত্রে করি। আপনার কোডে এটি অন্তর্ভুক্ত করতে ভুলবেন না!
তারপর আমরা সেই ব্লকের মধ্যে দুটি লুপ লিখি, প্রতিটিতে একটি trpl::sleep
কল রয়েছে, যা পরবর্তী বার্তা পাঠানোর আগে আধা সেকেন্ড (৫০০ মিলিসেকেন্ড) অপেক্ষা করে। আমরা একটি লুপ trpl::spawn_task
-এর বডিতে এবং অন্যটি একটি টপ-লেভেল for
লুপে রাখি। আমরা sleep
কলগুলোর পরে একটি await
যোগ করি।
এই কোডটি থ্রেড-ভিত্তিক ইমপ্লিমেন্টেশনের মতোই আচরণ করে—যার মধ্যে এই ঘটনাটিও অন্তর্ভুক্ত যে আপনি যখন এটি চালাবেন তখন আপনার নিজের টার্মিনালে বার্তাগুলি একটি ভিন্ন ক্রমে উপস্থিত হতে পারে:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!```
এই সংস্করণটি প্রধান async ব্লকের বডিতে `for` লুপ শেষ হওয়ার সাথে সাথেই থেমে যায়, কারণ `spawn_task` দ্বারা তৈরি করা টাস্কটি `main` ফাংশন শেষ হলে বন্ধ হয়ে যায়। আপনি যদি এটিকে টাস্কের সমাপ্তি পর্যন্ত চালাতে চান, তবে প্রথম টাস্কটি সম্পূর্ণ হওয়ার জন্য অপেক্ষা করতে আপনাকে একটি জয়েন হ্যান্ডেল ব্যবহার করতে হবে। থ্রেডের সাথে, আমরা থ্রেডটি চলা শেষ না হওয়া পর্যন্ত "ব্লক" করার জন্য `join` মেথড ব্যবহার করেছি। লিস্টিং ১৭-৭-এ, আমরা একই কাজ করার জন্য `await` ব্যবহার করতে পারি, কারণ টাস্ক হ্যান্ডেল নিজেই একটি future। এর `Output` টাইপ একটি `Result`, তাই আমরা এটিকে await করার পরে `unwrap` করি।
<Listing number="17-7" caption="একটি টাস্ককে সমাপ্তি পর্যন্ত চালানোর জন্য একটি জয়েন হ্যান্ডেলের সাথে `await` ব্যবহার করা" file-name="src/main.rs">
```rust
# extern crate trpl; // required for mdbook test
#
# use std::time::Duration;
#
# fn main() {
# trpl::run(async {
let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
# });
# }
এই আপডেট করা সংস্করণটি উভয় লুপ শেষ না হওয়া পর্যন্ত চলে।
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
এখন পর্যন্ত, মনে হচ্ছে async এবং থ্রেড আমাদের একই মৌলিক ফলাফল দেয়, শুধু ভিন্ন সিনট্যাক্স দিয়ে: জয়েন হ্যান্ডেলে join
কল করার পরিবর্তে await
ব্যবহার করা, এবং sleep
কলগুলিকে await করা।
বড় পার্থক্য হলো এই কাজটি করার জন্য আমাদের অন্য একটি অপারেটিং সিস্টেম থ্রেড তৈরি করার প্রয়োজন হয়নি। আসলে, আমাদের এখানে একটি টাস্কও তৈরি করার প্রয়োজন নেই। যেহেতু async ব্লকগুলি নামহীন future-এ কম্পাইল হয়, আমরা প্রতিটি লুপকে একটি async ব্লকে রাখতে পারি এবং রানটাইমকে trpl::join
ফাংশন ব্যবহার করে সেগুলিকে সম্পূর্ণ করতে দিতে পারি।
Waiting for All Threads to Finishing Using join
Handles বিভাগে, আমরা দেখিয়েছিলাম কীভাবে std::thread::spawn
কল করার সময় রিটার্ন করা JoinHandle
টাইপের উপর join
মেথড ব্যবহার করতে হয়। trpl::join
ফাংশনটি একই রকম, কিন্তু future-এর জন্য। যখন আপনি এটিকে দুটি future দেন, তখন এটি একটি নতুন future তৈরি করে যার আউটপুট হলো একটি টাপল (tuple) যা আপনার পাস করা প্রতিটি future-এর আউটপুট ধারণ করে যখন তারা উভয়ই সম্পূর্ণ হয়। সুতরাং, লিস্টিং ১৭-৮-এ, আমরা fut1
এবং fut2
উভয়ই শেষ হওয়ার জন্য অপেক্ষা করতে trpl::join
ব্যবহার করি। আমরা fut1
এবং fut2
-কে await করি না, বরং trpl::join
দ্বারা উৎপাদিত নতুন future-কে await করি। আমরা আউটপুট উপেক্ষা করি, কারণ এটি কেবল দুটি ইউনিট মান ধারণকারী একটি টাপল।
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let fut1 = async { for i in 1..10 { println!("hi number {i} from the first task!"); trpl::sleep(Duration::from_millis(500)).await; } }; let fut2 = async { for i in 1..5 { println!("hi number {i} from the second task!"); trpl::sleep(Duration::from_millis(500)).await; } }; trpl::join(fut1, fut2).await; }); }
যখন আমরা এটি চালাই, আমরা দেখি উভয় future-ই সম্পূর্ণভাবে চলে:
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
এখন, আপনি প্রতিবার ঠিক একই ক্রম দেখতে পাবেন, যা আমরা থ্রেডের সাথে যা দেখেছিলাম তার থেকে খুব ভিন্ন। এর কারণ হলো trpl::join
ফাংশনটি ফেয়ার (fair), যার অর্থ এটি প্রতিটি future-কে সমানভাবে পরীক্ষা করে, তাদের মধ্যে পর্যায়ক্রমে পরিবর্তন করে, এবং অন্যটি প্রস্তুত থাকলে একটিকে এগিয়ে যেতে দেয় না। থ্রেডের সাথে, অপারেটিং সিস্টেম সিদ্ধান্ত নেয় কোন থ্রেডটি পরীক্ষা করতে হবে এবং এটিকে কতক্ষণ চালাতে দিতে হবে। async রাস্টের সাথে, রানটাইম সিদ্ধান্ত নেয় কোন টাস্কটি পরীক্ষা করতে হবে। (বাস্তবে, বিশদ বিবরণগুলি জটিল হয়ে যায় কারণ একটি async রানটাইম কনকারেন্সি পরিচালনার অংশ হিসাবে পর্দার আড়ালে অপারেটিং সিস্টেম থ্রেড ব্যবহার করতে পারে, তাই ফেয়ারনেস নিশ্চিত করা একটি রানটাইমের জন্য আরও বেশি কাজ হতে পারে—তবে এটি এখনও সম্ভব!) রানটাইমগুলিকে যেকোনো প্রদত্ত অপারেশনের জন্য ফেয়ারনেসের গ্যারান্টি দিতে হয় না, এবং তারা প্রায়শই আপনাকে ফেয়ারনেস চান কি না তা বেছে নিতে বিভিন্ন API অফার করে।
ফিউচার await করার এই ভিন্নতাগুলো চেষ্টা করে দেখুন এবং দেখুন তারা কী করে:
- যেকোনো একটি বা উভয় লুপের চারপাশ থেকে async ব্লকটি সরিয়ে ফেলুন।
- প্রতিটি async ব্লক সংজ্ঞায়িত করার সাথে সাথেই await করুন।
- কেবলমাত্র প্রথম লুপটিকে একটি async ব্লকে র্যাপ করুন, এবং দ্বিতীয় লুপের বডির পরে ফলাফলস্বরূপ future-টিকে await করুন।
একটি অতিরিক্ত চ্যালেঞ্জের জন্য, দেখুন আপনি প্রতিটি ক্ষেত্রে কোড চালানোর আগে আউটপুট কী হবে তা বের করতে পারেন কিনা!
মেসেজ পাসিং ব্যবহার করে দুটি টাস্কে গণনা করা
Future-এর মধ্যে ডেটা শেয়ার করাও পরিচিত মনে হবে: আমরা আবার মেসেজ পাসিং ব্যবহার করব, কিন্তু এবার async সংস্করণ টাইপ এবং ফাংশনের সাথে। আমরা Using Message Passing to Transfer Data Between Threads-এ যে পথ নিয়েছিলাম তার থেকে কিছুটা ভিন্ন পথ নেব যাতে থ্রেড-ভিত্তিক এবং future-ভিত্তিক কনকারেন্সির মধ্যে কিছু মূল পার্থক্য তুলে ধরা যায়। লিস্টিং ১৭-৯-এ, আমরা কেবল একটি async ব্লক দিয়ে শুরু করব—একটি পৃথক থ্রেড তৈরি করার মতো করে একটি পৃথক টাস্ক তৈরি না করে।
extern crate trpl; // required for mdbook test fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let val = String::from("hi"); tx.send(val).unwrap(); let received = rx.recv().await.unwrap(); println!("received '{received}'"); }); }
এখানে, আমরা trpl::channel
ব্যবহার করি, যা চ্যাপ্টার ১৬-তে আমরা থ্রেডের সাথে ব্যবহার করা মাল্টিপল-প্রডিউসার, সিঙ্গেল-কনজিউমার চ্যানেল API-এর একটি async সংস্করণ। API-এর async সংস্করণটি থ্রেড-ভিত্তিক সংস্করণ থেকে সামান্য ভিন্ন: এটি একটি অপরিবর্তনীয় রিসিভার rx
-এর পরিবর্তে একটি পরিবর্তনযোগ্য রিসিভার ব্যবহার করে, এবং এর recv
মেথড সরাসরি মান তৈরি করার পরিবর্তে একটি future তৈরি করে যা আমাদের await করতে হবে। এখন আমরা সেন্ডার থেকে রিসিভারে মেসেজ পাঠাতে পারি। লক্ষ্য করুন যে আমাদের একটি পৃথক থ্রেড বা এমনকি একটি টাস্কও তৈরি করতে হবে না; আমাদের কেবল rx.recv
কলটি await করতে হবে।
std::mpsc::channel
-এর সিঙ্ক্রোনাস Receiver::recv
মেথডটি একটি মেসেজ না পাওয়া পর্যন্ত ব্লক করে। trpl::Receiver::recv
মেথডটি তা করে না, কারণ এটি async। ব্লক করার পরিবর্তে, এটি রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দেয় যতক্ষণ না একটি মেসেজ পাওয়া যায় বা চ্যানেলের সেন্ড সাইড বন্ধ হয়ে যায়। এর বিপরীতে, আমরা send
কলটি await করি না, কারণ এটি ব্লক করে না। এটির প্রয়োজন নেই, কারণ আমরা যে চ্যানেলে পাঠাচ্ছি তা আনবাউন্ডেড (unbounded)।
দ্রষ্টব্য: যেহেতু এই সমস্ত async কোড একটি
trpl::run
কলের মধ্যে একটি async ব্লকে চলে, তাই এর মধ্যে সবকিছুই ব্লকিং এড়াতে পারে। যাইহোক, এর বাইরের কোডটিrun
ফাংশন রিটার্ন করার উপর ব্লক করবে। এটাইtrpl::run
ফাংশনের মূল উদ্দেশ্য: এটি আপনাকে বেছে নিতে দেয় কোথায় কিছু async কোডের সেটের উপর ব্লক করতে হবে, এবং এইভাবে সিঙ্ক এবং async কোডের মধ্যে কোথায় রূপান্তর করতে হবে। বেশিরভাগ async রানটাইমে,run
-এর নাম আসলে ঠিক এই কারণেইblock_on
।
এই উদাহরণ সম্পর্কে দুটি জিনিস লক্ষ্য করুন। প্রথমত, বার্তাটি সাথে সাথেই পৌঁছে যাবে। দ্বিতীয়ত, যদিও আমরা এখানে একটি future ব্যবহার করি, এখনও কোনো কনকারেন্সি নেই। লিস্টিংয়ের সবকিছু ক্রমানুসারে ঘটে, ঠিক যেমনটি হতো যদি কোনো future জড়িত না থাকত।
আসুন প্রথম অংশটি মোকাবেলা করি একটি সিরিজের বার্তা পাঠিয়ে এবং তাদের মধ্যে sleep
করে, যেমনটি লিস্টিং ১৭-১০-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
}
বার্তা পাঠানোর পাশাপাশি, আমাদের সেগুলি গ্রহণ করতে হবে। এই ক্ষেত্রে, যেহেতু আমরা জানি কতগুলি বার্তা আসছে, আমরা চারবার rx.recv().await
কল করে এটি ম্যানুয়ালি করতে পারতাম। তবে বাস্তব জগতে, আমরা সাধারণত কিছু অজানা সংখ্যক বার্তার জন্য অপেক্ষা করব, তাই আমাদের আর কোনো বার্তা নেই তা নির্ধারণ না করা পর্যন্ত অপেক্ষা করতে হবে।
লিস্টিং ১৬-১০-এ, আমরা একটি সিঙ্ক্রোনাস চ্যানেল থেকে প্রাপ্ত সমস্ত আইটেম প্রসেস করার জন্য একটি for
লুপ ব্যবহার করেছি। তবে রাস্টের কাছে এখনও আইটেমের একটি অ্যাসিঙ্ক্রোনাস সিরিজের উপর for
লুপ লেখার কোনো উপায় নেই, তাই আমাদের এমন একটি লুপ ব্যবহার করতে হবে যা আমরা আগে দেখিনি: while let
কন্ডিশনাল লুপ। এটি হলো if let
কনস্ট্রাক্টের লুপ সংস্করণ যা আমরা Concise Control Flow with if let
and let else
বিভাগে দেখেছি। লুপটি ততক্ষণ চলতে থাকবে যতক্ষণ এর নির্দিষ্ট করা প্যাটার্নটি মানের সাথে মিলতে থাকবে।
rx.recv
কল একটি future তৈরি করে, যা আমরা await করি। রানটাইম future-টিকে প্রস্তুত না হওয়া পর্যন্ত পজ (pause) করবে। একবার একটি বার্তা এসে গেলে, future-টি যতবার একটি বার্তা আসবে ততবার Some(message)
-এ রিজলভ হবে। যখন চ্যানেলটি বন্ধ হয়ে যাবে, কোনো বার্তা এসেছে কি না নির্বিশেষে, future-টি পরিবর্তে None
রিজলভ করবে এটি নির্দেশ করার জন্য যে আর কোনো মান নেই এবং তাই আমাদের পোলিং বন্ধ করা উচিত—অর্থাৎ, await করা বন্ধ করা উচিত।
while let
লুপ এই সবকিছুকে একত্রিত করে। যদি rx.recv().await
কল করার ফলাফল Some(message)
হয়, আমরা বার্তাটিতে অ্যাক্সেস পাই এবং আমরা এটি লুপের বডিতে ব্যবহার করতে পারি, যেমনটি আমরা if let
-এর সাথে করতে পারতাম। যদি ফলাফলটি None
হয়, লুপটি শেষ হয়ে যায়। প্রতিবার লুপটি সম্পূর্ণ হলে, এটি আবার await পয়েন্টে আঘাত করে, তাই রানটাইম এটিকে আবার পজ করে যতক্ষণ না অন্য একটি বার্তা আসে।
কোডটি এখন সফলভাবে সমস্ত বার্তা পাঠায় এবং গ্রহণ করে। দুর্ভাগ্যবশত, এখনও কয়েকটি সমস্যা রয়েছে। একটি হলো, বার্তাগুলি আধা-সেকেন্ডের ব্যবধানে আসে না। প্রোগ্রাম শুরু করার ২ সেকেন্ড (২,০০০ মিলিসেকেন্ড) পরে সেগুলি একবারে আসে। আরেকটি হলো, এই প্রোগ্রামটি কখনও শেষ হয় না! পরিবর্তে, এটি নতুন বার্তার জন্য চিরকাল অপেক্ষা করে। আপনাকে ctrl-c ব্যবহার করে এটি বন্ধ করতে হবে।
আসুন প্রথমে পরীক্ষা করে দেখি কেন বার্তাগুলি পুরো বিলম্বের পরে একবারে আসে, প্রতিটিটির মধ্যে বিলম্ব সহ আসার পরিবর্তে। একটি নির্দিষ্ট async ব্লকের মধ্যে, কোডে await
কিওয়ার্ডগুলি যে ক্রমে উপস্থিত হয়, প্রোগ্রাম চলার সময় সেগুলি সেই ক্রমেই কার্যকর হয়।
লিস্টিং ১৭-১০-এ কেবল একটি async ব্লক রয়েছে, তাই এর মধ্যে সবকিছু রৈখিকভাবে (linearly) চলে। এখনও কোনো কনকারেন্সি নেই। সমস্ত tx.send
কল ঘটে, যার মধ্যে সমস্ত trpl::sleep
কল এবং তাদের সংশ্লিষ্ট await পয়েন্টগুলি মিশে থাকে। শুধুমাত্র তারপরেই while let
লুপ recv
কলগুলির কোনো await
পয়েন্টের মধ্য দিয়ে যেতে পারে।
আমরা যে আচরণটি চাই তা পেতে, যেখানে প্রতিটি বার্তার মধ্যে sleep
বিলম্ব ঘটে, আমাদের tx
এবং rx
অপারেশনগুলিকে তাদের নিজস্ব async ব্লকে রাখতে হবে, যেমনটি লিস্টিং ১৭-১১-এ দেখানো হয়েছে। তারপরে রানটাইম trpl::join
ব্যবহার করে সেগুলির প্রতিটি আলাদাভাবে চালাতে পারে, ঠিক গণনার উদাহরণের মতো। আবারও, আমরা trpl::join
কল করার ফলাফল await করি, পৃথক future-গুলিকে নয়। যদি আমরা পৃথক future-গুলিকে ক্রমানুসারে await করতাম, আমরা কেবল একটি ক্রমিক প্রবাহে ফিরে আসতাম—ঠিক যা আমরা না করার চেষ্টা করছি।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
লিস্টিং ১৭-১১-এর আপডেট করা কোড দিয়ে, বার্তাগুলি ২ সেকেন্ড পরে একবারে আসার পরিবর্তে ৫০০-মিলিসেকেন্ডের ব্যবধানে প্রিন্ট হয়।
প্রোগ্রামটি এখনও কখনও শেষ হয় না, কারণ while let
লুপটি trpl::join
-এর সাথে যেভাবে ইন্টারঅ্যাক্ট করে তার জন্য:
trpl::join
থেকে রিটার্ন করা future-টি তখনই সম্পূর্ণ হয় যখন এটিতে পাস করা উভয় future সম্পূর্ণ হয়।tx
future-টিvals
-এর শেষ বার্তাটি পাঠানোর পরে স্লিপ করা শেষ হলে সম্পূর্ণ হয়।rx
future-টিwhile let
লুপ শেষ না হওয়া পর্যন্ত সম্পূর্ণ হবে না।while let
লুপটিrx.recv
await করাNone
তৈরি না করা পর্যন্ত শেষ হবে না।rx.recv
await করা তখনইNone
রিটার্ন করবে যখন চ্যানেলের অন্য প্রান্তটি বন্ধ হয়ে যাবে।- চ্যানেলটি তখনই বন্ধ হবে যদি আমরা
rx.close
কল করি বা যখন সেন্ডার সাইড,tx
, ড্রপ (drop) হয়ে যায়। - আমরা কোথাও
rx.close
কল করি না, এবংtx
ড্রপ হবে না যতক্ষণ নাtrpl::run
-কে পাস করা সবচেয়ে বাইরের async ব্লকটি শেষ হয়। - ব্লকটি শেষ হতে পারে না কারণ এটি
trpl::join
সম্পূর্ণ হওয়ার উপর ব্লক হয়ে আছে, যা আমাদের এই তালিকার শীর্ষে ফিরিয়ে নিয়ে যায়।
আমরা rx.close
কল করে ম্যানুয়ালি rx
বন্ধ করতে পারতাম, কিন্তু এর খুব একটা মানে হয় না। একটি নির্বিচারে সংখ্যক বার্তা পরিচালনা করার পরে থামলে প্রোগ্রামটি বন্ধ হয়ে যাবে, কিন্তু আমরা বার্তা মিস করতে পারি। আমাদের নিশ্চিত করার জন্য অন্য কোনো উপায় প্রয়োজন যাতে tx
ফাংশনটির শেষ হওয়ার আগে ড্রপ হয়ে যায়।
এখন, আমরা যে async ব্লকে বার্তা পাঠাই তা কেবল tx
ধার (borrow) করে কারণ একটি বার্তা পাঠানোর জন্য মালিকানার (ownership) প্রয়োজন হয় না, কিন্তু যদি আমরা tx
-কে সেই async ব্লকের মধ্যে মুভ (move) করতে পারতাম, তবে সেই ব্লকটি শেষ হয়ে গেলে এটি ড্রপ হয়ে যেত। চ্যাপ্টার ১৩-এর Capturing References or Moving Ownership বিভাগে, আপনি শিখেছেন কীভাবে ক্লোজারের সাথে move
কিওয়ার্ড ব্যবহার করতে হয়, এবং, চ্যাপ্টার ১৬-এর Using move
Closures with Threads বিভাগে আলোচনা করা হয়েছে, থ্রেডের সাথে কাজ করার সময় আমাদের প্রায়শই ক্লোজারের মধ্যে ডেটা মুভ করতে হয়। একই মৌলিক গতিবিদ্যা async ব্লকগুলিতেও প্রযোজ্য, তাই move
কিওয়ার্ড async ব্লকগুলির সাথে ঠিক সেভাবেই কাজ করে যেমনটি ক্লোজারের সাথে করে।
লিস্টিং ১৭-১২-এ, আমরা বার্তা পাঠানোর জন্য ব্যবহৃত ব্লকটিকে async
থেকে async move
-এ পরিবর্তন করি। যখন আমরা কোডের এই সংস্করণটি চালাই, তখন শেষ বার্তাটি পাঠানো এবং গ্রহণ করার পরে এটি সুন্দরভাবে বন্ধ হয়ে যায়।
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_millis(500)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; trpl::join(tx_fut, rx_fut).await; }); }
এই async চ্যানেলটিও একটি মাল্টিপল-প্রডিউসার চ্যানেল, তাই আমরা যদি একাধিক future থেকে বার্তা পাঠাতে চাই তবে tx
-এর উপর clone
কল করতে পারি, যেমনটি লিস্টিং ১৭-১৩-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_millis(500)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; let tx_fut = async move { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_millis(1500)).await; } }; trpl::join3(tx1_fut, tx_fut, rx_fut).await; }); }
প্রথমে, আমরা tx
ক্লোন করি, প্রথম async ব্লকের বাইরে tx1
তৈরি করি। আমরা tx1
-কে সেই ব্লকের মধ্যে মুভ করি ঠিক যেমনটি আমরা আগে tx
-এর সাথে করেছি। তারপরে, পরে, আমরা মূল tx
-কে একটি নতুন async ব্লকে মুভ করি, যেখানে আমরা সামান্য ধীর গতিতে আরও বার্তা পাঠাই। আমরা ঘটনাচক্রে এই নতুন async ব্লকটি বার্তা গ্রহণের জন্য async ব্লকের পরে রাখি, তবে এটি এর আগেও থাকতে পারত। মূল বিষয় হলো future-গুলি কোন ক্রমে await করা হয়, কোন ক্রমে সেগুলি তৈরি করা হয় তা নয়।
বার্তা পাঠানোর জন্য উভয় async ব্লককেই async move
ব্লক হতে হবে যাতে tx
এবং tx1
উভয়ই সেই ব্লকগুলি শেষ হলে ড্রপ হয়ে যায়। অন্যথায়, আমরা সেই একই অসীম লুপে ফিরে যাব যা দিয়ে আমরা শুরু করেছিলাম। অবশেষে, আমরা অতিরিক্ত future-টি পরিচালনা করার জন্য trpl::join
থেকে trpl::join3
-এ স্যুইচ করি।
এখন আমরা উভয় সেন্ডিং future থেকে সমস্ত বার্তা দেখতে পাই, এবং যেহেতু সেন্ডিং future-গুলি পাঠানোর পরে সামান্য ভিন্ন বিলম্ব ব্যবহার করে, বার্তাগুলিও সেই ভিন্ন ব্যবধানে গৃহীত হয়।
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
এটি একটি ভালো শুরু, কিন্তু এটি আমাদের কেবল কয়েকটি future-এর মধ্যে সীমাবদ্ধ রাখে: join
-এর সাথে দুটি, বা join3
-এর সাথে তিনটি। আসুন দেখি আমরা কীভাবে আরও future-এর সাথে কাজ করতে পারি।
যেকোনো সংখ্যক ফিউচারের সাথে কাজ করা
পূর্ববর্তী বিভাগে যখন আমরা দুটি ফিউচার থেকে তিনটি ফিউচারে স্যুইচ করেছি, তখন আমাদের join
থেকে join3
ব্যবহার করতে হয়েছিল। আমরা যতবার ফিউচারের সংখ্যা পরিবর্তন করব, ততবার একটি ভিন্ন ফাংশন কল করতে হলে তা বিরক্তিকর হতো। আনন্দের বিষয়, আমাদের কাছে join
-এর একটি ম্যাক্রো ফর্ম রয়েছে যেখানে আমরা ইচ্ছামত আর্গুমেন্ট পাস করতে পারি। এটি ফিউচারগুলোকে await করার কাজও নিজেই করে। সুতরাং, আমরা লিস্টিং 17-13 থেকে কোডটি join3
-এর পরিবর্তে join!
ব্যবহার করে পুনরায় লিখতে পারি, যেমনটি লিস্টিং 17-14-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; let tx_fut = async move { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; trpl::join!(tx1_fut, tx_fut, rx_fut); }); }
এটি অবশ্যই join
এবং join3
এবং join4
ইত্যাদির মধ্যে অদলবদল করার চেয়ে একটি উন্নতি! যাইহোক, এমনকি এই ম্যাক্রো ফর্মটি কেবল তখনই কাজ করে যখন আমরা আগে থেকে ফিউচারের সংখ্যা জানি। কিন্তু বাস্তব-জগতের রাস্ট কোডে, ফিউচারগুলোকে একটি কালেকশনে পুশ করা এবং তারপরে তাদের কিছু বা সমস্ত ফিউচার সম্পূর্ণ হওয়ার জন্য অপেক্ষা করা একটি সাধারণ প্যাটার্ন।
কোনো কালেকশনের সমস্ত ফিউচার পরীক্ষা করার জন্য, আমাদের সেগুলোর সবগুলোর উপর ইটারেট করতে হবে এবং জয়েন করতে হবে। trpl::join_all
ফাংশনটি যেকোনো টাইপ গ্রহণ করে যা Iterator
ট্রেইট ইমপ্লিমেন্ট করে, যা আপনি চ্যাপ্টার ১৩-এর The Iterator Trait and the next
Method-এ শিখেছেন, তাই এটি ঠিক কাজের জিনিস বলে মনে হচ্ছে। আসুন আমাদের ফিউচারগুলোকে একটি ভেক্টরে রাখি এবং join!
-কে join_all
দিয়ে প্রতিস্থাপন করার চেষ্টা করি যেমনটি লিস্টিং 17-15-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures = vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
দুর্ভাগ্যবশত, এই কোডটি কম্পাইল হয় না। পরিবর্তে, আমরা এই ত্রুটিটি পাই:
error[E0308]: mismatched types
--> src/main.rs:45:37
|
10 | let tx1_fut = async move {
| ---------- the expected `async` block
...
24 | let rx_fut = async {
| ----- the found `async` block
...
45 | let futures = vec![tx1_fut, rx_fut, tx_fut];
| ^^^^^^ expected `async` block, found a different `async` block
|
= note: expected `async` block `{async block@src/main.rs:10:23: 10:33}`
found `async` block `{async block@src/main.rs:24:22: 24:27}`
= note: no two async blocks, even if identical, have the same type
= help: consider pinning your async block and casting it to a trait object
এটি আশ্চর্যজনক হতে পারে। সর্বোপরি, কোনো async ব্লকই কিছু রিটার্ন করে না, তাই প্রতিটি একটি Future<Output = ()>
তৈরি করে। মনে রাখবেন যে Future
একটি ট্রেইট, এবং কম্পাইলার প্রতিটি async ব্লকের জন্য একটি অনন্য enum তৈরি করে। আপনি একটি Vec
-এ দুটি ভিন্ন হাতে লেখা struct রাখতে পারবেন না, এবং একই নিয়ম কম্পাইলার দ্বারা জেনারেট করা বিভিন্ন enum-এর ক্ষেত্রেও প্রযোজ্য।
এটি কাজ করানোর জন্য, আমাদের ট্রেইট অবজেক্ট ব্যবহার করতে হবে, ঠিক যেমনটি আমরা চ্যাপ্টার ১২-এর “Returning Errors from the run function”-এ করেছিলাম। (আমরা চ্যাপ্টার ১৮-এ ট্রেইট অবজেক্ট নিয়ে বিস্তারিত আলোচনা করব।) ট্রেইট অবজেক্ট ব্যবহার করে আমরা এই টাইপগুলো দ্বারা উৎপাদিত প্রতিটি নামহীন ফিউচারকে একই টাইপ হিসাবে বিবেচনা করতে পারি, কারণ সেগুলির সবগুলোই Future
ট্রেইট ইমপ্লিমেন্ট করে।
দ্রষ্টব্য: চ্যাপ্টার ৮-এর Using an Enum to Store Multiple Values-এ, আমরা একটি
Vec
-এ একাধিক টাইপ অন্তর্ভুক্ত করার আরেকটি উপায় নিয়ে আলোচনা করেছি: ভেক্টরে উপস্থিত হতে পারে এমন প্রতিটি টাইপকে উপস্থাপন করার জন্য একটি enum ব্যবহার করা। তবে, আমরা এখানে তা করতে পারি না। একটি কারণ হলো, আমাদের বিভিন্ন টাইপের নামকরণ করার কোনো উপায় নেই, কারণ সেগুলি নামহীন। আরেকটি কারণ হলো, আমরা একটি ভেক্টর এবংjoin_all
ব্যবহার করার মূল কারণটি ছিল ফিউচারের একটি ডাইনামিক কালেকশনের সাথে কাজ করতে পারা যেখানে আমরা কেবল তাদের একই আউটপুট টাইপ থাকা নিয়েই চিন্তা করি।
আমরা লিস্টিং 17-16-এ দেখানো হিসাবে vec!
-এর প্রতিটি ফিউচারকে একটি Box::new
-এ র্যাপ করে শুরু করি।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
দুর্ভাগ্যবশত, এই কোডটি এখনও কম্পাইল হয় না। আসলে, আমরা দ্বিতীয় এবং তৃতীয় Box::new
কল উভয়ের জন্য আগের মতোই একই মৌলিক ত্রুটি পাই, সেইসাথে Unpin
ট্রেইট উল্লেখ করে নতুন ত্রুটিও পাই। আমরা এক মুহূর্তের মধ্যে Unpin
ত্রুটিগুলিতে ফিরে আসব। প্রথমে, আসুন futures
ভেরিয়েবলের টাইপটি স্পষ্টভাবে উল্লেখ করে Box::new
কলগুলিতে টাইপের ত্রুটিগুলি ঠিক করি (দেখুন লিস্টিং 17-17)।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
এই টাইপ ডিক্লারেশনটি একটু জটিল, তাই আসুন এটি ধাপে ধাপে দেখি:
- সবচেয়ে ভেতরের টাইপটি হলো ফিউচার নিজেই। আমরা স্পষ্টভাবে উল্লেখ করি যে ফিউচারের আউটপুট হলো ইউনিট টাইপ
()
যাFuture<Output = ()>
লিখে করা হয়েছে। - তারপর আমরা ট্রেইটটিকে ডাইনামিক হিসাবে চিহ্নিত করতে
dyn
দিয়ে টীকাবদ্ধ (annotate) করি। - পুরো ট্রেইট রেফারেন্সটি একটি
Box
-এ মোড়ানো হয়। - অবশেষে, আমরা স্পষ্টভাবে বলি যে
futures
হলো একটিVec
যা এই আইটেমগুলি ধারণ করে।
এটি ইতিমধ্যেই একটি বড় পার্থক্য তৈরি করেছে। এখন যখন আমরা কম্পাইলার চালাই, আমরা কেবল Unpin
উল্লেখ করা ত্রুটিগুলি পাই। যদিও তিনটি ত্রুটি আছে, তাদের বিষয়বস্তু খুব অনুরূপ।
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:49:24
|
49 | trpl::join_all(futures).await;
| -------------- ^^^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
| |
| required by a bound introduced by this call
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `join_all`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:105:14
|
102 | pub fn join_all<I>(iter: I) -> JoinAll<I::Item>
| -------- required by a bound in this function
...
105 | I::Item: Future,
| ^^^^^^ required by this bound in `join_all`
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:49:9
|
49 | trpl::join_all(futures).await;
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:49:33
|
49 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `async_await` (bin "async_await") due to 3 previous errors
এটি হজম করার জন্য অনেক কিছু, তাই আসুন এটি ভেঙে দেখি। বার্তার প্রথম অংশটি আমাদের বলে যে প্রথম async ব্লকটি (src/main.rs:8:23: 20:10
) Unpin
ট্রেইট ইমপ্লিমেন্ট করে না এবং এটি সমাধান করার জন্য pin!
বা Box::pin
ব্যবহার করার পরামর্শ দেয়। অধ্যায়ের পরে, আমরা Pin
এবং Unpin
সম্পর্কে আরও কিছু বিশদ বিবরণে যাব। তবে এই মুহূর্তে, আমরা কেবল আটকে যাওয়া অবস্থা থেকে বের হতে কম্পাইলারের পরামর্শ অনুসরণ করতে পারি। লিস্টিং 17-18-এ, আমরা std::pin
থেকে Pin
ইমপোর্ট করে শুরু করি। এরপরে আমরা futures
-এর জন্য টাইপ অ্যানোটেশন আপডেট করি, প্রতিটি Box
-কে একটি Pin
দিয়ে র্যাপ করে। অবশেষে, আমরা ফিউচারগুলিকে পিন করার জন্য Box::pin
ব্যবহার করি।
extern crate trpl; // required for mdbook test use std::pin::Pin; // -- snip -- use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = async move { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; let rx_fut = async { while let Some(value) = rx.recv().await { println!("received '{value}'"); } }; let tx_fut = async move { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }; let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> = vec![Box::pin(tx1_fut), Box::pin(rx_fut), Box::pin(tx_fut)]; trpl::join_all(futures).await; }); }
যদি আমরা এটি কম্পাইল করে চালাই, আমরা অবশেষে সেই আউটপুট পাব যা আমরা আশা করেছিলাম:
received 'hi'
received 'more'
received 'from'
received 'messages'
received 'the'
received 'for'
received 'future'
received 'you'
যাক বাবা!
এখানে আরও কিছু অন্বেষণ করার আছে। একটি বিষয় হলো, Pin<Box<T>>
ব্যবহার করা এই ফিউচারগুলিকে Box
দিয়ে হিপে রাখার কারণে সামান্য পরিমাণ ওভারহেড যোগ করে—এবং আমরা এটি কেবল টাইপগুলি মেলানোর জন্যই করছি। আমাদের আসলে প্রয়োজন নেই হিপ অ্যালোকেশনের, সর্বোপরি: এই ফিউচারগুলি এই নির্দিষ্ট ফাংশনের জন্য স্থানীয়। যেমন আগে উল্লেখ করা হয়েছে, Pin
নিজেই একটি র্যাপার টাইপ, তাই আমরা Vec
-এ একটি একক টাইপ থাকার সুবিধা পেতে পারি—যা ছিল Box
ব্যবহার করার মূল কারণ—হিপ অ্যালোকেশন ছাড়াই। আমরা প্রতিটি ফিউচারের সাথে সরাসরি Pin
ব্যবহার করতে পারি, std::pin::pin
ম্যাক্রো ব্যবহার করে।
যাইহোক, আমাদের এখনও পিন করা রেফারেন্সের টাইপ সম্পর্কে সুস্পষ্ট হতে হবে; অন্যথায়, রাস্ট এখনও জানবে না যে এগুলিকে ডাইনামিক ট্রেইট অবজেক্ট হিসাবে ব্যাখ্যা করতে হবে, যা Vec
-এ আমাদের প্রয়োজন। তাই আমরা std::pin
থেকে আমাদের ইমপোর্টের তালিকায় pin
যোগ করি। তারপরে আমরা প্রতিটি ফিউচারকে pin!
করতে পারি যখন আমরা এটি সংজ্ঞায়িত করি এবং futures
-কে ডাইনামিক ফিউচার টাইপের পিন করা মিউটেবল রেফারেন্স ধারণকারী একটি Vec
হিসাবে সংজ্ঞায়িত করি, যেমনটি লিস্টিং 17-19-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::pin::{Pin, pin}; // -- snip -- use std::time::Duration; fn main() { trpl::run(async { let (tx, mut rx) = trpl::channel(); let tx1 = tx.clone(); let tx1_fut = pin!(async move { // --snip-- let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("future"), ]; for val in vals { tx1.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }); let rx_fut = pin!(async { // --snip-- while let Some(value) = rx.recv().await { println!("received '{value}'"); } }); let tx_fut = pin!(async move { // --snip-- let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); trpl::sleep(Duration::from_secs(1)).await; } }); let futures: Vec<Pin<&mut dyn Future<Output = ()>>> = vec![tx1_fut, rx_fut, tx_fut]; trpl::join_all(futures).await; }); }
আমরা এই পর্যন্ত এসেছি এই সত্যটিকে উপেক্ষা করে যে আমাদের বিভিন্ন Output
টাইপ থাকতে পারে। উদাহরণস্বরূপ, লিস্টিং 17-20-এ, a
-এর জন্য নামহীন ফিউচারটি Future<Output = u32>
ইমপ্লিমেন্ট করে, b
-এর জন্য নামহীন ফিউচারটি Future<Output = &str>
ইমপ্লিমেন্ট করে, এবং c
-এর জন্য নামহীন ফিউচারটি Future<Output = bool>
ইমপ্লিমেন্ট করে।
extern crate trpl; // required for mdbook test fn main() { trpl::run(async { let a = async { 1u32 }; let b = async { "Hello!" }; let c = async { true }; let (a_result, b_result, c_result) = trpl::join!(a, b, c); println!("{a_result}, {b_result}, {c_result}"); }); }
আমরা তাদের await করার জন্য trpl::join!
ব্যবহার করতে পারি, কারণ এটি আমাদের একাধিক ফিউচার টাইপ পাস করার অনুমতি দেয় এবং সেই টাইপগুলির একটি টাপল তৈরি করে। আমরা trpl::join_all
ব্যবহার করতে পারি না, কারণ এটির জন্য পাস করা সমস্ত ফিউচারের একই টাইপ থাকা প্রয়োজন। মনে রাখবেন, সেই ত্রুটিটিই আমাদের Pin
-এর সাথে এই অ্যাডভেঞ্চার শুরু করিয়েছিল!
এটি একটি মৌলিক ট্রেড-অফ: আমরা হয় join_all
-এর সাথে একটি ডাইনামিক সংখ্যক ফিউচারের সাথে ডিল করতে পারি, যতক্ষণ না তাদের সবার একই টাইপ থাকে, অথবা আমরা join
ফাংশন বা join!
ম্যাক্রোর সাথে একটি নির্দিষ্ট সংখ্যক ফিউচারের সাথে ডিল করতে পারি, এমনকি তাদের বিভিন্ন টাইপ থাকলেও। এটি একই পরিস্থিতি যা আমরা রাস্টে অন্য কোনো টাইপের সাথে কাজ করার সময় সম্মুখীন হতাম। ফিউচারগুলি বিশেষ কিছু নয়, যদিও আমাদের তাদের সাথে কাজ করার জন্য কিছু চমৎকার সিনট্যাক্স আছে, এবং এটি একটি ভালো জিনিস।
ফিউচার রেসিং
যখন আমরা join
পরিবারের ফাংশন এবং ম্যাক্রোগুলোর সাথে ফিউচারগুলিকে "জয়েন" করি, তখন আমাদের এগিয়ে যাওয়ার আগে সেগুলির সবগুলো শেষ হওয়ার প্রয়োজন হয়। তবে কখনও কখনও, এগিয়ে যাওয়ার আগে আমাদের একটি সেট থেকে কেবল কিছু ফিউচার শেষ হলেই চলে— অনেকটা একটি ফিউচারকে অন্যটির বিরুদ্ধে রেস করানোর মতো।
লিস্টিং 17-21-এ, আমরা আবারও trpl::race
ব্যবহার করে দুটি ফিউচার, slow
এবং fast
-কে একে অপরের বিরুদ্ধে চালাই।
extern crate trpl; // required for mdbook test use std::time::Duration; fn main() { trpl::run(async { let slow = async { println!("'slow' started."); trpl::sleep(Duration::from_millis(100)).await; println!("'slow' finished."); }; let fast = async { println!("'fast' started."); trpl::sleep(Duration::from_millis(50)).await; println!("'fast' finished."); }; trpl::race(slow, fast).await; }); }
প্রতিটি ফিউচার যখন চলতে শুরু করে তখন একটি বার্তা প্রিন্ট করে, sleep
কল করে এবং await করে কিছু সময়ের জন্য পজ করে, এবং তারপরে যখন এটি শেষ হয় তখন আরেকটি বার্তা প্রিন্ট করে। তারপরে আমরা slow
এবং fast
উভয়কেই trpl::race
-এ পাস করি এবং তাদের মধ্যে একটি শেষ হওয়ার জন্য অপেক্ষা করি। (এখানের ফলাফল খুব আশ্চর্যজনক নয়: fast
জেতে।) যখন আমরা “Our First Async Program”-এ race
ব্যবহার করেছিলাম তার থেকে ভিন্ন, আমরা এখানে এটি রিটার্ন করা Either
ইন্সট্যান্সটিকে উপেক্ষা করি, কারণ সমস্ত আকর্ষণীয় আচরণ async ব্লকগুলির বডিতে ঘটে।
লক্ষ্য করুন যে আপনি যদি race
-এর আর্গুমেন্টের ক্রম উল্টে দেন, তবে "started" বার্তাগুলির ক্রম পরিবর্তিত হয়, যদিও fast
ফিউচারটি সবসময় প্রথমে সম্পন্ন হয়। এর কারণ হলো এই নির্দিষ্ট race
ফাংশনের ইমপ্লিমেন্টেশনটি ফেয়ার (fair) নয়। এটি সর্বদা আর্গুমেন্ট হিসাবে পাস করা ফিউচারগুলিকে যে ক্রমে পাস করা হয় সেই ক্রমে চালায়। অন্যান্য ইমপ্লিমেন্টেশনগুলি ফেয়ার এবং এলোমেলোভাবে বেছে নেবে কোন ফিউচারটি প্রথমে পোল (poll) করতে হবে। race
-এর যে ইমপ্লিমেন্টেশন আমরা ব্যবহার করছি তা ফেয়ার হোক বা না হোক, একটি ফিউচার অন্য টাস্ক শুরু করার আগে তার বডিতে প্রথম await
পর্যন্ত চলবে।
Our First Async Program থেকে স্মরণ করুন যে প্রতিটি await পয়েন্টে, রাস্ট একটি রানটাইমকে টাস্কটি পজ করার এবং অন্য একটিতে স্যুইচ করার সুযোগ দেয় যদি await করা ফিউচারটি প্রস্তুত না থাকে। এর বিপরীতটিও সত্য: রাস্ট কেবলমাত্র একটি await পয়েন্টে async ব্লকগুলি পজ করে এবং একটি রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দেয়। await পয়েন্টগুলির মধ্যে সবকিছুই সিঙ্ক্রোনাস।
এর মানে হলো যদি আপনি একটি await পয়েন্ট ছাড়াই একটি async ব্লকে অনেক কাজ করেন, তবে সেই ফিউচারটি অন্য কোনো ফিউচারকে অগ্রগতি করতে বাধা দেবে। আপনি কখনও কখনও এটিকে একটি ফিউচার অন্য ফিউচারকে স্টার্ভিং (starving) হিসাবে উল্লেখ করতে শুনতে পারেন। কিছু ক্ষেত্রে, এটি একটি বড় ব্যাপার নাও হতে পারে। যাইহোক, যদি আপনি কোনো ধরনের ব্যয়বহুল সেটআপ বা দীর্ঘ সময় ধরে চলা কাজ করছেন, বা যদি আপনার একটি ফিউচার থাকে যা অনির্দিষ্টকালের জন্য কোনো নির্দিষ্ট কাজ করতে থাকবে, তবে আপনাকে কখন এবং কোথায় রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দিতে হবে তা নিয়ে ভাবতে হবে।
একইভাবে, যদি আপনার দীর্ঘ সময় ধরে চলা ব্লকিং অপারেশন থাকে, তবে async প্রোগ্রামের বিভিন্ন অংশ একে অপরের সাথে কীভাবে সম্পর্কিত হবে তার উপায় সরবরাহ করার জন্য একটি দরকারী টুল হতে পারে।
কিন্তু সেই ক্ষেত্রে আপনি কীভাবে রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দেবেন?
রানটাইমকে নিয়ন্ত্রণ ছেড়ে দেওয়া (Yielding Control)
আসুন একটি দীর্ঘ সময় ধরে চলা অপারেশন সিমুলেট করি। লিস্টিং 17-22 একটি slow
ফাংশন উপস্থাপন করে।
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { // We will call `slow` here later }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
এই কোডটি trpl::sleep
-এর পরিবর্তে std::thread::sleep
ব্যবহার করে যাতে slow
কল করা বর্তমান থ্রেডটিকে কিছু সংখ্যক মিলিসেকেন্ডের জন্য ব্লক করে। আমরা slow
-কে বাস্তব-বিশ্বের অপারেশনগুলির বিকল্প হিসাবে ব্যবহার করতে পারি যা দীর্ঘ সময় ধরে চলে এবং ব্লকিং।
লিস্টিং 17-23-এ, আমরা একজোড়া ফিউচারে এই ধরনের CPU-বাউন্ড কাজ অনুকরণ করতে slow
ব্যবহার করি।
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { let a = async { println!("'a' started."); slow("a", 30); slow("a", 10); slow("a", 20); trpl::sleep(Duration::from_millis(50)).await; println!("'a' finished."); }; let b = async { println!("'b' started."); slow("b", 75); slow("b", 10); slow("b", 15); slow("b", 350); trpl::sleep(Duration::from_millis(50)).await; println!("'b' finished."); }; trpl::race(a, b).await; }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
শুরু করার জন্য, প্রতিটি ফিউচার কেবল একগুচ্ছ ধীর অপারেশন করার পরেই রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দেয়। আপনি যদি এই কোডটি চালান, আপনি এই আউটপুট দেখতে পাবেন:
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
আমাদের আগের উদাহরণের মতো, a
শেষ হওয়ার সাথে সাথেই race
শেষ হয়। তবে, দুটি ফিউচারের মধ্যে কোনো ইন্টারলিভিং নেই। a
ফিউচারটি trpl::sleep
কল await না হওয়া পর্যন্ত তার সমস্ত কাজ করে, তারপর b
ফিউচারটি তার নিজের trpl::sleep
কল await না হওয়া পর্যন্ত তার সমস্ত কাজ করে, এবং অবশেষে a
ফিউচারটি সম্পন্ন হয়। উভয় ফিউচারকে তাদের ধীর কাজগুলির মধ্যে অগ্রগতি করার অনুমতি দিতে, আমাদের await পয়েন্ট প্রয়োজন যাতে আমরা রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দিতে পারি। এর মানে হলো আমাদের এমন কিছু দরকার যা আমরা await করতে পারি!
আমরা ইতিমধ্যে লিস্টিং 17-23-এ এই ধরনের হ্যান্ডঅফ দেখতে পাচ্ছি: যদি আমরা a
ফিউচারের শেষে trpl::sleep
সরিয়ে ফেলি, তবে এটি b
ফিউচারটি একেবারেই না চলেই সম্পন্ন হবে। আসুন অপারেশনগুলিকে অগ্রগতিতে স্যুইচ করার জন্য একটি সূচনা বিন্দু হিসাবে sleep
ফাংশনটি ব্যবহার করার চেষ্টা করি, যেমনটি লিস্টিং 17-24-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { let one_ms = Duration::from_millis(1); let a = async { println!("'a' started."); slow("a", 30); trpl::sleep(one_ms).await; slow("a", 10); trpl::sleep(one_ms).await; slow("a", 20); trpl::sleep(one_ms).await; println!("'a' finished."); }; let b = async { println!("'b' started."); slow("b", 75); trpl::sleep(one_ms).await; slow("b", 10); trpl::sleep(one_ms).await; slow("b", 15); trpl::sleep(one_ms).await; slow("b", 350); trpl::sleep(one_ms).await; println!("'b' finished."); }; trpl::race(a, b).await; }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
লিস্টিং 17-24-এ, আমরা slow
-এর প্রতিটি কলের মধ্যে await পয়েন্ট সহ trpl::sleep
কল যোগ করি। এখন দুটি ফিউচারের কাজ ইন্টারলিভড (interleaved) বা মিশ্রিত:
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
a
ফিউচারটি b
-কে নিয়ন্ত্রণ হস্তান্তর করার আগে এখনও কিছুক্ষণ চলে, কারণ এটি trpl::sleep
কল করার আগেই slow
কল করে, কিন্তু তারপরে ফিউচারগুলি প্রতিবার যখন তাদের মধ্যে একটি await পয়েন্টে পৌঁছায় তখন অদলবদল করে। এই ক্ষেত্রে, আমরা slow
-এর প্রতিটি কলের পরে এটি করেছি, কিন্তু আমরা কাজটি আমাদের জন্য সবচেয়ে অর্থপূর্ণ উপায়ে ভাগ করতে পারতাম।
তবে আমরা এখানে সত্যিই স্লিপ করতে চাই না: আমরা যত দ্রুত সম্ভব অগ্রগতি করতে চাই। আমাদের কেবল রানটাইমের কাছে নিয়ন্ত্রণ ফিরিয়ে দিতে হবে। আমরা এটি সরাসরি করতে পারি, yield_now
ফাংশন ব্যবহার করে। লিস্টিং 17-25-এ, আমরা সেই সমস্ত sleep
কলগুলিকে yield_now
দিয়ে প্রতিস্থাপন করি।
extern crate trpl; // required for mdbook test use std::{thread, time::Duration}; fn main() { trpl::run(async { let a = async { println!("'a' started."); slow("a", 30); trpl::yield_now().await; slow("a", 10); trpl::yield_now().await; slow("a", 20); trpl::yield_now().await; println!("'a' finished."); }; let b = async { println!("'b' started."); slow("b", 75); trpl::yield_now().await; slow("b", 10); trpl::yield_now().await; slow("b", 15); trpl::yield_now().await; slow("b", 350); trpl::yield_now().await; println!("'b' finished."); }; trpl::race(a, b).await; }); } fn slow(name: &str, ms: u64) { thread::sleep(Duration::from_millis(ms)); println!("'{name}' ran for {ms}ms"); }
এই কোডটি প্রকৃত উদ্দেশ্য সম্পর্কে আরও স্পষ্ট এবং sleep
ব্যবহার করার চেয়ে উল্লেখযোগ্যভাবে দ্রুত হতে পারে, কারণ sleep
দ্বারা ব্যবহৃত টাইমারগুলির মতো টাইমারগুলির প্রায়শই তারা কতটা সূক্ষ্ম হতে পারে তার উপর সীমাবদ্ধতা থাকে। আমরা যে sleep
-এর সংস্করণটি ব্যবহার করছি, উদাহরণস্বরূপ, এটি সর্বদা কমপক্ষে এক মিলিসেকেন্ডের জন্য ঘুমাবে, এমনকি যদি আমরা এটিকে এক ন্যানোসেকেন্ডের একটি Duration
পাস করি। আবারও, আধুনিক কম্পিউটারগুলি দ্রুত: তারা এক মিলিসেকেন্ডে অনেক কিছু করতে পারে!
আপনি লিস্টিং 17-26-এর মতো একটি ছোট বেঞ্চমার্ক সেট আপ করে এটি নিজেই দেখতে পারেন। (এটি পারফরম্যান্স পরীক্ষা করার জন্য একটি বিশেষ কঠোর উপায় নয়, তবে এটি এখানে পার্থক্য দেখানোর জন্য যথেষ্ট।)
extern crate trpl; // required for mdbook test use std::time::{Duration, Instant}; fn main() { trpl::run(async { let one_ns = Duration::from_nanos(1); let start = Instant::now(); async { for _ in 1..1000 { trpl::sleep(one_ns).await; } } .await; let time = Instant::now() - start; println!( "'sleep' version finished after {} seconds.", time.as_secs_f32() ); let start = Instant::now(); async { for _ in 1..1000 { trpl::yield_now().await; } } .await; let time = Instant::now() - start; println!( "'yield' version finished after {} seconds.", time.as_secs_f32() ); }); }
এখানে, আমরা সমস্ত স্ট্যাটাস প্রিন্টিং এড়িয়ে যাই, trpl::sleep
-কে এক-ন্যানোসেকেন্ড Duration
পাস করি, এবং প্রতিটি ফিউচারকে নিজে চলতে দিই, ফিউচারগুলির মধ্যে কোনো স্যুইচিং ছাড়াই। তারপরে আমরা ১,০০০ ইটারেশন চালাই এবং দেখি trpl::sleep
ব্যবহারকারী ফিউচারটি trpl::yield_now
ব্যবহারকারী ফিউচারের তুলনায় কত সময় নেয়।
yield_now
সহ সংস্করণটি অনেক দ্রুত!
এর মানে হলো যে async এমনকি কম্পিউট-বাউন্ড টাস্কগুলির জন্যও উপযোগী হতে পারে, আপনার প্রোগ্রাম আর কী করছে তার উপর নির্ভর করে, কারণ এটি প্রোগ্রামের বিভিন্ন অংশের মধ্যে সম্পর্ক কাঠামোবদ্ধ করার জন্য একটি দরকারী টুল সরবরাহ করে। এটি কো-অপারেটিভ মাল্টিটাস্কিং (cooperative multitasking)-এর একটি রূপ, যেখানে প্রতিটি ফিউচারের await পয়েন্টের মাধ্যমে কখন নিয়ন্ত্রণ হস্তান্তর করবে তা নির্ধারণ করার ক্ষমতা রয়েছে। তাই প্রতিটি ফিউচারেরও খুব বেশিক্ষণ ব্লক করা এড়ানোর দায়িত্ব রয়েছে। কিছু রাস্ট-ভিত্তিক এমবেডেড অপারেটিং সিস্টেমে, এটিই একমাত্র ধরনের মাল্টিটাস্কিং!
বাস্তব-জগতের কোডে, আপনি সাধারণত প্রতিটি লাইনে await পয়েন্টগুলির সাথে ফাংশন কলগুলিকে অদলবদল করবেন না, অবশ্যই। যদিও এইভাবে নিয়ন্ত্রণ হস্তান্তর করা তুলনামূলকভাবে সস্তা, এটি বিনামূল্যে নয়। অনেক ক্ষেত্রে, একটি কম্পিউট-বাউন্ড টাস্ককে ভাঙার চেষ্টা করলে এটি উল্লেখযোগ্যভাবে ধীর হতে পারে, তাই কখনও কখনও সামগ্রিক পারফরম্যান্সের জন্য একটি অপারেশনকে সংক্ষিপ্তভাবে ব্লক করতে দেওয়া ভালো। আপনার কোডের প্রকৃত পারফরম্যান্সের বাধাগুলি কী তা দেখতে সর্বদা পরিমাপ করুন। তবে, যদি আপনি এমন অনেক কাজ সিরিয়ালে হতে দেখেন যা আপনি কনকারেন্টলি হওয়ার আশা করেছিলেন, তবে অন্তর্নিহিত গতিশীলতাটি মনে রাখা গুরুত্বপূর্ণ!
আমাদের নিজস্ব অ্যাসিঙ্ক্রোনাস অ্যাবস্ট্র্যাকশন তৈরি করা
আমরা নতুন প্যাটার্ন তৈরি করতে ফিউচারগুলিকে একসাথে কম্পোজও করতে পারি। উদাহরণস্বরূপ, আমরা আমাদের কাছে ইতিমধ্যে থাকা async বিল্ডিং ব্লকগুলি দিয়ে একটি timeout
ফাংশন তৈরি করতে পারি। যখন আমরা শেষ করব, ফলাফলটি আরেকটি বিল্ডিং ব্লক হবে যা আমরা আরও async অ্যাবস্ট্র্যাকশন তৈরি করতে ব্যবহার করতে পারি।
লিস্টিং 17-27 দেখায় যে আমরা এই timeout
একটি ধীর ফিউচারের সাথে কীভাবে কাজ করবে বলে আশা করব।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_millis(100)).await;
"I finished!"
};
match timeout(slow, Duration::from_millis(10)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
আসুন এটি ইমপ্লিমেন্ট করি! শুরু করার জন্য, আসুন timeout
-এর API সম্পর্কে চিন্তা করি:
- এটি নিজে একটি async ফাংশন হতে হবে যাতে আমরা এটিকে await করতে পারি।
- এর প্রথম প্যারামিটারটি চালানোর জন্য একটি ফিউচার হওয়া উচিত। আমরা এটিকে যেকোনো ফিউচারের সাথে কাজ করার অনুমতি দেওয়ার জন্য জেনেরিক করতে পারি।
- এর দ্বিতীয় প্যারামিটারটি হবে অপেক্ষা করার সর্বোচ্চ সময়। যদি আমরা একটি
Duration
ব্যবহার করি, তবে এটিtrpl::sleep
-এ পাস করা সহজ হবে। - এটি একটি
Result
রিটার্ন করা উচিত। যদি ফিউচারটি সফলভাবে সম্পন্ন হয়,Result
টিOk
হবে ফিউচার দ্বারা উৎপাদিত মান সহ। যদি টাইমআউটটি আগে শেষ হয়ে যায়,Result
টিErr
হবে টাইমআউটটি যে সময় অপেক্ষা করেছে সেই সময়কাল সহ।
লিস্টিং 17-28 এই ডিক্লারেশনটি দেখায়।
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::run(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
এটি টাইপের জন্য আমাদের লক্ষ্য পূরণ করে। এখন আসুন আমাদের প্রয়োজনীয় আচরণ সম্পর্কে চিন্তা করি: আমরা পাস করা ফিউচারটিকে সময়কালের বিরুদ্ধে রেস করতে চাই। আমরা সময়কাল থেকে একটি টাইমার ফিউচার তৈরি করতে trpl::sleep
ব্যবহার করতে পারি, এবং সেই টাইমারটিকে কলারের পাস করা ফিউচারের সাথে চালানোর জন্য trpl::race
ব্যবহার করতে পারি।
আমরা আরও জানি যে race
ফেয়ার নয়, আর্গুমেন্টগুলিকে যে ক্রমে পাস করা হয় সেই ক্রমে পোল করে। সুতরাং, আমরা future_to_try
-কে প্রথমে race
-এ পাস করি যাতে max_time
খুব কম সময় হলেও এটি সম্পন্ন হওয়ার সুযোগ পায়। যদি future_to_try
প্রথমে শেষ হয়, race
future_to_try
-এর আউটপুট সহ Left
রিটার্ন করবে। যদি timer
প্রথমে শেষ হয়, race
টাইমারের আউটপুট ()
সহ Right
রিটার্ন করবে।
লিস্টিং 17-29-এ, আমরা trpl::race
await করার ফলাফলের উপর ম্যাচ করি।
extern crate trpl; // required for mdbook test use std::time::Duration; use trpl::Either; // --snip-- fn main() { trpl::run(async { let slow = async { trpl::sleep(Duration::from_secs(5)).await; "Finally finished" }; match timeout(slow, Duration::from_secs(2)).await { Ok(message) => println!("Succeeded with '{message}'"), Err(duration) => { println!("Failed after {} seconds", duration.as_secs()) } } }); } async fn timeout<F: Future>( future_to_try: F, max_time: Duration, ) -> Result<F::Output, Duration> { match trpl::race(future_to_try, trpl::sleep(max_time)).await { Either::Left(output) => Ok(output), Either::Right(_) => Err(max_time), } }
যদি future_to_try
সফল হয় এবং আমরা একটি Left(output)
পাই, আমরা Ok(output)
রিটার্ন করি। যদি স্লিপ টাইমারটি পরিবর্তে শেষ হয়ে যায় এবং আমরা একটি Right(())
পাই, আমরা _
দিয়ে ()
উপেক্ষা করি এবং পরিবর্তে Err(max_time)
রিটার্ন করি।
এর সাথে, আমাদের কাছে দুটি অন্য async হেল্পার দিয়ে তৈরি একটি কার্যকরী timeout
আছে। যদি আমরা আমাদের কোড চালাই, এটি টাইমআউটের পরে ব্যর্থতার মোড প্রিন্ট করবে:
Failed after 2 seconds
যেহেতু ফিউচারগুলি অন্য ফিউচারের সাথে কম্পোজ করে, আপনি ছোট async বিল্ডিং ব্লক ব্যবহার করে সত্যিই শক্তিশালী টুল তৈরি করতে পারেন। উদাহরণস্বরূপ, আপনি রিট্রাইয়ের সাথে টাইমআউট একত্রিত করতে এই একই পদ্ধতি ব্যবহার করতে পারেন, এবং পরিবর্তে সেগুলি নেটওয়ার্ক কলের মতো অপারেশনের সাথে ব্যবহার করতে পারেন (অধ্যায়ের শুরুর উদাহরণগুলির মধ্যে একটি)।
বাস্তবে, আপনি সাধারণত সরাসরি async
এবং await
-এর সাথে কাজ করবেন, এবং দ্বিতীয়ত join
, join_all
, race
, এবং আরও অনেক ফাংশন এবং ম্যাক্রোর সাথে কাজ করবেন। আপনাকে শুধুমাত্র সেই API-গুলির সাথে ফিউচার ব্যবহার করার জন্য মাঝে মাঝে pin
-এর প্রয়োজন হবে।
আমরা এখন একই সময়ে একাধিক ফিউচারের সাথে কাজ করার বেশ কয়েকটি উপায় দেখেছি। এরপরে, আমরা দেখব কীভাবে আমরা সময়ের সাথে সাথে স্ট্রিম (streams) দিয়ে একটি ক্রমানুসারে একাধিক ফিউচারের সাথে কাজ করতে পারি। তবে, প্রথমে আপনার বিবেচনা করার মতো আরও কয়েকটি বিষয় এখানে রয়েছে:
-
কোনো গ্রুপের সমস্ত ফিউচার শেষ হওয়ার জন্য অপেক্ষা করতে আমরা
join_all
-এর সাথে একটিVec
ব্যবহার করেছি। আপনি কীভাবে একটিVec
ব্যবহার করে ফিউচারের একটি গ্রুপকে ক্রমানুসারে প্রসেস করতে পারেন? এটি করার ট্রেড-অফগুলি কী কী? -
futures
ক্রেট থেকেfutures::stream::FuturesUnordered
টাইপটি দেখুন। এটি ব্যবহার করা একটিVec
ব্যবহার করার থেকে কীভাবে ভিন্ন হবে? (ক্রেটেরstream
অংশ থেকে এটি এসেছে এই সত্যটি নিয়ে চিন্তা করবেন না; এটি যেকোনো ফিউচারের কালেকশনের সাথে ঠিকঠাক কাজ করে।)
স্ট্রীম: পর্যায়ক্রমিক ফিউচার (Futures in Sequence)
এই অধ্যায়ে এখন পর্যন্ত আমরা মূলত স্বতন্ত্র ফিউচার (individual futures) নিয়ে কাজ করেছি। এর একটি বড় ব্যতিক্রম ছিল আমরা যে অ্যাসিঙ্ক্রোনাস চ্যানেলটি ব্যবহার করেছি। মনে করুন, এই অধ্যায়ের শুরুতে ["Message Passing"][17-02-messages] বিভাগে আমরা আমাদের অ্যাসিঙ্ক্রোনাস চ্যানেলের রিসিভারটি কীভাবে ব্যবহার করেছি। অ্যাসিঙ্ক্রোনাস recv
মেথড সময়ের সাথে সাথে আইটেমের একটি ক্রম তৈরি করে। এটি একটি অনেক বেশি সাধারণ প্যাটার্নের উদাহরণ যা স্ট্রীম (stream) নামে পরিচিত।
আমরা চ্যাপ্টার ১৩-এ আইটেমের একটি ক্রম দেখেছিলাম, যখন আমরা [The Iterator Trait and the next
Method][iterator-trait] বিভাগে Iterator
ট্রেইট নিয়ে আলোচনা করেছিলাম, কিন্তু ইটারেটর এবং অ্যাসিঙ্ক্রোনাস চ্যানেল রিসিভারের মধ্যে দুটি পার্থক্য রয়েছে। প্রথম পার্থক্য হলো সময়: ইটারেটরগুলি সিঙ্ক্রোনাস, যেখানে চ্যানেল রিসিভার অ্যাসিঙ্ক্রোনাস। দ্বিতীয়টি হলো API। সরাসরি Iterator
-এর সাথে কাজ করার সময়, আমরা এর সিঙ্ক্রোনাস next
মেথড কল করি। বিশেষ করে trpl::Receiver
স্ট্রীমের সাথে, আমরা এর পরিবর্তে একটি অ্যাসিঙ্ক্রোনাস recv
মেথড কল করেছি। অন্যথায়, এই API-গুলি খুব অনুরূপ মনে হয়, এবং এই সাদৃশ্যটি কোনো কাকতালীয় ঘটনা নয়। একটি স্ট্রীম হলো ইটারেশনের একটি অ্যাসিঙ্ক্রোনাস রূপ। যেখানে trpl::Receiver
বিশেষভাবে বার্তা পাওয়ার জন্য অপেক্ষা করে, সেখানে সাধারণ-উদ্দেশ্যমূলক স্ট্রীম API অনেক বেশি বিস্তৃত: এটি Iterator
-এর মতো পরবর্তী আইটেম সরবরাহ করে, কিন্তু অ্যাসিঙ্ক্রোনাসভাবে।
রাস্টে ইটারেটর এবং স্ট্রীমের মধ্যে সাদৃশ্য থাকার মানে হলো আমরা আসলে যেকোনো ইটারেটর থেকে একটি স্ট্রীম তৈরি করতে পারি। একটি ইটারেটরের মতো, আমরা একটি স্ট্রীমের next
মেথড কল করে এবং তারপর আউটপুট await করে এর সাথে কাজ করতে পারি, যেমনটি লিস্টিং 17-30-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test
fn main() {
trpl::run(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
আমরা সংখ্যার একটি অ্যারে দিয়ে শুরু করি, যেটিকে আমরা একটি ইটারেটরে রূপান্তর করি এবং তারপর সমস্ত মান দ্বিগুণ করার জন্য map
কল করি। তারপর আমরা trpl::stream_from_iter
ফাংশন ব্যবহার করে ইটারেটরটিকে একটি স্ট্রীমে রূপান্তর করি। এরপর, আমরা while let
লুপ দিয়ে স্ট্রীমের আইটেমগুলির উপর লুপ করি যখন সেগুলি আসে।
দুর্ভাগ্যবশত, যখন আমরা কোডটি চালানোর চেষ্টা করি, এটি কম্পাইল হয় না, বরং এটি রিপোর্ট করে যে কোনো next
মেথড উপলব্ধ নেই:
error[E0599]: no method named `next` found for struct `Iter` in the current scope
--> src/main.rs:10:40
|
10 | while let Some(value) = stream.next().await {
| ^^^^
|
= note: the full type name has been written to 'file:///projects/async-await/target/debug/deps/async_await-575db3dd3197d257.long-type-14490787947592691573.txt'
= note: consider using `--verbose` to print the full type name to the console
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
|
1 + use crate::trpl::StreamExt;
|
1 + use futures_util::stream::stream::StreamExt;
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
help: there is a method `try_next` with a similar name
|
10 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~
এই আউটপুট যেমন ব্যাখ্যা করে, কম্পাইলার ত্রুটির কারণ হলো next
মেথড ব্যবহার করতে সক্ষম হওয়ার জন্য আমাদের সঠিক ট্রেইটটি স্কোপে (scope) প্রয়োজন। আমাদের এখন পর্যন্ত আলোচনার ভিত্তিতে, আপনি যুক্তিসঙ্গতভাবে আশা করতে পারেন যে সেই ট্রেইটটি হবে Stream
, কিন্তু এটি আসলে StreamExt
। এক্সটেনশন (extension)-এর সংক্ষিপ্ত রূপ, Ext
হলো রাস্ট কমিউনিটিতে একটি সাধারণ প্যাটার্ন একটি ট্রেইটকে অন্য একটি দিয়ে প্রসারিত করার জন্য।
আমরা অধ্যায়ের শেষে Stream
এবং StreamExt
ট্রেইটগুলি সম্পর্কে আরও কিছু বিস্তারিত ব্যাখ্যা করব, কিন্তু আপাতত আপনার যা জানা দরকার তা হলো Stream
ট্রেইট একটি নিম্ন-স্তরের ইন্টারফেস সংজ্ঞায়িত করে যা কার্যকরভাবে Iterator
এবং Future
ট্রেইটগুলিকে একত্রিত করে। StreamExt
Stream
-এর উপরে একটি উচ্চ-স্তরের API সেট সরবরাহ করে, যার মধ্যে next
মেথড এবং Iterator
ট্রেইট দ্বারা প্রদত্ত অন্যান্য ইউটিলিটি মেথডগুলির মতো অন্যান্য মেথডও রয়েছে। Stream
এবং StreamExt
এখনও রাস্টের স্ট্যান্ডার্ড লাইব্রেরির অংশ নয়, তবে বেশিরভাগ ইকোসিস্টেম ক্রেট একই সংজ্ঞা ব্যবহার করে।
কম্পাইলার ত্রুটির সমাধান হলো trpl::StreamExt
-এর জন্য একটি use
স্টেটমেন্ট যোগ করা, যেমনটি লিস্টিং 17-31-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use trpl::StreamExt; fn main() { trpl::run(async { let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let iter = values.iter().map(|n| n * 2); let mut stream = trpl::stream_from_iter(iter); while let Some(value) = stream.next().await { println!("The value was: {value}"); } }); }
এই সমস্ত অংশগুলি একসাথে রাখলে, এই কোডটি আমাদের ইচ্ছামতো কাজ করে! আরও কী, এখন যেহেতু আমাদের স্কোপে StreamExt
আছে, আমরা এর সমস্ত ইউটিলিটি মেথড ব্যবহার করতে পারি, ঠিক ইটারেটরের মতোই। উদাহরণস্বরূপ, লিস্টিং 17-32-এ, আমরা তিন এবং পাঁচের গুণিতক ছাড়া বাকি সবকিছু ফিল্টার করতে filter
মেথড ব্যবহার করি।
extern crate trpl; // required for mdbook test use trpl::StreamExt; fn main() { trpl::run(async { let values = 1..101; let iter = values.map(|n| n * 2); let stream = trpl::stream_from_iter(iter); let mut filtered = stream.filter(|value| value % 3 == 0 || value % 5 == 0); while let Some(value) = filtered.next().await { println!("The value was: {value}"); } }); }
অবশ্যই, এটি খুব আকর্ষণীয় নয়, যেহেতু আমরা সাধারণ ইটারেটর দিয়ে এবং কোনো async ছাড়াই একই কাজ করতে পারতাম। আসুন দেখি স্ট্রীমের জন্য অনন্য কী করা যায়।
স্ট্রীম কম্পোজ করা (Composing Streams)
অনেক ধারণা স্বাভাবিকভাবেই স্ট্রীম হিসাবে উপস্থাপিত হয়: একটি কিউ (queue)-তে আইটেম উপলব্ধ হওয়া, ফাইলসিস্টেম থেকে ডেটার খণ্ডাংশ ধীরে ধীরে আনা যখন পুরো ডেটা সেট কম্পিউটারের মেমরির জন্য খুব বড় হয়, অথবা সময়ের সাথে সাথে নেটওয়ার্কের মাধ্যমে ডেটা আসা। যেহেতু স্ট্রীমগুলি ফিউচার, আমরা সেগুলিকে অন্য যেকোনো ধরনের ফিউচারের সাথে ব্যবহার করতে পারি এবং সেগুলিকে আকর্ষণীয় উপায়ে একত্রিত করতে পারি। উদাহরণস্বরূপ, আমরা খুব বেশি নেটওয়ার্ক কল ট্রিগার করা এড়াতে ইভেন্টগুলিকে ব্যাচ করতে পারি, দীর্ঘ সময় ধরে চলা অপারেশনগুলির সিকোয়েন্সে টাইমআউট সেট করতে পারি, অথবা অপ্রয়োজনীয় কাজ করা এড়াতে ইউজার ইন্টারফেস ইভেন্টগুলিকে থ্রটল করতে পারি।
আসুন আমরা একটি ছোট বার্তা স্ট্রীম তৈরি করে শুরু করি যা ওয়েবসকেট বা অন্য কোনো রিয়েল-টাইম কমিউনিকেশন প্রোটোকল থেকে আমরা যে ডেটা স্ট্রীম দেখতে পারি তার একটি বিকল্প হিসাবে কাজ করবে, যেমনটি লিস্টিং 17-33-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = get_messages(); while let Some(message) = messages.next().await { println!("{message}"); } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for message in messages { tx.send(format!("Message: '{message}'")).unwrap(); } ReceiverStream::new(rx) }
প্রথমে, আমরা get_messages
নামে একটি ফাংশন তৈরি করি যা impl Stream<Item = String>
রিটার্ন করে। এর ইমপ্লিমেন্টেশনের জন্য, আমরা একটি async চ্যানেল তৈরি করি, ইংরেজি বর্ণমালার প্রথম ১০টি অক্ষরের উপর লুপ করি, এবং সেগুলি চ্যানেলের মাধ্যমে পাঠাই।
আমরা একটি নতুন টাইপও ব্যবহার করি: ReceiverStream
, যা trpl::channel
থেকে rx
রিসিভারকে next
মেথড সহ একটি Stream
-এ রূপান্তরিত করে। main
-এ ফিরে, আমরা স্ট্রীম থেকে সমস্ত বার্তা প্রিন্ট করতে একটি while let
লুপ ব্যবহার করি।
যখন আমরা এই কোডটি চালাই, আমরা ঠিক সেই ফলাফল পাই যা আমরা আশা করেছিলাম:
Message: 'a'
Message: 'b'
Message: 'c'
Message: 'd'
Message: 'e'
Message: 'f'
Message: 'g'
Message: 'h'
Message: 'i'
Message: 'j'
আবারও, আমরা এটি নিয়মিত Receiver
API বা এমনকি নিয়মিত Iterator
API দিয়েও করতে পারতাম, তাই আসুন এমন একটি ফিচার যোগ করি যার জন্য স্ট্রীম প্রয়োজন: স্ট্রীমের প্রতিটি আইটেমের জন্য একটি টাইমআউট যোগ করা, এবং আমরা যে আইটেমগুলি নির্গত করি সেগুলিতে একটি বিলম্ব যোগ করা, যেমনটি লিস্টিং 17-34-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = pin!(get_messages().timeout(Duration::from_millis(200))); while let Some(result) = messages.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for message in messages { tx.send(format!("Message: '{message}'")).unwrap(); } ReceiverStream::new(rx) }
আমরা timeout
মেথড দিয়ে স্ট্রীমে একটি টাইমআউট যোগ করে শুরু করি, যা StreamExt
ট্রেইট থেকে আসে। তারপর আমরা while let
লুপের বডি আপডেট করি, কারণ স্ট্রীমটি এখন একটি Result
রিটার্ন করে। Ok
ভ্যারিয়েন্টটি নির্দেশ করে যে একটি বার্তা সময়মতো এসেছে; Err
ভ্যারিয়েন্টটি নির্দেশ করে যে কোনো বার্তা আসার আগে টাইমআউট শেষ হয়ে গেছে। আমরা সেই ফলাফলের উপর match
করি এবং হয় সফলভাবে বার্তা পেলে সেটি প্রিন্ট করি অথবা টাইমআউট সম্পর্কে একটি বিজ্ঞপ্তি প্রিন্ট করি। অবশেষে, লক্ষ্য করুন যে আমরা টাইমআউট প্রয়োগ করার পরে বার্তাগুলি পিন করি, কারণ টাইমআউট হেল্পার একটি স্ট্রীম তৈরি করে যা পোল করার জন্য পিন করা প্রয়োজন।
তবে, বার্তাগুলির মধ্যে কোনো বিলম্ব না থাকায়, এই টাইমআউটটি প্রোগ্রামের আচরণ পরিবর্তন করে না। আসুন আমরা যে বার্তাগুলি পাঠাই সেগুলিতে একটি পরিবর্তনশীল বিলম্ব যোগ করি, যেমনটি লিস্টিং 17-35-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = pin!(get_messages().timeout(Duration::from_millis(200))); while let Some(result) = messages.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; tx.send(format!("Message: '{message}'")).unwrap(); } }); ReceiverStream::new(rx) }
get_messages
-এ, আমরা messages
অ্যারের সাথে enumerate
ইটারেটর মেথড ব্যবহার করি যাতে আমরা আইটেমটির সাথে আমরা যে প্রতিটি আইটেম পাঠাচ্ছি তার ইনডেক্স পেতে পারি। তারপর আমরা জোড়-ইনডেক্স আইটেমগুলিতে ১০০-মিলিসেকেন্ড বিলম্ব এবং বিজোড়-ইনডেক্স আইটেমগুলিতে ৩০০-মিলিসেকেন্ড বিলম্ব প্রয়োগ করি যাতে বাস্তব জগতে আমরা একটি বার্তা স্ট্রীম থেকে যে বিভিন্ন বিলম্ব দেখতে পারি তা অনুকরণ করা যায়। যেহেতু আমাদের টাইমআউট ২০০ মিলিসেকেন্ডের জন্য, এটি অর্ধেক বার্তাগুলিকে প্রভাবিত করা উচিত।
get_messages
ফাংশনে বার্তাগুলির মধ্যে স্লিপ করার জন্য ব্লকিং ছাড়াই, আমাদের async ব্যবহার করতে হবে। যাইহোক, আমরা get_messages
নিজেই একটি async ফাংশন করতে পারি না, কারণ তাহলে আমরা Stream<Item = String>>
-এর পরিবর্তে একটি Future<Output = Stream<Item = String>>
রিটার্ন করতাম। কলারকে স্ট্রীমে অ্যাক্সেস পেতে get_messages
নিজেই await করতে হতো। কিন্তু মনে রাখবেন: একটি নির্দিষ্ট ফিউচারের মধ্যে সবকিছু রৈখিকভাবে ঘটে; কনকারেন্সি ফিউচারগুলির মধ্যে ঘটে। get_messages
await করার জন্য রিসিভার স্ট্রীম রিটার্ন করার আগে প্রতিটি বার্তার মধ্যে স্লিপ বিলম্ব সহ সমস্ত বার্তা পাঠাতে হতো। ফলস্বরূপ, টাইমআউটটি অকেজো হয়ে যেত। স্ট্রীমে নিজেই কোনো বিলম্ব থাকত না; স্ট্রীমটি উপলব্ধ হওয়ার আগেই সেগুলি ঘটত।
পরিবর্তে, আমরা get_messages
-কে একটি নিয়মিত ফাংশন হিসাবে রেখে দিই যা একটি স্ট্রীম রিটার্ন করে, এবং আমরা async sleep
কলগুলি পরিচালনা করার জন্য একটি টাস্ক তৈরি করি।
দ্রষ্টব্য: এইভাবে
spawn_task
কল করা কাজ করে কারণ আমরা ইতিমধ্যে আমাদের রানটাইম সেট আপ করেছি; যদি আমরা না করতাম, এটি একটি প্যানিক (panic) সৃষ্টি করত। অন্যান্য ইমপ্লিমেন্টেশনগুলি বিভিন্ন ট্রেড-অফ বেছে নেয়: তারা একটি নতুন রানটাইম তৈরি করতে পারে এবং প্যানিক এড়াতে পারে কিন্তু সামান্য অতিরিক্ত ওভারহেডের সাথে শেষ হতে পারে, অথবা তারা রানটাইমের রেফারেন্স ছাড়াই টাস্ক তৈরি করার জন্য একটি স্বতন্ত্র উপায় সরবরাহ নাও করতে পারে। নিশ্চিত করুন যে আপনি জানেন আপনার রানটাইম কোন ট্রেড-অফ বেছে নিয়েছে এবং সেই অনুযায়ী আপনার কোড লিখুন!
এখন আমাদের কোডের একটি অনেক বেশি আকর্ষণীয় ফলাফল রয়েছে। প্রতি দুটি বার্তার মধ্যে, একটি Problem: Elapsed(())
ত্রুটি দেখা যায়।
Message: 'a'
Problem: Elapsed(())
Message: 'b'
Message: 'c'
Problem: Elapsed(())
Message: 'd'
Message: 'e'
Problem: Elapsed(())
Message: 'f'
Message: 'g'
Problem: Elapsed(())
Message: 'h'
Message: 'i'
Problem: Elapsed(())
Message: 'j'
টাইমআউটটি শেষ পর্যন্ত বার্তাগুলিকে আসতে বাধা দেয় না। আমরা এখনও সমস্ত মূল বার্তা পাই, কারণ আমাদের চ্যানেলটি আনবাউন্ডেড (unbounded): এটি মেমরিতে যতগুলি বার্তা ফিট করতে পারে ততগুলি ধরে রাখতে পারে। যদি বার্তাটি টাইমআউটের আগে না আসে, আমাদের স্ট্রীম হ্যান্ডলার সেটির হিসাব রাখবে, কিন্তু যখন এটি আবার স্ট্রীমটি পোল করবে, তখন বার্তাটি এখন এসে থাকতে পারে।
প্রয়োজনে আপনি অন্যান্য ধরনের চ্যানেল বা সাধারণভাবে অন্যান্য ধরনের স্ট্রীম ব্যবহার করে ভিন্ন আচরণ পেতে পারেন। আসুন সেগুলির মধ্যে একটি বাস্তবে দেখি সময় ব্যবধানের একটি স্ট্রীমকে এই বার্তাগুলির স্ট্রীমের সাথে একত্রিত করে।
স্ট্রীম মার্জ করা (Merging Streams)
প্রথমে, আসুন আরেকটি স্ট্রীম তৈরি করি, যা সরাসরি চালালে প্রতি মিলিসেকেন্ডে একটি আইটেম নির্গত করবে। সরলতার জন্য, আমরা একটি বিলম্ব সহ একটি বার্তা পাঠাতে sleep
ফাংশন ব্যবহার করতে পারি এবং এটিকে get_messages
-এ ব্যবহৃত একই পদ্ধতির সাথে একত্রিত করতে পারি যেখানে একটি চ্যানেল থেকে একটি স্ট্রীম তৈরি করা হয়েছিল। পার্থক্য হলো এইবার, আমরা অতিবাহিত ব্যবধানের সংখ্যা ফেরত পাঠাতে যাচ্ছি, তাই রিটার্ন টাইপ হবে impl Stream<Item = u32>
, এবং আমরা ফাংশনটিকে get_intervals
বলতে পারি (দেখুন লিস্টিং 17-36)।
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let mut messages = pin!(get_messages().timeout(Duration::from_millis(200))); while let Some(result) = messages.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; tx.send(format!("Message: '{message}'")).unwrap(); } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let mut count = 0; loop { trpl::sleep(Duration::from_millis(1)).await; count += 1; tx.send(count).unwrap(); } }); ReceiverStream::new(rx) }
আমরা টাস্কের মধ্যে একটি count
সংজ্ঞায়িত করে শুরু করি। (আমরা এটিকে টাস্কের বাইরেও সংজ্ঞায়িত করতে পারতাম, তবে যেকোনো প্রদত্ত ভেরিয়েবলের স্কোপ সীমিত রাখা পরিষ্কার।) তারপর আমরা একটি অসীম লুপ তৈরি করি। লুপের প্রতিটি ইটারেশন অ্যাসিঙ্ক্রোনাসভাবে এক মিলিসেকেন্ড ঘুমায়, কাউন্ট বৃদ্ধি করে, এবং তারপর এটি চ্যানেলের মাধ্যমে পাঠায়। যেহেতু এই সবকিছুই spawn_task
দ্বারা তৈরি করা টাস্কের মধ্যে মোড়ানো, তাই এর সবকিছু—অসীম লুপ সহ—রানটাইমের সাথে পরিষ্কার হয়ে যাবে।
এই ধরনের অসীম লুপ, যা কেবল তখনই শেষ হয় যখন পুরো রানটাইমটি ভেঙে যায়, async রাস্টে বেশ সাধারণ: অনেক প্রোগ্রামের অনির্দিষ্টকালের জন্য চলতে থাকতে হয়। async-এর সাথে, এটি অন্য কিছু ব্লক করে না, যতক্ষণ না লুপের প্রতিটি ইটারেশনে অন্তত একটি await পয়েন্ট থাকে।
এখন, আমাদের মূল ফাংশনের async ব্লকে ফিরে, আমরা messages
এবং intervals
স্ট্রীমগুলিকে মার্জ করার চেষ্টা করতে পারি, যেমনটি লিস্টিং 17-37-এ দেখানো হয়েছে।
extern crate trpl; // required for mdbook test
use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};
fn main() {
trpl::run(async {
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals();
let merged = messages.merge(intervals);
while let Some(result) = merged.next().await {
match result {
Ok(message) => println!("{message}"),
Err(reason) => eprintln!("Problem: {reason:?}"),
}
}
})
}
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
tx.send(format!("Message: '{message}'")).unwrap();
}
});
ReceiverStream::new(rx)
}
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});
ReceiverStream::new(rx)
}
আমরা get_intervals
কল করে শুরু করি। তারপর আমরা messages
এবং intervals
স্ট্রীমগুলিকে merge
মেথড দিয়ে মার্জ করি, যা একাধিক স্ট্রীমকে একটি স্ট্রীমে একত্রিত করে যা যেকোনো সোর্স স্ট্রীম থেকে আইটেমগুলি উপলব্ধ হওয়ার সাথে সাথেই তৈরি করে, কোনো নির্দিষ্ট ক্রম আরোপ না করে। অবশেষে, আমরা messages
-এর পরিবর্তে সেই একত্রিত স্ট্রীমের উপর লুপ করি।
এই মুহুর্তে, messages
বা intervals
কোনোটিকেই পিন বা মিউটেবল (mutable) করার প্রয়োজন নেই, কারণ উভয়ই একক merged
স্ট্রীমে একত্রিত হবে। যাইহোক, merge
-এর এই কলটি কম্পাইল হয় না! (while let
লুপে next
কলটিও হয় না, তবে আমরা সেটিতে পরে ফিরে আসব।) এর কারণ হলো দুটি স্ট্রীমের বিভিন্ন টাইপ রয়েছে। messages
স্ট্রীমটির টাইপ Timeout<impl Stream<Item = String>>
, যেখানে Timeout
হলো সেই টাইপ যা একটি timeout
কলের জন্য Stream
ইমপ্লিমেন্ট করে। intervals
স্ট্রীমটির টাইপ impl Stream<Item = u32>
। এই দুটি স্ট্রীমকে মার্জ করতে, আমাদের একটিকে অন্যটির সাথে মেলানোর জন্য রূপান্তর করতে হবে। আমরা intervals
স্ট্রীমটি পুনরায় কাজ করব, কারণ messages
ইতিমধ্যে আমাদের কাঙ্ক্ষিত মৌলিক বিন্যাসে রয়েছে এবং টাইমআউট ত্রুটিগুলি পরিচালনা করতে হবে (দেখুন লিস্টিং 17-38)।
extern crate trpl; // required for mdbook test
use std::{pin::pin, time::Duration};
use trpl::{ReceiverStream, Stream, StreamExt};
fn main() {
trpl::run(async {
let messages = get_messages().timeout(Duration::from_millis(200));
let intervals = get_intervals()
.map(|count| format!("Interval: {count}"))
.timeout(Duration::from_secs(10));
let merged = messages.merge(intervals);
let mut stream = pin!(merged);
while let Some(result) = stream.next().await {
match result {
Ok(message) => println!("{message}"),
Err(reason) => eprintln!("Problem: {reason:?}"),
}
}
})
}
fn get_messages() -> impl Stream<Item = String> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
for (index, message) in messages.into_iter().enumerate() {
let time_to_sleep = if index % 2 == 0 { 100 } else { 300 };
trpl::sleep(Duration::from_millis(time_to_sleep)).await;
tx.send(format!("Message: '{message}'")).unwrap();
}
});
ReceiverStream::new(rx)
}
fn get_intervals() -> impl Stream<Item = u32> {
let (tx, rx) = trpl::channel();
trpl::spawn_task(async move {
let mut count = 0;
loop {
trpl::sleep(Duration::from_millis(1)).await;
count += 1;
tx.send(count).unwrap();
}
});
ReceiverStream::new(rx)
}
প্রথমত, আমরা intervals
-কে একটি স্ট্রিং-এ রূপান্তর করতে map
হেল্পার মেথড ব্যবহার করতে পারি। দ্বিতীয়ত, আমাদের messages
থেকে Timeout
-এর সাথে মেলাতে হবে। যেহেতু আমরা আসলে intervals
-এর জন্য কোনো টাইমআউট চাই না, তাই আমরা কেবল একটি টাইমআউট তৈরি করতে পারি যা আমাদের ব্যবহার করা অন্যান্য সময়কালের চেয়ে দীর্ঘ। এখানে, আমরা Duration::from_secs(10)
দিয়ে একটি ১০-সেকেন্ডের টাইমআউট তৈরি করি। অবশেষে, আমাদের stream
-কে মিউটেবল করতে হবে, যাতে while let
লুপের next
কলগুলি স্ট্রীমের মধ্য দিয়ে ইটারেট করতে পারে, এবং এটিকে পিন করতে হবে যাতে এটি করা নিরাপদ হয়। এটি আমাদের প্রায় যেখানে পৌঁছানো দরকার সেখানে নিয়ে যায়। সবকিছু টাইপ চেক করে। তবে আপনি যদি এটি চালান, দুটি সমস্যা হবে। প্রথমত, এটি কখনই থামবে না! আপনাকে ctrl-c দিয়ে এটি থামাতে হবে। দ্বিতীয়ত, ইংরেজি বর্ণমালার বার্তাগুলি সমস্ত ব্যবধান কাউন্টার বার্তাগুলির মধ্যে চাপা পড়ে যাবে:
--snip--
Interval: 38
Interval: 39
Interval: 40
Message: 'a'
Interval: 41
Interval: 42
Interval: 43
--snip--
লিস্টিং 17-39 এই শেষ দুটি সমস্যার সমাধান করার একটি উপায় দেখায়।
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval: {count}")) .throttle(Duration::from_millis(100)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(message) => println!("{message}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }) } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; tx.send(format!("Message: '{message}'")).unwrap(); } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let mut count = 0; loop { trpl::sleep(Duration::from_millis(1)).await; count += 1; tx.send(count).unwrap(); } }); ReceiverStream::new(rx) }
প্রথমে, আমরা intervals
স্ট্রীমে throttle
মেথড ব্যবহার করি যাতে এটি messages
স্ট্রীমকে অভিভূত না করে। থ্রটলিং (throttling) হলো একটি ফাংশন কত ঘন ঘন কল করা হবে—অথবা, এই ক্ষেত্রে, স্ট্রীমটি কত ঘন ঘন পোল করা হবে—তা সীমিত করার একটি উপায়। প্রতি ১০০ মিলিসেকেন্ডে একবার করলেই হবে, কারণ আমাদের বার্তাগুলি প্রায় তত ঘন ঘনই আসে।
আমরা একটি স্ট্রীম থেকে কতগুলি আইটেম গ্রহণ করব তা সীমিত করতে, আমরা merged
স্ট্রীমে take
মেথড প্রয়োগ করি, কারণ আমরা কেবল একটি স্ট্রীম বা অন্যটিকে নয়, চূড়ান্ত আউটপুট সীমিত করতে চাই।
এখন যখন আমরা প্রোগ্রামটি চালাই, এটি স্ট্রীম থেকে ২০টি আইটেম নেওয়ার পরে থেমে যায়, এবং ব্যবধানগুলি বার্তাগুলিকে অভিভূত করে না। আমরা Interval: 100
বা Interval: 200
ইত্যাদিও পাই না, বরং Interval: 1
, Interval: 2
, ইত্যাদি পাই—যদিও আমাদের কাছে একটি সোর্স স্ট্রীম আছে যা প্রতি মিলিসেকেন্ডে একটি ইভেন্ট তৈরি করতে পারে। এর কারণ হলো throttle
কল একটি নতুন স্ট্রীম তৈরি করে যা মূল স্ট্রীমটিকে র্যাপ করে যাতে মূল স্ট্রীমটি কেবল থ্রটল হারে পোল করা হয়, তার নিজস্ব "নেটিভ" হারে নয়। আমাদের কাছে একগুচ্ছ অব্যবহৃত ব্যবধান বার্তা নেই যা আমরা উপেক্ষা করার সিদ্ধান্ত নিচ্ছি। পরিবর্তে, আমরা প্রথম স্থানে সেই ব্যবধান বার্তাগুলি তৈরিই করি না! এটি রাস্টের ফিউচারের অন্তর্নিহিত "অলসতা" আবার কাজে লাগছে, যা আমাদের পারফরম্যান্স বৈশিষ্ট্যগুলি বেছে নেওয়ার সুযোগ দেয়।
Interval: 1
Message: 'a'
Interval: 2
Interval: 3
Problem: Elapsed(())
Interval: 4
Message: 'b'
Interval: 5
Message: 'c'
Interval: 6
Interval: 7
Problem: Elapsed(())
Interval: 8
Message: 'd'
Interval: 9
Message: 'e'
Interval: 10
Interval: 11
Problem: Elapsed(())
Interval: 12
আমাদের শেষ একটি জিনিস সামলাতে হবে: ত্রুটি! এই দুটি চ্যানেল-ভিত্তিক স্ট্রীমের সাথে, চ্যানেলের অন্য দিকটি বন্ধ হয়ে গেলে send
কলগুলি ব্যর্থ হতে পারে—এবং এটি কেবল রানটাইম কীভাবে স্ট্রীম গঠনকারী ফিউচারগুলি চালায় তার উপর নির্ভর করে। এখন পর্যন্ত, আমরা unwrap
কল করে এই সম্ভাবনাটি উপেক্ষা করেছি, কিন্তু একটি সুশৃঙ্খল অ্যাপে, আমাদের স্পষ্টভাবে ত্রুটিটি পরিচালনা করা উচিত, ন্যূনতমভাবে লুপটি শেষ করে যাতে আমরা আর কোনো বার্তা পাঠানোর চেষ্টা না করি। লিস্টিং 17-40 একটি সাধারণ ত্রুটি কৌশল দেখায়: সমস্যাটি প্রিন্ট করা এবং তারপর লুপগুলি থেকে break
করা।
extern crate trpl; // required for mdbook test use std::{pin::pin, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval #{count}")) .throttle(Duration::from_millis(500)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(item) => println!("{item}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; if let Err(send_error) = tx.send(format!("Message: '{message}'")) { eprintln!("Cannot send message '{message}': {send_error}"); break; } } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let mut count = 0; loop { trpl::sleep(Duration::from_millis(1)).await; count += 1; if let Err(send_error) = tx.send(count) { eprintln!("Could not send interval {count}: {send_error}"); break; }; } }); ReceiverStream::new(rx) }``` </Listing> যথারীতি, একটি বার্তা পাঠানোর ত্রুটি পরিচালনা করার সঠিক উপায় পরিবর্তিত হবে; শুধু নিশ্চিত করুন যে আপনার একটি কৌশল আছে। এখন যেহেতু আমরা বাস্তবে অনেক async দেখেছি, আসুন এক ধাপ পিছিয়ে গিয়ে `Future`, `Stream`, এবং রাস্ট async কাজ করানোর জন্য যে অন্যান্য মূল ট্রেইটগুলি ব্যবহার করে সেগুলির কিছু বিশদ বিবরণে যাই। [17-02-messages]: ch17-02-concurrency-with-async.html#message-passing [iterator-trait]: ch13-02-iterators.html#the-iterator-trait-and-the-next-method
অ্যাসিঙ্ক (Async)-এর জন্য ব্যবহৃত ট্রেইটগুলির একটি নিবিড় পর্যবেক্ষণ
অধ্যায় জুড়ে, আমরা বিভিন্ন উপায়ে Future
, Pin
, Unpin
, Stream
, এবং StreamExt
ট্রেইটগুলি ব্যবহার করেছি। এখন পর্যন্ত, আমরা এগুলি কীভাবে কাজ করে বা কীভাবে একসাথে খাপ খায় তার বিস্তারিত বিবরণে খুব বেশি যাইনি, যা আপনার দৈনন্দিন রাস্ট কোডিংয়ের জন্য বেশিরভাগ সময় ঠিক আছে। তবে কখনও কখনও, আপনি এমন পরিস্থিতির মুখোমুখি হবেন যেখানে আপনাকে এই বিবরণগুলির আরও কয়েকটি বুঝতে হবে। এই বিভাগে, আমরা সেই পরিস্থিতিগুলিতে সাহায্য করার জন্য যথেষ্ট গভীরে যাব, তবে সত্যিকারের গভীর আলোচনা অন্যান্য ডকুমেন্টেশনের জন্য রেখে দেব।
Future
ট্রেইট
আসুন Future
ট্রেইটটি কীভাবে কাজ করে তা আরও নিবিড়ভাবে দেখে শুরু করি। রাস্ট এটি যেভাবে সংজ্ঞায়িত করে তা এখানে দেওয়া হলো:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
এই ট্রেইট সংজ্ঞায় একগুচ্ছ নতুন টাইপ এবং এমন কিছু সিনট্যাক্স রয়েছে যা আমরা আগে দেখিনি, তাই আসুন ধাপে ধাপে সংজ্ঞাটি পর্যালোচনা করি।
প্রথমত, Future
-এর অ্যাসোসিয়েটেড টাইপ Output
বলে যে ফিউচারটি কিসে রিজলভ (resolve) হবে। এটি Iterator
ট্রেইটের Item
অ্যাসোসিয়েটেড টাইপের অনুরূপ। দ্বিতীয়ত, Future
-এর poll
মেথডও রয়েছে, যা এর self
প্যারামিটারের জন্য একটি বিশেষ Pin
রেফারেন্স এবং একটি Context
টাইপের মিউটেবল রেফারেন্স নেয়, এবং একটি Poll<Self::Output>
রিটার্ন করে। আমরা কিছুক্ষণ পরে Pin
এবং Context
সম্পর্কে আরও কথা বলব। আপাতত, আসুন মেথডটি যা রিটার্ন করে, সেই Poll
টাইপের উপর মনোযোগ দিই:
#![allow(unused)] fn main() { enum Poll<T> { Ready(T), Pending, } }
এই Poll
টাইপটি একটি Option
-এর মতো। এর একটি ভ্যারিয়েন্ট রয়েছে যার একটি মান আছে, Ready(T)
, এবং একটি যার নেই, Pending
। তবে Poll
-এর অর্থ Option
থেকে বেশ ভিন্ন! Pending
ভ্যারিয়েন্টটি নির্দেশ করে যে ফিউচারের এখনও কাজ বাকি আছে, তাই কলারকে পরে আবার পরীক্ষা করতে হবে। Ready
ভ্যারিয়েন্টটি নির্দেশ করে যে ফিউচারটি তার কাজ শেষ করেছে এবং T
মানটি উপলব্ধ।
দ্রষ্টব্য: বেশিরভাগ ফিউচারের ক্ষেত্রে, ফিউচারটি
Ready
রিটার্ন করার পরে কলারের আবারpoll
কল করা উচিত নয়। অনেক ফিউচার রেডি (ready) হওয়ার পরে আবার পোল করা হলে প্যানিক (panic) করবে। যে ফিউচারগুলি আবার পোল করা নিরাপদ, সেগুলি তাদের ডকুমেন্টেশনে স্পষ্টভাবে উল্লেখ করবে। এটিIterator::next
যেভাবে আচরণ করে তার অনুরূপ।
যখন আপনি await
ব্যবহার করে কোড দেখেন, তখন রাস্ট পর্দার আড়ালে এটিকে poll
কল করে এমন কোডে কম্পাইল করে। আপনি যদি লিস্টিং ১৭-৪-এ ফিরে তাকান, যেখানে আমরা একটি একক URL-এর পেজ টাইটেল রিজলভ হওয়ার পরে প্রিন্ট করেছিলাম, রাস্ট এটিকে প্রায় (যদিও ঠিক নয়) এইরকম কিছুতে কম্পাইল করে:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// কিন্তু এখানে কী হবে?
}
}
ফিউচারটি যখন এখনও Pending
থাকে তখন আমাদের কী করা উচিত? আমাদের আবার, এবং আবার, এবং আবার চেষ্টা করার কোনো উপায় দরকার, যতক্ষণ না ফিউচারটি অবশেষে রেডি হয়। অন্য কথায়, আমাদের একটি লুপ দরকার:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
তবে যদি রাস্ট এটিকে ঠিক সেই কোডে কম্পাইল করত, তবে প্রতিটি await
ব্লকিং হয়ে যেত—আমরা যা চেয়েছিলাম তার ঠিক বিপরীত! পরিবর্তে, রাস্ট নিশ্চিত করে যে লুপটি এমন কিছুর কাছে নিয়ন্ত্রণ হস্তান্তর করতে পারে যা এই ফিউচারের কাজ পজ (pause) করে অন্য ফিউচারগুলিতে কাজ করতে পারে এবং তারপরে এটিকে আবার পরীক্ষা করতে পারে। যেমন আমরা দেখেছি, সেই কিছু হলো একটি async runtime, এবং এই সময়সূচী এবং সমন্বয়ের কাজটি এর অন্যতম প্রধান কাজ।
অধ্যায়ের শুরুতে, আমরা rx.recv
-এর জন্য অপেক্ষা করার বর্ণনা দিয়েছিলাম। recv
কল একটি ফিউচার রিটার্ন করে, এবং ফিউচারটি await করা এটিকে পোল করে। আমরা উল্লেখ করেছি যে একটি রানটাইম ফিউচারটিকে পজ করবে যতক্ষণ না এটি Some(message)
বা চ্যানেল বন্ধ হয়ে গেলে None
-এর সাথে রেডি হয়। Future
ট্রেইট এবং বিশেষ করে Future::poll
সম্পর্কে আমাদের গভীর বোঝার সাথে, আমরা দেখতে পাচ্ছি এটি কীভাবে কাজ করে। রানটাইম জানে যে ফিউচারটি রেডি নয় যখন এটি Poll::Pending
রিটার্ন করে। বিপরীতভাবে, রানটাইম জানে যে ফিউচারটি রেডি এবং poll
যখন Poll::Ready(Some(message))
বা Poll::Ready(None)
রিটার্ন করে তখন এটিকে এগিয়ে নিয়ে যায়।
একটি রানটাইম কীভাবে এটি করে তার সঠিক বিবরণ এই বইয়ের সুযোগের বাইরে, কিন্তু মূল বিষয় হলো ফিউচারের মৌলিক মেকানিক্স দেখা: একটি রানটাইম তার দায়িত্বে থাকা প্রতিটি ফিউচারকে পোল করে, যখন এটি এখনও রেডি না হয় তখন ফিউচারটিকে ঘুমাতে পাঠায়।
Pin
এবং Unpin
ট্রেইট
যখন আমরা লিস্টিং ১৭-১৬-এ পিনিং (pinning)-এর ধারণাটি উপস্থাপন করেছিলাম, তখন আমরা একটি খুব জটিল ত্রুটি বার্তার সম্মুখীন হয়েছিলাম। এখানে এর প্রাসঙ্গিক অংশটি আবার দেওয়া হলো:
error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
এই ত্রুটি বার্তাটি আমাদের কেবল এটিই বলে না যে আমাদের মানগুলি পিন করতে হবে, বরং পিনিং কেন প্রয়োজন তাও বলে। trpl::join_all
ফাংশনটি JoinAll
নামে একটি struct রিটার্ন করে। সেই struct-টি F
টাইপের উপর জেনেরিক, যা Future
ট্রেইট ইমপ্লিমেন্ট করার জন্য সীমাবদ্ধ। await
দিয়ে সরাসরি একটি ফিউচার await করা ফিউচারটিকে অন্তর্নিহিতভাবে পিন করে। একারণে আমাদের যেখানেই ফিউচার await করতে চাই সেখানে pin!
ব্যবহার করার প্রয়োজন হয় না।
যাইহোক, আমরা এখানে সরাসরি একটি ফিউচার await করছি না। পরিবর্তে, আমরা join_all
ফাংশনে ফিউচারের একটি কালেকশন পাস করে একটি নতুন ফিউচার, JoinAll
তৈরি করছি। join_all
-এর সিগনেচার প্রয়োজন করে যে কালেকশনের আইটেমগুলির টাইপগুলি সবই Future
ট্রেইট ইমপ্লিমেন্ট করে, এবং Box<T>
কেবল তখনই Future
ইমপ্লিমেন্ট করে যদি এটি যে T
-কে র্যাপ (wrap) করে তা Unpin
ট্রেইট ইমপ্লিমেন্ট করা একটি ফিউচার হয়।
এটি হজম করার জন্য অনেক কিছু! এটি সত্যিই বুঝতে হলে, আসুন Future
ট্রেইটটি আসলে কীভাবে কাজ করে, বিশেষ করে পিনিং এর আশেপাশে, সে সম্পর্কে আরও গভীরে যাই।
Future
ট্রেইটের সংজ্ঞায় আবার দেখুন:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; // Required method fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
cx
প্যারামিটার এবং এর Context
টাইপ হলো একটি রানটাইম কীভাবে অলস থেকেও যেকোনো প্রদত্ত ফিউচার কখন পরীক্ষা করতে হবে তা জানে তার চাবিকাঠি। আবারও, এটি কীভাবে কাজ করে তার বিবরণ এই অধ্যায়ের সুযোগের বাইরে, এবং আপনাকে সাধারণত কেবল একটি কাস্টম Future
ইমপ্লিমেন্টেশন লেখার সময় এটি নিয়ে ভাবতে হবে। আমরা পরিবর্তে self
-এর জন্য টাইপের উপর মনোযোগ দেব, কারণ এটিই প্রথমবার যখন আমরা এমন একটি মেথড দেখেছি যেখানে self
-এর একটি টাইপ অ্যানোটেশন রয়েছে। self
-এর জন্য একটি টাইপ অ্যানোটেশন অন্যান্য ফাংশন প্যারামিটারের জন্য টাইপ অ্যানোটেশনের মতোই কাজ করে, তবে দুটি মূল পার্থক্য রয়েছে:
- এটি রাস্টকে বলে যে মেথডটি কল করার জন্য
self
কোন টাইপের হতে হবে। - এটি যেকোনো টাইপ হতে পারে না। এটি যে টাইপের উপর মেথডটি ইমপ্লিমেন্ট করা হয়েছে, সেই টাইপের একটি রেফারেন্স বা স্মার্ট পয়েন্টার, বা সেই টাইপের একটি রেফারেন্সকে র্যাপ করা একটি
Pin
-এর মধ্যে সীমাবদ্ধ।
আমরা চ্যাপ্টার ১৮-এ এই সিনট্যাক্স সম্পর্কে আরও দেখব। আপাতত, এটি জানাই যথেষ্ট যে যদি আমরা একটি ফিউচার পোল করতে চাই এটি Pending
নাকি Ready(Output)
তা পরীক্ষা করার জন্য, আমাদের টাইপের একটি Pin
-র্যাপ করা মিউটেবল রেফারেন্স প্রয়োজন।
Pin
হলো &
, &mut
, Box
, এবং Rc
-এর মতো পয়েন্টার-সদৃশ টাইপের জন্য একটি র্যাপার। (টেকনিক্যালি, Pin
Deref
বা DerefMut
ট্রেইট ইমপ্লিমেন্ট করা টাইপের সাথে কাজ করে, তবে এটি কার্যকরভাবে কেবল পয়েন্টারের সাথে কাজ করার সমতুল্য।) Pin
নিজে কোনো পয়েন্টার নয় এবং এর নিজস্ব কোনো আচরণ নেই যেমন Rc
এবং Arc
-এর রেফারেন্স কাউন্টিংয়ের সাথে আছে; এটি সম্পূর্ণরূপে একটি টুল যা কম্পাইলার পয়েন্টার ব্যবহারের উপর সীমাবদ্ধতা আরোপ করতে ব্যবহার করতে পারে।
await
-কে poll
কলের মাধ্যমে ইমপ্লিমেন্ট করা হয়েছে মনে করলে, আমরা আগে যে ত্রুটি বার্তাটি দেখেছিলাম তা ব্যাখ্যা করা শুরু হয়, কিন্তু সেটি ছিল Unpin
-এর ক্ষেত্রে, Pin
-এর নয়। তাহলে Pin
কীভাবে Unpin
-এর সাথে সম্পর্কিত, এবং poll
কল করার জন্য Future
-এর self
-কে একটি Pin
টাইপের মধ্যে থাকতে হবে কেন?
এই অধ্যায়ের শুরুতে মনে করুন, একটি ফিউচারের মধ্যে একাধিক await পয়েন্ট একটি স্টেট মেশিনে কম্পাইল হয়, এবং কম্পাইলার নিশ্চিত করে যে সেই স্টেট মেশিনটি রাস্টের স্বাভাবিক নিরাপত্তা নিয়মগুলি, যার মধ্যে borrowing এবং ownership রয়েছে, অনুসরণ করে। এটি কাজ করানোর জন্য, রাস্ট দেখে যে একটি await পয়েন্ট এবং পরবর্তী await পয়েন্ট বা async ব্লকের শেষের মধ্যে কোন ডেটা প্রয়োজন। তারপরে এটি কম্পাইল করা স্টেট মেশিনে একটি সংশ্লিষ্ট ভ্যারিয়েন্ট তৈরি করে। প্রতিটি ভ্যারিয়েন্ট সোর্স কোডের সেই বিভাগে ব্যবহৃত ডেটাতে প্রয়োজনীয় অ্যাক্সেস পায়, হয় সেই ডেটার মালিকানা নিয়ে অথবা এর একটি মিউটেবল বা অপরিবর্তনীয় রেফারেন্স পেয়ে।
এখন পর্যন্ত, সব ঠিক আছে: যদি আমরা একটি নির্দিষ্ট async ব্লকে মালিকানা বা রেফারেন্স সম্পর্কে কোনো ভুল করি, borrow checker আমাদের বলে দেবে। যখন আমরা সেই ব্লকের সাথে সঙ্গতিপূর্ণ ফিউচারটি সরাতে চাই—যেমন join_all
-এ পাস করার জন্য এটিকে একটি Vec
-এ সরানো—তখন জিনিসগুলি আরও জটিল হয়ে যায়।
যখন আমরা একটি ফিউচার সরাই—সেটি join_all
-এর সাথে ইটারেটর হিসাবে ব্যবহার করার জন্য একটি ডেটা স্ট্রাকচারে পুশ করে হোক বা একটি ফাংশন থেকে এটি রিটার্ন করে হোক—এর অর্থ আসলে রাস্ট আমাদের জন্য যে স্টেট মেশিন তৈরি করে তা সরানো। এবং রাস্টের বেশিরভাগ অন্যান্য টাইপের থেকে ভিন্ন, async ব্লকের জন্য রাস্ট যে ফিউচারগুলি তৈরি করে সেগুলি যেকোনো প্রদত্ত ভ্যারিয়েন্টের ফিল্ডে নিজেদের রেফারেন্স দিয়ে শেষ হতে পারে, যেমনটি চিত্র ১৭-৪-এর সরলীকৃত চিত্রে দেখানো হয়েছে।
ডিফল্টরূপে, যে কোনো অবজেক্ট যার নিজের কাছে একটি রেফারেন্স আছে তা সরানো অনিরাপদ, কারণ রেফারেন্সগুলি সর্বদা তাদের উল্লেখ করা জিনিসের আসল মেমরি ঠিকানায় নির্দেশ করে (চিত্র ১৭-৫ দেখুন)। আপনি যদি ডেটা স্ট্রাকচারটি নিজেই সরান, তবে সেই অভ্যন্তরীণ রেফারেন্সগুলি পুরানো অবস্থানে নির্দেশ করতে থাকবে। যাইহোক, সেই মেমরি অবস্থানটি এখন অবৈধ। একটি কারণ হলো, আপনি ডেটা স্ট্রাকচারে পরিবর্তন করলে এর মান আপডেট হবে না। আরেকটি—আরও গুরুত্বপূর্ণ—কারণ হলো, কম্পিউটার এখন সেই মেমরিটি অন্যান্য উদ্দেশ্যে পুনরায় ব্যবহার করতে স্বাধীন! আপনি পরে সম্পূর্ণ সম্পর্কহীন ডেটা পড়তে পারেন।
তাত্ত্বিকভাবে, রাস্ট কম্পাইলার যখনই কোনো অবজেক্ট সরানো হয় তখন সেটির প্রতিটি রেফারেন্স আপডেট করার চেষ্টা করতে পারত, কিন্তু এটি অনেক পারফরম্যান্স ওভারহেড যোগ করতে পারত, বিশেষ করে যদি রেফারেন্সের পুরো একটি জাল আপডেট করার প্রয়োজন হয়। যদি আমরা পরিবর্তে নিশ্চিত করতে পারতাম যে প্রশ্নবিদ্ধ ডেটা স্ট্রাকচারটি মেমরিতে নড়াচড়া করে না, তাহলে আমাদের কোনো রেফারেন্স আপডেট করতে হতো না। রাস্টের borrow checker ঠিক এটাই প্রয়োজন করে: নিরাপদ কোডে, এটি আপনাকে এমন কোনো আইটেম সরাতে বাধা দেয় যার কাছে একটি সক্রিয় রেফারেন্স রয়েছে।
Pin
এর উপর ভিত্তি করে আমাদের ঠিক সেই গ্যারান্টি দেয় যা আমাদের প্রয়োজন। যখন আমরা একটি মানকে পিন করি সেই মানের একটি পয়েন্টারকে Pin
-এ র্যাপ করে, তখন এটি আর নড়াচড়া করতে পারে না। সুতরাং, যদি আপনার Pin<Box<SomeType>>
থাকে, আপনি আসলে SomeType
মানটি পিন করেন, Box
পয়েন্টারটি নয়। চিত্র ১৭-৬ এই প্রক্রিয়াটি চিত্রিত করে।
<img alt="পাশাপাশি রাখা তিনটি বক্স। প্রথমটির লেবেল "Pin", দ্বিতীয়টির "b1", এবং তৃতীয়টির "pinned"। "pinned"-এর মধ্যে একটি টেবিল রয়েছে যার লেবেল "fut", একটি একক কলাম সহ; এটি ডেটা স্ট্রাকচারের প্রতিটি অংশের জন্য সেল সহ একটি ফিউচার প্রতিনিধিত্ব করে। এর প্রথম সেলে "0" মান রয়েছে, এর দ্বিতীয় সেল থেকে একটি তীর বেরিয়ে চতুর্থ এবং শেষ সেলে নির্দেশ করছে, যার মধ্যে "1" মান রয়েছে, এবং তৃতীয় সেলে ড্যাশড লাইন এবং একটি এলিপসিস রয়েছে যা নির্দেশ করে যে ডেটা স্ট্রাকচারের অন্যান্য অংশ থাকতে পারে। সব মিলিয়ে, "fut" টেবিলটি একটি ফিউচার প্রতিনিধিত্ব করে যা সেলফ-রেফারেনশিয়াল। "Pin" লেবেলযুক্ত বক্স থেকে একটি তীর বেরিয়ে, "b1" বক্সের মধ্য দিয়ে গিয়ে "pinned" বক্সের ভিতরে "fut" টেবিলে শেষ হয়।" src="img/trpl17-06.svg" class="center" />
আসলে, Box
পয়েন্টারটি এখনও অবাধে নড়াচড়া করতে পারে। মনে রাখবেন: আমরা নিশ্চিত করতে চাই যে অবশেষে রেফারেন্স করা ডেটা জায়গায় থাকে। যদি একটি পয়েন্টার নড়াচড়া করে, কিন্তু এটি যে ডেটার দিকে নির্দেশ করে তা একই জায়গায় থাকে, যেমন চিত্র ১৭-৭-এ, কোনো সম্ভাব্য সমস্যা নেই। একটি স্বতন্ত্র অনুশীলন হিসাবে, টাইপগুলির ডকুমেন্টেশন এবং std::pin
মডিউল দেখুন এবং একটি Pin
র্যাপিং Box
-এর সাথে এটি কীভাবে করবেন তা বের করার চেষ্টা করুন।) মূল বিষয় হলো সেলফ-রেফারেনশিয়াল টাইপটি নিজে নড়াচড়া করতে পারে না, কারণ এটি এখনও পিন করা আছে।
<img alt="তিনটি মোটামুটি কলামে রাখা চারটি বক্স, যা পূর্ববর্তী ডায়াগ্রামের মতোই তবে দ্বিতীয় কলামে একটি পরিবর্তন সহ। এখন দ্বিতীয় কলামে দুটি বক্স আছে, "b1" এবং "b2" লেবেলযুক্ত, "b1" ধূসর রঙের, এবং "Pin" থেকে তীরটি "b1"-এর পরিবর্তে "b2"-এর মধ্য দিয়ে যায়, যা নির্দেশ করে যে পয়েন্টারটি "b1" থেকে "b2"-তে সরে গেছে, কিন্তু "pinned"-এর ডেটা সরেনি।" src="img/trpl17-07.svg" class="center" />
যাইহোক, বেশিরভাগ টাইপই চারপাশে সরানো পুরোপুরি নিরাপদ, এমনকি যদি সেগুলি একটি Pin
র্যাপারের পিছনে থাকে। আমাদের কেবল তখনই পিনিং সম্পর্কে ভাবতে হবে যখন আইটেমগুলির অভ্যন্তরীণ রেফারেন্স থাকে। সংখ্যা এবং বুলিয়ানের মতো আদিম মানগুলি নিরাপদ কারণ তাদের স্পষ্টতই কোনো অভ্যন্তরীণ রেফারেন্স নেই। রাস্টে আপনি সাধারণত যে বেশিরভাগ টাইপের সাথে কাজ করেন সেগুলিও নেই। আপনি উদাহরণস্বরূপ, একটি Vec
চারপাশে সরাতে পারেন, কোনো চিন্তা ছাড়াই। আমরা এখন পর্যন্ত যা দেখেছি তা দিয়ে, যদি আপনার একটি Pin<Vec<String>>
থাকে, তবে আপনাকে Pin
দ্বারা প্রদত্ত নিরাপদ কিন্তু সীমাবদ্ধ API-এর মাধ্যমে সবকিছু করতে হতো, যদিও একটি Vec<String>
সর্বদা সরানো নিরাপদ যদি এর অন্য কোনো রেফারেন্স না থাকে। আমাদের কম্পাইলারকে বলার একটি উপায় প্রয়োজন যে এই ধরনের ক্ষেত্রে আইটেমগুলি চারপাশে সরানো ঠিক আছে—এবং এখানেই Unpin
কাজে আসে।
Unpin
একটি মার্কার ট্রেইট, যা আমরা চ্যাপ্টার ১৬-তে দেখা Send
এবং Sync
ট্রেইটের মতো, এবং তাই এর নিজস্ব কোনো কার্যকারিতা নেই। মার্কার ট্রেইটগুলি কেবল কম্পাইলারকে বলার জন্য বিদ্যমান যে একটি নির্দিষ্ট প্রসঙ্গে একটি প্রদত্ত ট্রেইট ইমপ্লিমেন্ট করা টাইপ ব্যবহার করা নিরাপদ। Unpin
কম্পাইলারকে জানায় যে একটি প্রদত্ত টাইপকে মানটি নিরাপদে সরানো যায় কিনা সে সম্পর্কে কোনো গ্যারান্টি বজায় রাখার প্রয়োজন নেই।
Send
এবং Sync
-এর মতোই, কম্পাইলার স্বয়ংক্রিয়ভাবে সমস্ত টাইপের জন্য Unpin
ইমপ্লিমেন্ট করে যেখানে এটি প্রমাণ করতে পারে যে এটি নিরাপদ। একটি বিশেষ ক্ষেত্র, আবার Send
এবং Sync
-এর মতো, যেখানে একটি টাইপের জন্য Unpin
ইমপ্লিমেন্ট করা হয় না। এর জন্য নোটেশন হলো impl !Unpin for SomeType
, যেখানে SomeType
হলো এমন একটি টাইপের নাম যা সেই গ্যারান্টিগুলি বজায় রাখার প্রয়োজন করে যখন সেই টাইপের একটি পয়েন্টার একটি Pin
-এ ব্যবহৃত হয়।
অন্য কথায়, Pin
এবং Unpin
-এর মধ্যে সম্পর্ক সম্পর্কে দুটি জিনিস মনে রাখতে হবে। প্রথমত, Unpin
হলো "স্বাভাবিক" ক্ষেত্র, এবং !Unpin
হলো বিশেষ ক্ষেত্র। দ্বিতীয়ত, একটি টাইপ Unpin
নাকি !Unpin
ইমপ্লিমেন্ট করে তা কেবলমাত্র তখনই গুরুত্বপূর্ণ যখন আপনি সেই টাইপের একটি পিন করা পয়েন্টার ব্যবহার করছেন যেমন Pin<&mut SomeType>
।
এটি সুনির্দিষ্ট করতে, একটি String
সম্পর্কে ভাবুন: এর একটি দৈর্ঘ্য এবং ইউনিকোড অক্ষর রয়েছে যা এটিকে তৈরি করে। আমরা একটি String
-কে Pin
-এ র্যাপ করতে পারি, যেমন চিত্র ১৭-৮-এ দেখা গেছে। যাইহোক, String
স্বয়ংক্রিয়ভাবে Unpin
ইমপ্লিমেন্ট করে, যেমন রাস্টের বেশিরভাগ অন্যান্য টাইপ করে।
ফলস্বরূপ, আমরা এমন কিছু করতে পারি যা অবৈধ হতো যদি String
!Unpin
ইমপ্লিমেন্ট করত, যেমন মেমরিতে ঠিক একই স্থানে একটি স্ট্রিংকে অন্য একটি দিয়ে প্রতিস্থাপন করা, যেমন চিত্র ১৭-৯-এ। এটি Pin
চুক্তি লঙ্ঘন করে না, কারণ String
-এর কোনো অভ্যন্তরীণ রেফারেন্স নেই যা এটিকে চারপাশে সরানো অনিরাপদ করে! ঠিক একারণেই এটি !Unpin
-এর পরিবর্তে Unpin
ইমপ্লিমেন্ট করে।
এখন আমরা লিস্টিং ১৭-১৭ থেকে সেই join_all
কলের জন্য রিপোর্ট করা ত্রুটিগুলি বোঝার জন্য যথেষ্ট জানি। আমরা মূলত async ব্লক দ্বারা উৎপাদিত ফিউচারগুলিকে একটি Vec<Box<dyn Future<Output = ()>>>
-এ সরানোর চেষ্টা করেছিলাম, কিন্তু যেমন আমরা দেখেছি, সেই ফিউচারগুলির অভ্যন্তরীণ রেফারেন্স থাকতে পারে, তাই তারা Unpin
ইমপ্লিমেন্ট করে না। তাদের পিন করা দরকার, এবং তারপরে আমরা Pin
টাইপটিকে Vec
-এ পাস করতে পারি, আত্মবিশ্বাসী যে ফিউচারের অন্তর্নিহিত ডেটা সরানো হবে না।
Pin
এবং Unpin
বেশিরভাগই নিম্ন-স্তরের লাইব্রেরি তৈরির জন্য, অথবা যখন আপনি নিজে একটি রানটাইম তৈরি করছেন, দৈনন্দিন রাস্ট কোডের জন্য ততটা গুরুত্বপূর্ণ নয়। তবে যখন আপনি ত্রুটি বার্তাগুলিতে এই ট্রেইটগুলি দেখেন, এখন আপনার কোড কীভাবে ঠিক করতে হবে সে সম্পর্কে আরও ভালো ধারণা থাকবে!
দ্রষ্টব্য:
Pin
এবংUnpin
-এর এই সংমিশ্রণটি রাস্টে এক শ্রেণীর জটিল টাইপ নিরাপদে ইমপ্লিমেন্ট করা সম্ভব করে তোলে যা অন্যথায় চ্যালেঞ্জিং প্রমাণিত হতো কারণ সেগুলি সেলফ-রেফারেনশিয়াল। যে টাইপগুলিরPin
প্রয়োজন সেগুলি আজ async রাস্টে সবচেয়ে বেশি দেখা যায়, তবে মাঝে মাঝে, আপনি সেগুলিকে অন্যান্য প্রসঙ্গেও দেখতে পারেন।
Pin
এবংUnpin
কীভাবে কাজ করে, এবং তাদের যে নিয়মগুলি বজায় রাখতে হয়, সেগুলিstd::pin
-এর জন্য API ডকুমেন্টেশনে ব্যাপকভাবে আচ্ছাদিত, তাই আপনি যদি আরও শিখতে আগ্রহী হন, তবে এটি শুরু করার জন্য একটি দুর্দান্ত জায়গা।আপনি যদি পর্দার আড়ালে জিনিসগুলি কীভাবে কাজ করে তা আরও বিস্তারিতভাবে বুঝতে চান, Asynchronous Programming in Rust-এর ২ এবং ৪ অধ্যায় দেখুন।
Stream
ট্রেইট
এখন যেহেতু আপনার Future
, Pin
, এবং Unpin
ট্রেইটগুলির উপর গভীর ধারণা আছে, আমরা Stream
ট্রেইটের দিকে আমাদের মনোযোগ ফেরাতে পারি। যেমন আপনি অধ্যায়ের শুরুতে শিখেছেন, স্ট্রীমগুলি অ্যাসিঙ্ক্রোনাস ইটারেটরের মতো। Iterator
এবং Future
-এর থেকে ভিন্ন, Stream
-এর এই লেখার সময় স্ট্যান্ডার্ড লাইব্রেরিতে কোনো সংজ্ঞা নেই, তবে futures
ক্রেট থেকে একটি খুব সাধারণ সংজ্ঞা রয়েছে যা ইকোসিস্টেম জুড়ে ব্যবহৃত হয়।
আসুন Iterator
এবং Future
ট্রেইটগুলির সংজ্ঞা পর্যালোচনা করি একটি Stream
ট্রেইট কীভাবে সেগুলিকে একত্রিত করতে পারে তা দেখার আগে। Iterator
থেকে, আমাদের কাছে একটি ক্রমের ধারণা রয়েছে: এর next
মেথড একটি Option<Self::Item>
সরবরাহ করে। Future
থেকে, আমাদের কাছে সময়ের সাথে সাথে প্রস্তুতির ধারণা রয়েছে: এর poll
মেথড একটি Poll<Self::Output>
সরবরাহ করে। সময়ের সাথে সাথে প্রস্তুত হওয়া আইটেমগুলির একটি ক্রম উপস্থাপন করতে, আমরা একটি Stream
ট্রেইট সংজ্ঞায়িত করি যা সেই বৈশিষ্ট্যগুলিকে একত্রিত করে:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; trait Stream { type Item; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll<Option<Self::Item>>; } }
Stream
ট্রেইটটি স্ট্রীম দ্বারা উৎপাদিত আইটেমগুলির টাইপের জন্য Item
নামে একটি অ্যাসোসিয়েটেড টাইপ সংজ্ঞায়িত করে। এটি Iterator
-এর মতো, যেখানে শূন্য থেকে অনেক আইটেম থাকতে পারে, এবং Future
-এর থেকে ভিন্ন, যেখানে সর্বদা একটি একক Output
থাকে, এমনকি যদি এটি ইউনিট টাইপ ()
হয়।
Stream
-এর সেই আইটেমগুলি পাওয়ার জন্য একটি মেথডও সংজ্ঞায়িত করে। আমরা এটিকে poll_next
বলি, এটি স্পষ্ট করার জন্য যে এটি Future::poll
-এর মতোই পোল করে এবং Iterator::next
-এর মতোই আইটেমের একটি ক্রম তৈরি করে। এর রিটার্ন টাইপ Poll
-কে Option
-এর সাথে একত্রিত করে। বাইরের টাইপটি Poll
, কারণ এটি প্রস্তুতির জন্য পরীক্ষা করতে হবে, ঠিক একটি ফিউচারের মতো। ভেতরের টাইপটি Option
, কারণ এটি আরও বার্তা আছে কিনা তা সংকেত দিতে হবে, ঠিক একটি ইটারেটরের মতো।
এরকম কিছু সংজ্ঞা সম্ভবত রাস্টের স্ট্যান্ডার্ড লাইব্রেরির অংশ হিসাবে শেষ হবে। এর মধ্যে, এটি বেশিরভাগ রানটাইমের টুলকিটের অংশ, তাই আপনি এটির উপর নির্ভর করতে পারেন, এবং আমরা পরবর্তীতে যা কিছু কভার করব তা সাধারণত প্রযোজ্য হবে!
স্ট্রিমিং সম্পর্কিত বিভাগে আমরা যে উদাহরণটি দেখেছি, সেখানে আমরা poll_next
বা Stream
ব্যবহার করিনি, বরং next
এবং StreamExt
ব্যবহার করেছি। আমরা অবশ্যই poll_next
API-এর ভিত্তিতে সরাসরি কাজ করতে পারতাম, আমাদের নিজস্ব Stream
স্টেট মেশিন হাতে লিখে, ঠিক যেমন আমরা ফিউচারের সাথে সরাসরি তাদের poll
মেথডের মাধ্যমে কাজ করতে পারতাম। তবে await
ব্যবহার করা অনেক সুন্দর, এবং StreamExt
ট্রেইট next
মেথড সরবরাহ করে যাতে আমরা ঠিক তাই করতে পারি:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; trait Stream { type Item; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Option<Self::Item>>; } trait StreamExt: Stream { async fn next(&mut self) -> Option<Self::Item> where Self: Unpin; // other methods... } }
দ্রষ্টব্য: আমরা অধ্যায়ের শুরুতে যে প্রকৃত সংজ্ঞাটি ব্যবহার করেছি তা এর থেকে কিছুটা ভিন্ন দেখায়, কারণ এটি রাস্টের এমন সংস্করণগুলিকে সমর্থন করে যেগুলিতে এখনও ট্রেইটে async ফাংশন ব্যবহার করার সমর্থন ছিল না। ফলস্বরূপ, এটি এইরকম দেখায়:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
সেই
Next
টাইপটি হলো একটিstruct
যাFuture
ইমপ্লিমেন্ট করে এবং আমাদেরself
-এর রেফারেন্সের লাইফটাইমকেNext<'_, Self>
দিয়ে নামকরণ করার অনুমতি দেয়, যাতেawait
এই মেথডের সাথে কাজ করতে পারে।
StreamExt
ট্রেইটটি স্ট্রীমের সাথে ব্যবহার করার জন্য উপলব্ধ সমস্ত আকর্ষণীয় মেথডেরও হোম। StreamExt
স্বয়ংক্রিয়ভাবে প্রতিটি টাইপের জন্য ইমপ্লিমেন্ট করা হয় যা Stream
ইমপ্লিমেন্ট করে, তবে এই ট্রেইটগুলি আলাদাভাবে সংজ্ঞায়িত করা হয়েছে যাতে কমিউনিটি ফাউন্ডেশনাল ট্রেইটকে প্রভাবিত না করে সুবিধাজনক API-গুলির উপর পুনরাবৃত্তি করতে পারে।
trpl
ক্রেটে ব্যবহৃত StreamExt
-এর সংস্করণে, ট্রেইটটি কেবল next
মেথড সংজ্ঞায়িত করে না, বরং next
-এর একটি ডিফল্ট ইমপ্লিমেন্টেশনও সরবরাহ করে যা Stream::poll_next
কল করার বিবরণগুলি সঠিকভাবে পরিচালনা করে। এর মানে হলো এমনকি যখন আপনার নিজের স্ট্রিমিং ডেটা টাইপ লিখতে হবে, তখন আপনাকে কেবলমাত্র Stream
ইমপ্লিমেন্ট করতে হবে, এবং তারপরে যে কেউ আপনার ডেটা টাইপ ব্যবহার করে সে স্বয়ংক্রিয়ভাবে StreamExt
এবং এর মেথডগুলি ব্যবহার করতে পারবে।
এই ট্রেইটগুলির নিম্ন-স্তরের বিবরণ সম্পর্কে আমরা কেবল এটুকুই কভার করব। শেষ করার জন্য, আসুন বিবেচনা করি কীভাবে ফিউচার (স্ট্রীম সহ), টাস্ক এবং থ্রেড সব একসাথে খাপ খায়!
সবকিছু একত্রিত করা: ফিউচার, টাস্ক, এবং থ্রেড
যেমনটি আমরা চ্যাপ্টার ১৬-এ দেখেছি, থ্রেডগুলি কনকারেন্সির (concurrency) জন্য একটি পদ্ধতি সরবরাহ করে। আমরা এই অধ্যায়ে আরেকটি পদ্ধতি দেখেছি: ফিউচার এবং স্ট্রীমের সাথে অ্যাসিঙ্ক (async) ব্যবহার করা। আপনি যদি ভাবেন যে কোনটির উপর কোন পদ্ধতি বেছে নেবেন, উত্তর হলো: এটি নির্ভর করে! এবং অনেক ক্ষেত্রে, পছন্দটি থ্রেড অথবা অ্যাসিঙ্ক নয় বরং থ্রেড এবং অ্যাসিঙ্ক।
অনেক অপারেটিং সিস্টেম এখন কয়েক দশক ধরে থ্রেড-ভিত্তিক কনকারেন্সি মডেল সরবরাহ করেছে, এবং ফলস্বরূপ অনেক প্রোগ্রামিং ভাষা সেগুলি সমর্থন করে। যাইহোক, এই মডেলগুলি ট্রেড-অফ (trade-off) ছাড়া নয়। অনেক অপারেটিং সিস্টেমে, তারা প্রতিটি থ্রেডের জন্য বেশ কিছুটা মেমরি ব্যবহার করে, এবং সেগুলি শুরু এবং বন্ধ করার জন্য কিছু ওভারহেড নিয়ে আসে। থ্রেডগুলি কেবল তখনই একটি বিকল্প যখন আপনার অপারেটিং সিস্টেম এবং হার্ডওয়্যার সেগুলি সমর্থন করে। মূলধারার ডেস্কটপ এবং মোবাইল কম্পিউটারের মতো নয়, কিছু এমবেডেড সিস্টেমের কোনো ওএস (OS) নেই, তাই তাদের থ্রেডও নেই।
অ্যাসিঙ্ক মডেল একটি ভিন্ন—এবং শেষ পর্যন্ত পরিপূরক—ট্রেড-অফের সেট সরবরাহ করে। অ্যাসিঙ্ক মডেলে, কনকারেন্ট অপারেশনগুলির নিজস্ব থ্রেডের প্রয়োজন হয় না। পরিবর্তে, তারা টাস্কগুলিতে চলতে পারে, যেমনটি আমরা স্ট্রীম বিভাগে একটি সিঙ্ক্রোনাস ফাংশন থেকে কাজ শুরু করার জন্য trpl::spawn_task
ব্যবহার করেছি। একটি টাস্ক একটি থ্রেডের অনুরূপ, কিন্তু অপারেটিং সিস্টেম দ্বারা পরিচালিত হওয়ার পরিবর্তে, এটি লাইব্রেরি-স্তরের কোড দ্বারা পরিচালিত হয়: রানটাইম (runtime)।
পূর্ববর্তী বিভাগে, আমরা দেখেছি যে আমরা একটি অ্যাসিঙ্ক চ্যানেল ব্যবহার করে এবং একটি অ্যাসিঙ্ক টাস্ক তৈরি করে একটি স্ট্রীম তৈরি করতে পারি যা আমরা সিঙ্ক্রোনাস কোড থেকে কল করতে পারি। আমরা একটি থ্রেডের সাথে ঠিক একই কাজ করতে পারি। লিস্টিং ১৭-৪০-এ, আমরা trpl::spawn_task
এবং trpl::sleep
ব্যবহার করেছি। লিস্টিং ১৭-৪১-এ, আমরা get_intervals
ফাংশনে সেগুলিকে স্ট্যান্ডার্ড লাইব্রেরি থেকে thread::spawn
এবং thread::sleep
API দিয়ে প্রতিস্থাপন করি।
extern crate trpl; // required for mdbook test use std::{pin::pin, thread, time::Duration}; use trpl::{ReceiverStream, Stream, StreamExt}; fn main() { trpl::run(async { let messages = get_messages().timeout(Duration::from_millis(200)); let intervals = get_intervals() .map(|count| format!("Interval #{count}")) .throttle(Duration::from_millis(500)) .timeout(Duration::from_secs(10)); let merged = messages.merge(intervals).take(20); let mut stream = pin!(merged); while let Some(result) = stream.next().await { match result { Ok(item) => println!("{item}"), Err(reason) => eprintln!("Problem: {reason:?}"), } } }); } fn get_messages() -> impl Stream<Item = String> { let (tx, rx) = trpl::channel(); trpl::spawn_task(async move { let messages = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; for (index, message) in messages.into_iter().enumerate() { let time_to_sleep = if index % 2 == 0 { 100 } else { 300 }; trpl::sleep(Duration::from_millis(time_to_sleep)).await; if let Err(send_error) = tx.send(format!("Message: '{message}'")) { eprintln!("Cannot send message '{message}': {send_error}"); break; } } }); ReceiverStream::new(rx) } fn get_intervals() -> impl Stream<Item = u32> { let (tx, rx) = trpl::channel(); // This is *not* `trpl::spawn` but `std::thread::spawn`! thread::spawn(move || { let mut count = 0; loop { // Likewise, this is *not* `trpl::sleep` but `std::thread::sleep`! thread::sleep(Duration::from_millis(1)); count += 1; if let Err(send_error) = tx.send(count) { eprintln!("Could not send interval {count}: {send_error}"); break; }; } }); ReceiverStream::new(rx) }
আপনি যদি এই কোডটি চালান, আউটপুটটি লিস্টিং ১৭-৪০-এর মতোই হবে। এবং লক্ষ্য করুন কলিং কোডের দৃষ্টিকোণ থেকে এখানে কত কম পরিবর্তন হয়েছে। আরও কী, যদিও আমাদের একটি ফাংশন রানটাইমে একটি অ্যাসিঙ্ক টাস্ক তৈরি করেছে এবং অন্যটি একটি ওএস থ্রেড তৈরি করেছে, ফলস্বরূপ স্ট্রীমগুলি পার্থক্য দ্বারা প্রভাবিত হয়নি।
তাদের মিল থাকা সত্ত্বেও, এই দুটি পদ্ধতি খুব ভিন্নভাবে আচরণ করে, যদিও আমরা এই খুব সাধারণ উদাহরণে এটি পরিমাপ করতে কিছুটা বেগ পেতে পারি। আমরা যেকোনো আধুনিক ব্যক্তিগত কম্পিউটারে লক্ষ লক্ষ অ্যাসিঙ্ক টাস্ক তৈরি করতে পারতাম। যদি আমরা থ্রেডের সাথে এটি করার চেষ্টা করতাম, তাহলে আমাদের আক্ষরিক অর্থেই মেমরি ফুরিয়ে যেত!
যাইহোক, এই API-গুলি এত অনুরূপ হওয়ার একটি কারণ আছে। থ্রেডগুলি সিঙ্ক্রোনাস অপারেশনগুলির সেটের জন্য একটি সীমানা হিসাবে কাজ করে; কনকারেন্সি থ্রেডগুলির মধ্যে সম্ভব। টাস্কগুলি অ্যাসিঙ্ক্রোনাস অপারেশনগুলির সেটের জন্য একটি সীমানা হিসাবে কাজ করে; কনকারেন্সি টাস্কগুলির মধ্যে এবং ভিতরে উভয়ই সম্ভব, কারণ একটি টাস্ক তার বডিতে ফিউচারগুলির মধ্যে স্যুইচ করতে পারে। অবশেষে, ফিউচারগুলি হলো রাস্টের কনকারেন্সির সবচেয়ে ক্ষুদ্রতম একক, এবং প্রতিটি ফিউচার অন্যান্য ফিউচারের একটি ট্রি (tree) প্রতিনিধিত্ব করতে পারে। রানটাইম—বিশেষত, এর এক্সিকিউটর (executor)—টাস্কগুলি পরিচালনা করে, এবং টাস্কগুলি ফিউচারগুলি পরিচালনা করে। সেই ক্ষেত্রে, টাস্কগুলি লাইটওয়েট, রানটাইম-পরিচালিত থ্রেডের মতো যা অপারেটিং সিস্টেম দ্বারা পরিচালিত হওয়ার পরিবর্তে একটি রানটাইম দ্বারা পরিচালিত হওয়ার কারণে অতিরিক্ত ক্ষমতা পায়।
এর মানে এই নয় যে অ্যাসিঙ্ক টাস্কগুলি সবসময় থ্রেডের চেয়ে ভালো (বা বিপরীত)। থ্রেডের সাথে কনকারেন্সি কিছু উপায়ে async
-এর সাথে কনকারেন্সির চেয়ে একটি সহজ প্রোগ্রামিং মডেল। এটি একটি শক্তি বা দুর্বলতা হতে পারে। থ্রেডগুলি কিছুটা "ফায়ার অ্যান্ড ফরগেট" (fire and forget); তাদের ফিউচারের কোনো নেটিভ সমতুল্য নেই, তাই তারা অপারেটিং সিস্টেম নিজে ছাড়া অন্য কোনো কিছু দ্বারা বাধাগ্রস্ত না হয়ে কেবল সম্পূর্ণ না হওয়া পর্যন্ত চলে। অর্থাৎ, তাদের ইন্ট্রা-টাস্ক কনকারেন্সি (intratask concurrency)-র জন্য কোনো বিল্ট-ইন সমর্থন নেই যেমনটি ফিউচারের আছে। রাস্টে থ্রেডগুলির ক্যান্সেলেশনের (cancellation) জন্য কোনো মেকানিজমও নেই—একটি বিষয় যা আমরা এই অধ্যায়ে স্পষ্টভাবে কভার করিনি তবে এটি অন্তর্নিহিত ছিল যে যখনই আমরা একটি ফিউচার শেষ করেছি, তার স্টেট সঠিকভাবে পরিষ্কার হয়ে গেছে।
এই সীমাবদ্ধতাগুলি থ্রেডগুলিকে ফিউচারের চেয়ে কম্পোজ করা কঠিন করে তোলে। উদাহরণস্বরূপ, এই অধ্যায়ের শুরুতে আমরা যে timeout
এবং throttle
মেথডগুলি তৈরি করেছি সেগুলির মতো হেল্পার তৈরি করতে থ্রেড ব্যবহার করা অনেক বেশি কঠিন। ফিউচারগুলি যে আরও সমৃদ্ধ ডেটা স্ট্রাকচার, তার মানে হলো সেগুলি আরও স্বাভাবিকভাবে একসাথে কম্পোজ করা যেতে পারে, যেমন আমরা দেখেছি।
টাস্কগুলি, তাহলে, আমাদের ফিউচারগুলির উপর অতিরিক্ত নিয়ন্ত্রণ দেয়, যা আমাদের কোথায় এবং কীভাবে সেগুলিকে গ্রুপ করতে হবে তা বেছে নেওয়ার সুযোগ দেয়। এবং দেখা যাচ্ছে যে থ্রেড এবং টাস্কগুলি প্রায়শই খুব ভালোভাবে একসাথে কাজ করে, কারণ টাস্কগুলি (অন্তত কিছু রানটাইমে) থ্রেডগুলির মধ্যে সরানো যেতে পারে। আসলে, পর্দার আড়ালে, আমরা যে রানটাইমটি ব্যবহার করে আসছি—spawn_blocking
এবং spawn_task
ফাংশন সহ—ডিফল্টরূপে মাল্টিথ্রেডেড! অনেক রানটাইম সিস্টেমের সামগ্রিক পারফরম্যান্স উন্নত করার জন্য থ্রেডগুলির মধ্যে স্বচ্ছভাবে টাস্কগুলি সরানোর জন্য ওয়ার্ক স্টিলিং (work stealing) নামে একটি পদ্ধতি ব্যবহার করে, যা থ্রেডগুলি বর্তমানে কীভাবে ব্যবহৃত হচ্ছে তার উপর ভিত্তি করে। সেই পদ্ধতির জন্য আসলে থ্রেড এবং টাস্ক, এবং তাই ফিউচার প্রয়োজন।
কোন পদ্ধতি কখন ব্যবহার করতে হবে তা নিয়ে ভাবার সময়, এই সাধারণ নিয়মগুলি বিবেচনা করুন:
- যদি কাজটি খুবই প্যারালাল করা যায় (very parallelizable), যেমন একগুচ্ছ ডেটা প্রসেস করা যেখানে প্রতিটি অংশ আলাদাভাবে প্রসেস করা যায়, তবে থ্রেডগুলি একটি ভালো পছন্দ।
- যদি কাজটি খুবই কনকারেন্ট (very concurrent) হয়, যেমন বিভিন্ন উৎস থেকে আসা বার্তাগুলি পরিচালনা করা যা বিভিন্ন ব্যবধানে বা বিভিন্ন হারে আসতে পারে, তবে অ্যাসিঙ্ক একটি ভালো পছন্দ।
এবং যদি আপনার প্যারালালিসম এবং কনকারেন্সি উভয়ই প্রয়োজন হয়, তবে আপনাকে থ্রেড এবং অ্যাসিঙ্কের মধ্যে বেছে নিতে হবে না। আপনি সেগুলিকে অবাধে একসাথে ব্যবহার করতে পারেন, প্রত্যেকটিকে তার সেরা কাজটি করতে দিয়ে। উদাহরণস্বরূপ, লিস্টিং ১৭-৪২ বাস্তব-বিশ্বের রাস্ট কোডে এই ধরনের মিশ্রণের একটি মোটামুটি সাধারণ উদাহরণ দেখায়।
extern crate trpl; // for mdbook test use std::{thread, time::Duration}; fn main() { let (tx, mut rx) = trpl::channel(); thread::spawn(move || { for i in 1..11 { tx.send(i).unwrap(); thread::sleep(Duration::from_secs(1)); } }); trpl::run(async { while let Some(message) = rx.recv().await { println!("{message}"); } }); }
আমরা একটি অ্যাসিঙ্ক চ্যানেল তৈরি করে শুরু করি, তারপরে একটি থ্রেড তৈরি করি যা চ্যানেলের প্রেরক (sender) দিকের মালিকানা নেয়। থ্রেডের মধ্যে, আমরা ১ থেকে ১০ পর্যন্ত সংখ্যা পাঠাই, প্রতিটির মধ্যে এক সেকেন্ড ঘুমিয়ে। অবশেষে, আমরা অধ্যায় জুড়ে যেমন করেছি ঠিক তেমনই trpl::run
-এ পাস করা একটি অ্যাসিঙ্ক ব্লক দিয়ে তৈরি একটি ফিউচার চালাই। সেই ফিউচারে, আমরা সেই বার্তাগুলি await করি, ঠিক যেমন আমরা দেখেছি অন্যান্য বার্তা-প্রেরণের উদাহরণগুলিতে।
অধ্যায়ের শুরুতে আমরা যে পরিস্থিতি দিয়ে শুরু করেছিলাম সেটিতে ফিরে যেতে, কল্পনা করুন একটি ডেডিকেটেড থ্রেড ব্যবহার করে একগুচ্ছ ভিডিও এনকোডিং টাস্ক চালানো হচ্ছে (কারণ ভিডিও এনকোডিং কম্পিউট-বাউন্ড) কিন্তু একটি অ্যাসিঙ্ক চ্যানেল দিয়ে UI-কে জানানো হচ্ছে যে সেই অপারেশনগুলি শেষ হয়েছে। বাস্তব-বিশ্বের ব্যবহারের ক্ষেত্রে এই ধরনের সংমিশ্রণের অগণিত উদাহরণ রয়েছে।
সারসংক্ষেপ
এই বইয়ে কনকারেন্সির বিষয়ে এটিই শেষ নয়। চ্যাপ্টার ২১-এর প্রকল্পটি এখানে আলোচনা করা সহজ উদাহরণগুলির চেয়ে আরও বাস্তবসম্মত পরিস্থিতিতে এই ধারণাগুলি প্রয়োগ করবে এবং থ্রেডিং বনাম টাস্কের সাথে সমস্যা-সমাধানের আরও সরাসরি তুলনা করবে।
আপনি এই পদ্ধতিগুলির মধ্যে যেটিই বেছে নিন না কেন, রাস্ট আপনাকে নিরাপদ, দ্রুত, কনকারেন্ট কোড লেখার জন্য প্রয়োজনীয় টুলস সরবরাহ করে—সেটি একটি উচ্চ-থ্রুপুট ওয়েব সার্ভারের জন্য হোক বা একটি এমবেডেড অপারেটিং সিস্টেমের জন্য।
এরপরে, আমরা আপনার রাস্ট প্রোগ্রামগুলি বড় হওয়ার সাথে সাথে সমস্যা মডেলিং এবং সমাধান কাঠামোবদ্ধ করার ইডিওম্যাটিক উপায়গুলি নিয়ে আলোচনা করব। এছাড়াও, আমরা আলোচনা করব কীভাবে রাস্টের ইডিওমগুলি অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং থেকে আপনার পরিচিত হতে পারে এমন ইডিওমগুলির সাথে সম্পর্কিত।
অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং এর বৈশিষ্ট্য (Object-Oriented Programming Features)
অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং (OOP) হচ্ছে প্রোগ্রামকে মডেল করার একটি পদ্ধতি। প্রোগ্রামিং-এর একটি ধারণা বা concept হিসেবে অবজেক্ট (object) প্রথম পরিচিতি পায় ১৯৬০-এর দশকে Simula নামের একটি programming language-এ। সেই অবজেক্টগুলো অ্যালান কে-র (Alan Kay) programming architecture-কে প্রভাবিত করেছিল, যেখানে অবজেক্টগুলো একে অপরকে মেসেজ পাঠাতো। এই আর্কিটেকচার বর্ণনা করার জন্য তিনি ১৯৬৭ সালে অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিং কথাটি প্রথম ব্যবহার করেন।
OOP কী, তা নিয়ে অনেক প্রতিদ্বন্দ্বী সংজ্ঞা রয়েছে। এই সংজ্ঞাগুলোর কয়েকটির মতে Rust একটি অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজ, আবার অন্যগুলোর মতে তা নয়। এই অধ্যায়ে আমরা এমন কিছু বৈশিষ্ট্য নিয়ে আলোচনা করব যা সাধারণত অবজেক্ট-ওরিয়েন্টেড হিসেবে বিবেচিত হয় এবং দেখব সেই বৈশিষ্ট্যগুলো রাস্টের নিজস্ব রীতিতে (idiomatic Rust) কীভাবে প্রকাশ পায়। এরপর আমরা দেখাব কীভাবে রাস্টে একটি অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্ন প্রয়োগ করা যায় এবং এর সুবিধা-অসুবিধাগুলো (trade-offs) নিয়ে আলোচনা করব। এর পাশাপাশি, রাস্টের নিজস্ব শক্তিশালী দিকগুলো ব্যবহার করে একটি সমাধান তৈরির সাথে এর কী পার্থক্য, তাও তুলে ধরব।
অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজের বৈশিষ্ট্য (Characteristics of Object-Oriented Languages)
প্রোগ্রামিং কমিউনিটিতে কোনো একটি ল্যাঙ্গুয়েজকে অবজেক্ট-ওরিয়েন্টেড হিসেবে বিবেচনা করার জন্য তার কী কী বৈশিষ্ট্য থাকা আবশ্যক, সে বিষয়ে কোনো সর্বসম্মত মত নেই। Rust অনেকগুলো প্রোগ্রামিং প্যারাডাইম দ্বারা প্রভাবিত, যার মধ্যে OOP একটি; উদাহরণস্বরূপ, আমরা ১৩তম অধ্যায়ে ফাংশনাল প্রোগ্রামিং থেকে আসা বৈশিষ্ট্যগুলো দেখেছি। বলা যায়, OOP ল্যাঙ্গুয়েজগুলোর কিছু সাধারণ বৈশিষ্ট্য রয়েছে, যেমন—অবজেক্ট (objects), এনক্যাপসুলেশন (encapsulation), এবং ইনহেরিটেন্স (inheritance)। চলুন দেখি এই বৈশিষ্ট্যগুলোর প্রত্যেকটির অর্থ কী এবং Rust সেগুলোকে সমর্থন করে কিনা।
অবজেক্টে ডেটা এবং আচরণ (Behavior) দুটোই থাকে (Objects Contain Data and Behavior)
এরিক গামা, রিচার্ড হেলম, রালফ জনসন এবং জন ভিসাইডসের লেখা Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994) বইটি, যা কথোপকথনে "গ্যাং অফ ফোর" (The Gang of Four) বই হিসাবে পরিচিত, অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্নের একটি ক্যাটালগ। এটি OOP-কে এভাবে সংজ্ঞায়িত করে:
অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামগুলো অবজেক্ট দিয়ে তৈরি। একটি অবজেক্ট ডেটা এবং সেই ডেটার উপর কাজ করে এমন প্রসিডিউর (procedure) উভয়কেই প্যাকেজ করে। এই প্রসিডিউরগুলোকে সাধারণত মেথড (methods) বা অপারেশন (operations) বলা হয়।
এই সংজ্ঞা অনুসারে, Rust একটি অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজ: struct
এবং enum
-এর মধ্যে ডেটা থাকে, এবং impl
ব্লকগুলো struct
ও enum
-এর উপর মেথড সরবরাহ করে। যদিও মেথডসহ struct
এবং enum
-কে অবজেক্ট বলা হয় না, তবে "গ্যাং অফ ফোর"-এর সংজ্ঞা অনুসারে এগুলো একই কার্যকারিতা প্রদান করে।
এনক্যাপসুলেশন যা ভেতরের বিবরণ লুকিয়ে রাখে (Encapsulation That Hides Implementation Details)
OOP-এর সাথে জড়িত আরেকটি সাধারণ ধারণা হলো এনক্যাপসুলেশন (encapsulation), যার মানে হলো একটি অবজেক্টের ভেতরের কার্যকারিতার বিবরণ (implementation details) সেই অবজেক্ট ব্যবহারকারী কোডের কাছে সরাসরি অ্যাক্সেসযোগ্য থাকে না। সুতরাং, একটি অবজেক্টের সাথে ইন্টারঅ্যাক্ট করার একমাত্র উপায় হলো তার পাবলিক API; অবজেক্ট ব্যবহারকারী কোডের উচিত নয় অবজেক্টের গভীরে প্রবেশ করে সরাসরি ডেটা বা আচরণ পরিবর্তন করা। এটি প্রোগ্রামারকে অবজেক্ট ব্যবহারকারী কোড পরিবর্তন না করেই অবজেক্টের ভেতরের অংশ পরিবর্তন এবং রিফ্যাক্টর করার সুযোগ দেয়।
আমরা ৭ম অধ্যায়ে আলোচনা করেছি কীভাবে এনক্যাপসুলেশন নিয়ন্ত্রণ করতে হয়: আমরা pub
কীওয়ার্ড ব্যবহার করে ঠিক করতে পারি যে আমাদের কোডের কোন মডিউল, টাইপ, ফাংশন এবং মেথড পাবলিক হবে, এবং ডিফল্টভাবে বাকি সবকিছু প্রাইভেট থাকে। উদাহরণস্বরূপ, আমরা একটি AveragedCollection
struct সংজ্ঞায়িত করতে পারি যার একটি ফিল্ডে i32
মানের একটি ভেক্টর থাকবে। এই struct-এ এমন একটি ফিল্ডও থাকতে পারে যেখানে ভেক্টরের মানগুলোর গড় সংরক্ষিত থাকবে, যার মানে হলো যখনই কারো গড়ের প্রয়োজন হবে, তখন আর নতুন করে গণনা করতে হবে না। অন্য কথায়, AveragedCollection
আমাদের জন্য গণনা করা গড় ক্যাশ (cache) করে রাখবে। লিস্টিং ১৮-১ এ AveragedCollection
struct-এর সংজ্ঞা দেওয়া হলো।
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
Struct-টিকে pub
হিসেবে চিহ্নিত করা হয়েছে যাতে অন্য কোড এটি ব্যবহার করতে পারে, কিন্তু struct-এর ভেতরের ফিল্ডগুলো প্রাইভেট থাকে। এক্ষেত্রে এটি গুরুত্বপূর্ণ কারণ আমরা নিশ্চিত করতে চাই যে যখনই তালিকা থেকে কোনো মান যোগ বা حذف করা হবে, তখন গড়ও আপডেট করা হবে। আমরা এটি করি struct-এর উপর add
, remove
, এবং average
মেথড প্রয়োগ করে, যা লিস্টিং ১৮-২ এ দেখানো হয়েছে।
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
পাবলিক মেথড add
, remove
, এবং average
হলো AveragedCollection
-এর একটি ইনস্ট্যান্সের ডেটা অ্যাক্সেস বা পরিবর্তন করার একমাত্র উপায়। যখন add
মেথড ব্যবহার করে list
-এ একটি আইটেম যোগ করা হয় বা remove
মেথড ব্যবহার করে حذف করা হয়, তখন প্রতিটির ইমপ্লিমেন্টেশন প্রাইভেট update_average
মেথডকে কল করে, যা average
ফিল্ড আপডেট করার কাজ করে।
আমরা list
এবং average
ফিল্ড দুটিকে প্রাইভেট রেখেছি যাতে বাইরের কোনো কোড সরাসরি list
ফিল্ডে আইটেম যোগ বা حذف করতে না পারে; অন্যথায়, list
পরিবর্তন হলে average
ফিল্ডটি অসামঞ্জস্যপূর্ণ (out of sync) হয়ে যেতে পারে। average
মেথডটি average
ফিল্ডের মান রিটার্ন করে, যা বাইরের কোডকে average
পড়ার সুযোগ দেয় কিন্তু পরিবর্তন করার নয়।
যেহেতু আমরা AveragedCollection
struct-এর ভেতরের বিবরণ এনক্যাপসুলেট করেছি, তাই আমরা ভবিষ্যতে ডেটা স্ট্রাকচারের মতো বিভিন্ন দিক সহজেই পরিবর্তন করতে পারব। উদাহরণস্বরূপ, list
ফিল্ডের জন্য Vec<i32>
-এর পরিবর্তে HashSet<i32>
ব্যবহার করতে পারি। যতক্ষণ পর্যন্ত add
, remove
, এবং average
পাবলিক মেথডগুলোর সিগনেচার একই থাকবে, AveragedCollection
ব্যবহারকারী কোড পরিবর্তন করার কোনো প্রয়োজন হবে না। যদি আমরা list
-কে পাবলিক করতাম, তবে এটি সম্ভব হতো না: HashSet<i32>
এবং Vec<i32>
-তে আইটেম যোগ এবং حذف করার জন্য ভিন্ন ভিন্ন মেথড রয়েছে, তাই বাইরের কোড যদি সরাসরি list
পরিবর্তন করত, তবে সম্ভবত সেটিও পরিবর্তন করতে হতো।
যদি কোনো ল্যাঙ্গুয়েজকে অবজেক্ট-ওরিয়েন্টেড হিসেবে বিবেচনা করার জন্য এনক্যাপসুলেশন একটি প্রয়োজনীয় দিক হয়, তাহলে Rust সেই শর্ত পূরণ করে। কোডের বিভিন্ন অংশের জন্য pub
ব্যবহার করার বা না করার বিকল্পটি ভেতরের বিবরণ এনক্যাপসুলেট করতে সক্ষম করে।
টাইপ সিস্টেম এবং কোড শেয়ারিং হিসাবে ইনহেরিটেন্স (Inheritance as a Type System and as Code Sharing)
ইনহেরিটেন্স (Inheritance) হলো এমন একটি প্রক্রিয়া যার মাধ্যমে একটি অবজেক্ট অন্য একটি অবজেক্টের সংজ্ঞা থেকে বিভিন্ন উপাদান উত্তরাধিকার সূত্রে পেতে পারে, ফলে প্যারেন্ট অবজেক্টের ডেটা এবং আচরণ পুনরায় কোড না লিখেই পাওয়া যায়।
যদি কোনো ল্যাঙ্গুয়েজকে অবজেক্ট-ওরিয়েন্টেড হতে হলে ইনহেরিটেন্স থাকতেই হয়, তবে Rust সেই ধরনের ল্যাঙ্গুয়েজ নয়। ম্যাক্রো ব্যবহার না করে এমন কোনো struct সংজ্ঞায়িত করার উপায় নেই যা প্যারেন্ট struct-এর ফিল্ড এবং মেথড ইমপ্লিমেন্টেশন উত্তরাধিকার সূত্রে পাবে।
তবে, আপনি যদি আপনার প্রোগ্রামিং টুলবক্সে ইনহেরিটেন্স ব্যবহারে অভ্যস্ত হন, তবে Rust-এ আপনি অন্য সমাধান ব্যবহার করতে পারেন, যা নির্ভর করবে আপনি কী কারণে ইনহেরিটেন্স ব্যবহার করতে চাইছেন তার উপর।
আপনি মূলত দুটি প্রধান কারণে ইনহেরিটেন্স বেছে নেবেন। একটি হলো কোড পুনঃব্যবহার (reuse of code): আপনি একটি টাইপের জন্য নির্দিষ্ট আচরণ ইমপ্লিমেন্ট করতে পারেন এবং ইনহেরিটেন্স আপনাকে সেই ইমপ্লিমেন্টেশনটি অন্য একটি টাইপের জন্য পুনঃব্যবহারের সুযোগ দেয়। আপনি Rust কোডে ডিফল্ট ট্রেইট মেথড ইমপ্লিমেন্টেশন ব্যবহার করে সীমিত আকারে এটি করতে পারেন, যা আপনি লিস্টিং ১০-১৪-তে দেখেছেন যখন আমরা Summary
trait-এ summarize
মেথডের একটি ডিফল্ট ইমপ্লিমেন্টেশন যোগ করেছিলাম। Summary
trait ইমপ্লিমেন্ট করা যেকোনো টাইপ কোনো অতিরিক্ত কোড ছাড়াই summarize
মেথডটি ব্যবহার করতে পারবে। এটি অনেকটা একটি প্যারেন্ট ক্লাসের কোনো মেথডের ইমপ্লিমেন্টেশন থাকার মতো, যা উত্তরাধিকার সূত্রে পাওয়া চাইল্ড ক্লাসেও সেই মেথডটি থাকে। আমরা Summary
trait ইমপ্লিমেন্ট করার সময় summarize
মেথডের ডিফল্ট ইমপ্লিমেন্টেশনটি ওভাররাইডও করতে পারি, যা একটি চাইল্ড ক্লাসের প্যারেন্ট ক্লাস থেকে উত্তরাধিকার সূত্রে পাওয়া মেথডের ইমপ্লিমেন্টেশন ওভাররাইড করার মতো।
ইনহেরিটেন্স ব্যবহারের অন্য কারণটি টাইপ সিস্টেমের সাথে সম্পর্কিত: একটি চাইল্ড টাইপকে প্যারেন্ট টাইপের জায়গায় ব্যবহার করতে সক্ষম করা। একে পলিমরফিজম (polymorphism) বলা হয়, যার মানে হলো আপনি রানটাইমে একাধিক অবজেক্টকে একে অপরের বিকল্প হিসেবে ব্যবহার করতে পারবেন যদি তাদের মধ্যে নির্দিষ্ট কিছু বৈশিষ্ট্য থাকে।
পলিমরফিজম (Polymorphism)
অনেকের কাছে পলিমরফিজম এবং ইনহেরিটেন্স সমার্থক। কিন্তু এটি আসলে একটি আরও সাধারণ ধারণা যা এমন কোডকে বোঝায় যা একাধিক টাইপের ডেটা নিয়ে কাজ করতে পারে। ইনহেরিটেন্সের ক্ষেত্রে, এই টাইপগুলো সাধারণত সাব-ক্লাস (subclass) হয়।
এর পরিবর্তে, Rust বিভিন্ন সম্ভাব্য টাইপের জন্য জেনেরিক (generics) ব্যবহার করে এবং সেই টাইপগুলোকে কী সরবরাহ করতে হবে তার উপর সীমাবদ্ধতা আরোপ করার জন্য ট্রেইট বাউন্ড (trait bounds) ব্যবহার করে। একে কখনও কখনও বাউন্ডেড প্যারামেট্রিক পলিমরফিজম (bounded parametric polymorphism) বলা হয়।
ইনহেরিটেন্সের সুবিধা প্রদান না করে Rust ভিন্ন একটি পথ বেছে নিয়েছে। ইনহেরিটেন্স প্রায়শই প্রয়োজনের চেয়ে বেশি কোড শেয়ার করার ঝুঁকিতে থাকে। সাব-ক্লাসগুলোর সবসময় তাদের প্যারেন্ট ক্লাসের সমস্ত বৈশিষ্ট্য শেয়ার করা উচিত নয়, কিন্তু ইনহেরিটেন্সের মাধ্যমে তারা তা করে ফেলে। এটি একটি প্রোগ্রামের ডিজাইনকে কম নমনীয় করে তুলতে পারে। এটি সাব-ক্লাসের উপর এমন মেথড কল করার সম্ভাবনা তৈরি করে যা অর্থহীন বা ত্রুটির কারণ হতে পারে কারণ মেথডগুলো সাব-ক্লাসের জন্য প্রযোজ্য নয়। এছাড়াও, কিছু ল্যাঙ্গুয়েজ শুধুমাত্র সিঙ্গেল ইনহেরিটেন্স (single inheritance) অনুমোদন করে (অর্থাৎ একটি সাব-ক্লাস শুধুমাত্র একটি ক্লাস থেকে ইনহেরিট করতে পারে), যা একটি প্রোগ্রামের ডিজাইনের নমনীয়তাকে আরও সীমাবদ্ধ করে।
এই কারণগুলোর জন্য, Rust পলিমরফিজম সক্ষম করার জন্য ইনহেরিটেন্সের পরিবর্তে ট্রেইট অবজেক্ট (trait objects) ব্যবহারের ভিন্ন পদ্ধতি গ্রহণ করে। চলুন দেখি ট্রেইট অবজেক্ট কীভাবে কাজ করে।
সাধারণ আচরণ অ্যাবস্ট্র্যাক্ট করতে ট্রেইট অবজেক্ট ব্যবহার (Using Trait Objects to Abstract over Shared Behavior)
৮ম অধ্যায়ে আমরা উল্লেখ করেছিলাম যে ভেক্টরের একটি সীমাবদ্ধতা হলো এটি কেবল এক প্রকারের (one type) উপাদান সংরক্ষণ করতে পারে। আমরা লিস্টিং ৮-৯-এ এর একটি সমাধান তৈরি করেছিলাম যেখানে আমরা একটি SpreadsheetCell
নামের enum
সংজ্ঞায়িত করেছিলাম, যার মধ্যে ইন্টিজার, ফ্লোট এবং টেক্সট রাখার জন্য ভ্যারিয়েন্ট (variant) ছিল। এর মানে হলো আমরা প্রতিটি সেলে বিভিন্ন ধরণের ডেটা সংরক্ষণ করতে পারতাম এবং তারপরেও একটি ভেক্টর পেতাম যা সেলের একটি সারিকে উপস্থাপন করত। যখন আমাদের अदलाबदलযোগ্য আইটেমগুলো একটি নির্দিষ্ট সেটের টাইপ হয় যা কোড কম্পাইল করার সময় আমরা জানি, তখন এটি একটি চমৎকার সমাধান।
তবে, কখনও কখনও আমরা চাই যে আমাদের লাইব্রেরি ব্যবহারকারী একটি নির্দিষ্ট পরিস্থিতিতে বৈধ টাইপের সেটকে প্রসারিত (extend) করতে সক্ষম হোক। এটি কীভাবে অর্জন করা যেতে পারে তা দেখানোর জন্য, আমরা একটি উদাহরণ হিসাবে গ্রাফিক্যাল ইউজার ইন্টারফেস (GUI) টুল তৈরি করব যা আইটেমগুলির একটি তালিকার মধ্য দিয়ে যায় এবং স্ক্রিনে আঁকার জন্য প্রতিটির উপর একটি draw
মেথড কল করে—এটি GUI টুলগুলোর জন্য একটি সাধারণ কৌশল। আমরা gui
নামে একটি লাইব্রেরি ক্রেট তৈরি করব যাতে একটি GUI লাইব্রেরির কাঠামো থাকবে। এই ক্রেটে মানুষের ব্যবহারের জন্য কিছু টাইপ অন্তর্ভুক্ত থাকতে পারে, যেমন Button
বা TextField
। এছাড়াও, gui
ব্যবহারকারীরা তাদের নিজস্ব টাইপ তৈরি করতে চাইবে যা আঁকা যায়: উদাহরণস্বরূপ, একজন প্রোগ্রামার একটি Image
যোগ করতে পারে এবং অন্য একজন একটি SelectBox
যোগ করতে পারে।
লাইব্রেরি লেখার সময়, আমরা অন্য প্রোগ্রামাররা যে সমস্ত টাইপ তৈরি করতে চাইতে পারে তা আগে থেকে জানতে ও সংজ্ঞায়িত করতে পারি না। কিন্তু আমরা জানি যে gui
-কে বিভিন্ন ধরণের অনেকগুলো মান ট্র্যাক করতে হবে এবং এই ভিন্ন ভিন্ন টাইপের মানগুলির প্রতিটির উপর একটি draw
মেথড কল করতে হবে। draw
মেথড কল করার সময় ঠিক কী ঘটবে তা জানার প্রয়োজন নেই, শুধু জানা দরকার যে মানটির কাছে কল করার জন্য সেই মেথডটি থাকবে।
ইনহেরিটেন্স আছে এমন কোনো ল্যাঙ্গুয়েজে এটি করার জন্য, আমরা Component
নামে একটি ক্লাস সংজ্ঞায়িত করতে পারতাম যার একটি draw
নামের মেথড থাকবে। অন্যান্য ক্লাস, যেমন Button
, Image
, এবং SelectBox
, Component
থেকে ইনহেরিট করত এবং এর ফলে draw
মেথডটিও ইনহেরিট করত। তারা প্রত্যেকে তাদের নিজস্ব আচরণ সংজ্ঞায়িত করতে draw
মেথডকে ওভাররাইড করতে পারত, কিন্তু ফ্রেমওয়ার্কটি সমস্ত টাইপকে Component
-এর ইনস্ট্যান্স হিসাবে বিবেচনা করতে পারত এবং তাদের উপর draw
কল করতে পারত। কিন্তু যেহেতু Rust-এ ইনহেরিটেন্স নেই, তাই আমাদের gui
লাইব্রেরিটি এমনভাবে গঠন করার জন্য অন্য কোনো উপায়ের প্রয়োজন যাতে ব্যবহারকারীরা লাইব্রেরির সাথে সামঞ্জস্যপূর্ণ নতুন টাইপ তৈরি করতে পারে।
সাধারণ আচরণের জন্য একটি ট্রেইট সংজ্ঞায়িত করা (Defining a Trait for Common Behavior)
gui
-এর যে আচরণ আমরা চাই তা বাস্তবায়ন করতে, আমরা Draw
নামে একটি ট্রেইট সংজ্ঞায়িত করব যার একটি মেথড থাকবে, যার নাম draw
। তারপরে আমরা একটি ভেক্টর সংজ্ঞায়িত করতে পারি যা একটি ট্রেইট অবজেক্ট (trait object) নেয়। একটি ট্রেইট অবজেক্ট একই সাথে আমাদের নির্দিষ্ট করা ট্রেইট বাস্তবায়নকারী একটি টাইপের ইনস্ট্যান্স এবং রানটাইমে সেই টাইপের ট্রেইট মেথডগুলো খুঁজে বের করার জন্য ব্যবহৃত একটি টেবিলের দিকে নির্দেশ করে। আমরা একটি ট্রেইট অবজেক্ট তৈরি করি কোনো এক ধরণের পয়েন্টার, যেমন একটি &
রেফারেন্স বা একটি Box<T>
স্মার্ট পয়েন্টার, তারপর dyn
কীওয়ার্ড এবং তারপরে প্রাসঙ্গিক ট্রেইট নির্দিষ্ট করে। (ট্রেইট অবজেক্টকে কেন একটি পয়েন্টার ব্যবহার করতে হবে তার কারণ সম্পর্কে আমরা ২০তম অধ্যায়ে "ডাইনামিক্যালি সাইজড টাইপস এবং Sized
ট্রেইট" অংশে আলোচনা করব।) আমরা একটি জেনেরিক বা সুনির্দিষ্ট টাইপের জায়গায় ট্রেইট অবজেক্ট ব্যবহার করতে পারি। যেখানেই আমরা একটি ট্রেইট অবজেক্ট ব্যবহার করি, Rust-এর টাইপ সিস্টেম কম্পাইল টাইমে নিশ্চিত করবে যে সেই প্রেক্ষাপটে ব্যবহৃত যেকোনো মান ট্রেইট অবজেক্টের ট্রেইটটি বাস্তবায়ন করবে। ফলস্বরূপ, আমাদের কম্পাইল টাইমে সমস্ত সম্ভাব্য টাইপ জানার প্রয়োজন নেই।
আমরা উল্লেখ করেছি যে, Rust-এ, আমরা struct এবং enum-কে অন্য ল্যাঙ্গুয়েজের অবজেক্ট থেকে আলাদা করার জন্য "অবজেক্ট" বলা থেকে বিরত থাকি। একটি struct বা enum-এ, struct ফিল্ডের ডেটা এবং impl
ব্লকের আচরণ আলাদা থাকে, যেখানে অন্য ল্যাঙ্গুয়েজে ডেটা এবং আচরণ একত্রিত হয়ে একটি ধারণা তৈরি করে, যাকে প্রায়ই অবজেক্ট বলা হয়। ট্রেইট অবজেক্ট অন্য ল্যাঙ্গুয়েজের অবজেক্ট থেকে ভিন্ন কারণ আমরা একটি ট্রেইট অবজেক্টে ডেটা যোগ করতে পারি না। ট্রেইট অবজেক্ট অন্য ল্যাঙ্গুয়েজের অবজেক্টের মতো ততটা সাধারণভাবে দরকারী নয়: তাদের নির্দিষ্ট উদ্দেশ্য হলো সাধারণ আচরণের উপর অ্যাবস্ট্র্যাকশন তৈরি করা।
লিস্টিং ১৮-৩ দেখাচ্ছে কীভাবে draw
নামে একটি মেথড সহ Draw
নামে একটি ট্রেইট সংজ্ঞায়িত করতে হয়।
pub trait Draw {
fn draw(&self);
}
১০ম অধ্যায়ে ট্রেইট কীভাবে সংজ্ঞায়িত করতে হয় সে সম্পর্কে আমাদের আলোচনা থেকে এই সিনট্যাক্সটি পরিচিত মনে হওয়া উচিত। এরপর আসছে কিছু নতুন সিনট্যাক্স: লিস্টিং ১৮-৪ একটি Screen
নামের struct সংজ্ঞায়িত করে যা components
নামের একটি ভেক্টর ধারণ করে। এই ভেক্টরটির টাইপ হলো Box<dyn Draw>
, যা একটি ট্রেইট অবজেক্ট; এটি Box
-এর ভিতরে থাকা যেকোনো টাইপের জন্য একটি বিকল্প যা Draw
ট্রেইটটি বাস্তবায়ন করে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Screen
struct-এর উপর, আমরা run
নামে একটি মেথড সংজ্ঞায়িত করব যা তার প্রতিটি components
-এর উপর draw
মেথড কল করবে, যেমনটি লিস্টিং ১৮-৫-এ দেখানো হয়েছে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
এটি একটি জেনেরিক টাইপ প্যারামিটার এবং ট্রেইট বাউন্ড ব্যবহার করে struct সংজ্ঞায়িত করার থেকে ভিন্নভাবে কাজ করে। একটি জেনেরিক টাইপ প্যারামিটার একবারে কেবল একটি সুনির্দিষ্ট টাইপ দ্বারা প্রতিস্থাপিত হতে পারে, যেখানে ট্রেইট অবজেক্ট রানটাইমে একাধিক সুনির্দিষ্ট টাইপকে ট্রেইট অবজেক্টের জায়গা পূরণ করার সুযোগ দেয়। উদাহরণস্বরূপ, আমরা Screen
struct-কে একটি জেনেরিক টাইপ এবং একটি ট্রেইট বাউন্ড ব্যবহার করে সংজ্ঞায়িত করতে পারতাম, যেমন লিস্টিং ১৮-৬-এ দেখানো হয়েছে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
এটি আমাদের এমন একটি Screen
ইনস্ট্যান্সের মধ্যে সীমাবদ্ধ করে ফেলে যার কম্পোনেন্টের তালিকায় হয় সবগুলো Button
টাইপের অথবা সবগুলো TextField
টাইপের হবে। আপনি যদি শুধুমাত্র সমজাতীয় (homogeneous) সংগ্রহ ব্যবহার করেন, তবে জেনেরিক এবং ট্রেইট বাউন্ড ব্যবহার করা শ্রেয়, কারণ সংজ্ঞাগুলো কম্পাইল টাইমে সুনির্দিষ্ট টাইপ ব্যবহার করার জন্য মনোমর্ফাইজ (monomorphized) করা হবে।
অন্যদিকে, ট্রেইট অবজেক্ট ব্যবহারকারী মেথডটির মাধ্যমে, একটি Screen
ইনস্ট্যান্স এমন একটি Vec<T>
ধারণ করতে পারে যেখানে একটি Box<Button>
এবং একটি Box<TextField>
উভয়ই থাকতে পারে। চলুন দেখি এটি কীভাবে কাজ করে এবং তারপর আমরা রানটাইম পারফরম্যান্সের প্রভাব নিয়ে আলোচনা করব।
ট্রেইট ইমপ্লিমেন্ট করা (Implementing the Trait)
এখন আমরা Draw
ট্রেইট ইমপ্লিমেন্ট করে এমন কিছু টাইপ যোগ করব। আমরা Button
টাইপটি সরবরাহ করব। আবারও বলছি, একটি GUI লাইব্রেরি বাস্তবে ইমপ্লিমেন্ট করা এই বইয়ের আওতার বাইরে, তাই draw
মেথডের বডিতে কোনো দরকারী ইমপ্লিমেন্টেশন থাকবে না। ইমপ্লিমেন্টেশনটি কেমন হতে পারে তা কল্পনা করার জন্য, একটি Button
struct-এ width
, height
, এবং label
-এর জন্য ফিল্ড থাকতে পারে, যেমনটি লিস্টিং ১৮-৭-এ দেখানো হয়েছে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
Button
-এর width
, height
, এবং label
ফিল্ডগুলো অন্যান্য কম্পোনেন্টের ফিল্ড থেকে ভিন্ন হবে; উদাহরণস্বরূপ, একটি TextField
টাইপের এই একই ফিল্ডগুলো ছাড়াও একটি placeholder
ফিল্ড থাকতে পারে। আমরা স্ক্রিনে যে সব টাইপ আঁকতে চাই, তার প্রত্যেকটি Draw
ট্রেইট ইমপ্লিমেন্ট করবে কিন্তু draw
মেথডে ভিন্ন কোড ব্যবহার করবে সেই নির্দিষ্ট টাইপটি কীভাবে আঁকতে হয় তা সংজ্ঞায়িত করার জন্য, যেমন Button
এখানে করেছে (প্রকৃত GUI কোড ছাড়া, যেমনটি উল্লেখ করা হয়েছে)। Button
টাইপটির, উদাহরণস্বরূপ, একটি অতিরিক্ত impl
ব্লক থাকতে পারে যেখানে ব্যবহারকারী বাটনে ক্লিক করলে কী ঘটবে তার সাথে সম্পর্কিত মেথড থাকবে। এই ধরণের মেথড TextField
-এর মতো টাইপের ক্ষেত্রে প্রযোজ্য হবে না।
যদি আমাদের লাইব্রেরি ব্যবহারকারী কেউ width
, height
, এবং options
ফিল্ড সহ একটি SelectBox
struct ইমপ্লিমেন্ট করার সিদ্ধান্ত নেয়, তবে সে SelectBox
টাইপের উপরও Draw
ট্রেইট ইমপ্লিমেন্ট করবে, যেমনটি লিস্টিং ১৮-৮-এ দেখানো হয়েছে।
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
আমাদের লাইব্রেরির ব্যবহারকারী এখন একটি Screen
ইনস্ট্যান্স তৈরি করার জন্য তার main
ফাংশন লিখতে পারে। Screen
ইনস্ট্যান্সে, তারা একটি SelectBox
এবং একটি Button
যোগ করতে পারে, প্রতিটিকে Box<T>
-এর মধ্যে রেখে একটি ট্রেইট অবজেক্টে পরিণত করে। তারপর তারা Screen
ইনস্ট্যান্সের উপর run
মেথড কল করতে পারে, যা প্রতিটি কম্পোনেন্টের উপর draw
কল করবে। লিস্টিং ১৮-৯ এই ইমপ্লিমেন্টেশনটি দেখায়।
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
যখন আমরা লাইব্রেরিটি লিখেছিলাম, তখন আমরা জানতাম না যে কেউ SelectBox
টাইপটি যোগ করতে পারে, কিন্তু আমাদের Screen
ইমপ্লিমেন্টেশন নতুন টাইপের উপর কাজ করতে এবং এটি আঁকতে সক্ষম হয়েছিল কারণ SelectBox
Draw
ট্রেইটটি ইমপ্লিমেন্ট করে, যার মানে এটি draw
মেথডটি ইমপ্লিমেন্ট করে।
এই ধারণাটি—একটি মানের সুনির্দিষ্ট টাইপের পরিবর্তে শুধুমাত্র মানটি কোন বার্তাগুলিতে সাড়া দেয় সে সম্পর্কে উদ্বিগ্ন থাকা—ডাইনামিক্যালি টাইপড ল্যাঙ্গুয়েজগুলোর ডাক টাইপিং (duck typing) ধারণার মতো: যদি এটি হাঁসের মতো হাঁটে এবং হাঁসের মতো ডাকে, তবে এটি অবশ্যই একটি হাঁস! লিস্টিং ১৮-৫-এ Screen
-এর run
ইমপ্লিমেন্টেশনে, run
-কে জানতে হবে না প্রতিটি কম্পোনেন্টের সুনির্দিষ্ট টাইপ কী। এটি পরীক্ষা করে না যে একটি কম্পোনেন্ট Button
বা SelectBox
-এর ইনস্ট্যান্স কিনা, এটি কেবল কম্পোনেন্টের উপর draw
মেথড কল করে। components
ভেক্টরের মানগুলোর টাইপ হিসাবে Box<dyn Draw>
নির্দিষ্ট করে, আমরা Screen
-কে এমনভাবে সংজ্ঞায়িত করেছি যার এমন মান প্রয়োজন যার উপর আমরা draw
মেথড কল করতে পারি।
ডাক টাইপিং ব্যবহার করে লেখা কোডের মতো কোড লেখার জন্য ট্রেইট অবজেক্ট এবং Rust-এর টাইপ সিস্টেম ব্যবহার করার সুবিধা হলো আমাদের রানটাইমে কখনও পরীক্ষা করতে হবে না যে একটি মান একটি নির্দিষ্ট মেথড ইমপ্লিমেন্ট করে কিনা বা একটি মান মেথডটি ইমপ্লিমেন্ট না করলেও আমরা যদি তা কল করি তবে ত্রুটি পাওয়ার বিষয়ে চিন্তা করতে হবে না। যদি মানগুলো ট্রেইট অবজেক্টের প্রয়োজনীয় ট্রেইটগুলো ইমপ্লিমেন্ট না করে তবে Rust আমাদের কোড কম্পাইল করবে না।
উদাহরণস্বরূপ, লিস্টিং ১৮-১০ দেখায় যে আমরা যদি একটি String
-কে কম্পোনেন্ট হিসাবে নিয়ে একটি Screen
তৈরি করার চেষ্টা করি তবে কী ঘটে।
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
আমরা এই ত্রুটিটি পাব কারণ String
Draw
ট্রেইটটি ইমপ্লিমেন্ট করে না:
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
এই ত্রুটিটি আমাদের জানায় যে হয় আমরা Screen
-কে এমন কিছু পাঠাচ্ছি যা আমরা পাঠাতে চাইনি এবং তাই একটি ভিন্ন টাইপ পাঠানো উচিত, অথবা আমাদের String
-এর উপর Draw
ইমপ্লিমেন্ট করা উচিত যাতে Screen
এটির উপর draw
কল করতে পারে।
ট্রেইট অবজেক্ট ডাইনামিক ডিসপ্যাচ সম্পাদন করে (Trait Objects Perform Dynamic Dispatch)
১০ম অধ্যায়ে "জেনেরিক ব্যবহারকারী কোডের পারফরম্যান্স" অংশে কম্পাইলার দ্বারা জেনেরিকের উপর সঞ্চালিত মনোমর্ফাইজেশন প্রক্রিয়া সম্পর্কে আমাদের আলোচনা স্মরণ করুন: কম্পাইলার ফাংশন এবং মেথডগুলোর নন-জেনেরিক ইমপ্লিমেন্টেশন তৈরি করে প্রতিটি সুনির্দিষ্ট টাইপের জন্য যা আমরা একটি জেনেরিক টাইপ প্যারামিটারের জায়গায় ব্যবহার করি। মনোমর্ফাইজেশনের ফলে যে কোড তৈরি হয় তা স্ট্যাটিক ডিসপ্যাচ (static dispatch) করছে, যা তখন ঘটে যখন কম্পাইলার কম্পাইল টাইমে জানে আপনি কোন মেথড কল করছেন। এটি ডাইনামিক ডিসপ্যাচ (dynamic dispatch)-এর বিপরীত, যা তখন ঘটে যখন কম্পাইলার কম্পাইল টাইমে বলতে পারে না আপনি কোন মেথড কল করছেন। ডাইনামিক ডিসপ্যাচের ক্ষেত্রে, কম্পাইলার এমন কোড তৈরি করে যা রানটাইমে জানবে কোন মেথড কল করতে হবে।
যখন আমরা ট্রেইট অবজেক্ট ব্যবহার করি, তখন Rust-কে অবশ্যই ডাইনামিক ডিসপ্যাচ ব্যবহার করতে হবে। কম্পাইলার সমস্ত টাইপ জানে না যা ট্রেইট অবজেক্ট ব্যবহারকারী কোডের সাথে ব্যবহৃত হতে পারে, তাই এটি জানে না কোন টাইপের উপর ইমপ্লিমেন্ট করা কোন মেথড কল করতে হবে। পরিবর্তে, রানটাইমে, Rust ট্রেইট অবজেক্টের ভিতরের পয়েন্টারগুলো ব্যবহার করে জানে কোন মেথড কল করতে হবে। এই লুকআপ একটি রানটাইম খরচ বহন করে যা স্ট্যাটিক ডিসপ্যাচের সাথে ঘটে না। ডাইনামিক ডিসপ্যাচ কম্পাইলারকে একটি মেথডের কোড ইনলাইন করার পছন্দ থেকেও বিরত রাখে, যা ফলস্বরূপ কিছু অপটিমাইজেশন প্রতিরোধ করে। এবং Rust-এর কিছু নিয়ম আছে যেখানে আপনি ডাইনামিক ডিসপ্যাচ ব্যবহার করতে পারেন এবং কোথায় পারেন না, যাকে ডাইন কম্প্যাটিবিলিটি (dyn compatibility) বলা হয়। সেই নিয়মগুলি এই আলোচনার আওতার বাইরে, তবে আপনি তাদের সম্পর্কে আরও পড়তে পারেন রেফারেন্সে। তবে, আমরা লিস্টিং ১৮-৫-এ যে কোড লিখেছিলাম তাতে অতিরিক্ত নমনীয়তা পেয়েছিলাম এবং লিস্টিং ১৮-৯-এ সমর্থন করতে পেরেছিলাম, তাই এটি বিবেচনা করার মতো একটি ট্রেড-অফ।
একটি অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্ন ইমপ্লিমেন্ট করা (Implementing an Object-Oriented Design Pattern)
স্টেট প্যাটার্ন (state pattern) একটি অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্ন। এই প্যাটার্নের মূল ভিত্তি হলো, আমরা একটি ভ্যালুর সম্ভাব্য অভ্যন্তরীণ অবস্থা বা state-গুলোর একটি সেট নির্ধারণ করি। এই state-গুলোকে এক সেট স্টেট অবজেক্ট (state objects) দ্বারা প্রকাশ করা হয় এবং ভ্যালুটির state অনুযায়ী তার আচরণ পরিবর্তিত হয়। আমরা একটি ব্লগ পোস্টের struct-এর উদাহরণ নিয়ে কাজ করব, যার একটি ফিল্ড তার state ধরে রাখবে, এবং এই state অবজেক্টটি হবে "ড্রাফট" (draft), "রিভিউ" (review) বা "পাবলিশড" (published) এই তিনটির মধ্যে একটি।
স্টেট অবজেক্টগুলো কিছু কার্যকারিতা (functionality) শেয়ার করে: Rust-এ আমরা অবশ্যই অবজেক্ট এবং ইনহেরিটেন্সের পরিবর্তে struct এবং trait ব্যবহার করি। প্রতিটি স্টেট অবজেক্ট তার নিজের আচরণের জন্য এবং কখন অন্য state-এ পরিবর্তিত হবে তা নিয়ন্ত্রণের জন্য দায়ী। যে ভ্যালুটি স্টেট অবজেক্ট ধারণ করে, সে state-গুলোর বিভিন্ন আচরণ বা কখন তাদের মধ্যে পরিবর্তন হবে সে সম্পর্কে কিছুই জানে না।
স্টেট প্যাটার্ন ব্যবহারের সুবিধা হলো, যখন প্রোগ্রামের ব্যবসায়িক প্রয়োজনীয়তা (business requirements) পরিবর্তিত হয়, তখন আমাদের state ধারণকারী ভ্যালুর কোড বা সেই ভ্যালু ব্যবহারকারী কোড পরিবর্তন করতে হবে না। আমাদের শুধুমাত্র কোনো একটি স্টেট অবজেক্টের ভেতরের কোড আপডেট করে তার নিয়ম পরিবর্তন করতে হবে অথবা প্রয়োজনে আরও স্টেট অবজেক্ট যোগ করতে হবে।
প্রথমে আমরা স্টেট প্যাটার্নটি একটি প্রচলিত অবজেক্ট-ওরিয়েন্টেড পদ্ধতিতে ইমপ্লিমেন্ট করব, তারপর আমরা এমন একটি পদ্ধতি ব্যবহার করব যা Rust-এ কিছুটা বেশি স্বাভাবিক। চলুন, স্টেট প্যাটার্ন ব্যবহার করে একটি ব্লগ পোস্টের ওয়ার্কফ্লো ধাপে ধাপে ইমপ্লিমেন্ট করা যাক।
চূড়ান্ত কার্যকারিতাটি দেখতে এইরকম হবে:
- একটি ব্লগ পোস্ট একটি খালি ড্রাফট হিসাবে শুরু হয়।
- ড্রাফট লেখা শেষ হলে, পোস্টটির একটি রিভিউয়ের জন্য অনুরোধ করা হয়।
- পোস্টটি অনুমোদিত (approved) হলে, এটি পাবলিশড হয়ে যায়।
- শুধুমাত্র পাবলিশড ব্লগ পোস্টগুলোই প্রিন্ট করার জন্য কন্টেন্ট ফেরত দেয়, যাতে অননুমোদিত পোস্টগুলো ভুলবশত পাবলিশড না হয়ে যায়।
একটি পোস্টে অন্য কোনো পরিবর্তন করার চেষ্টা করলে তার কোনো প্রভাব থাকবে না। উদাহরণস্বরূপ, যদি আমরা রিভিউয়ের অনুরোধ করার আগেই একটি ড্রাফট ব্লগ পোস্ট অনুমোদন করার চেষ্টা করি, পোস্টটি একটি অপ্রকাশিত ড্রাফট হিসেবেই থাকবে।
একটি প্রচলিত অবজেক্ট-ওরিয়েন্টেড প্রচেষ্টা (A Traditional Object-oriented Attempt)
একই সমস্যা সমাধানের জন্য কোড গঠন করার অসীম উপায় রয়েছে, প্রতিটিরই ভিন্ন ভিন্ন সুবিধা-অসুবিধা (trade-offs) আছে। এই সেকশনের ইমপ্লিমেন্টেশনটি অনেকটা প্রচলিত অবজেক্ট-ওরিয়েন্টেড ধরনের, যা Rust-এ লেখা সম্ভব, কিন্তু এটি Rust-এর কিছু শক্তিশালী দিকের সুবিধা নেয় না। পরে, আমরা একটি ভিন্ন সমাধান দেখাব যা এখনও অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্ন ব্যবহার করে কিন্তু এমনভাবে গঠন করা হয়েছে যা অবজেক্ট-ওরিয়েন্টেড অভিজ্ঞতাসম্পন্ন প্রোগ্রামারদের কাছে কিছুটা অপরিচিত মনে হতে পারে। আমরা দুটি সমাধানের তুলনা করে দেখব যে অন্য ল্যাঙ্গুয়েজের কোডের চেয়ে ভিন্নভাবে Rust কোড ডিজাইন করার সুবিধা-অসুবিধাগুলো কী।
লিস্টিং ১৮-১১ এই ওয়ার্কফ্লোটিকে কোড আকারে দেখায়: এটি blog
নামে একটি লাইব্রেরি ক্রেটে আমরা যে API ইমপ্লিমেন্ট করব তার একটি উদাহরণ। এটি এখনও কম্পাইল হবে না কারণ আমরা blog
ক্রেটটি ইমপ্লিমেন্ট করিনি।
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
আমরা ব্যবহারকারীকে Post::new
দিয়ে একটি নতুন ড্রাফট ব্লগ পোস্ট তৈরি করার সুযোগ দিতে চাই। আমরা ব্লগ পোস্টে টেক্সট যোগ করার অনুমতি দিতে চাই। যদি আমরা অনুমোদনের আগে অবিলম্বে পোস্টের কন্টেন্ট পাওয়ার চেষ্টা করি, আমাদের কোনো টেক্সট পাওয়া উচিত নয় কারণ পোস্টটি এখনও একটি ড্রাফট। আমরা প্রদর্শনের উদ্দেশ্যে কোডে assert_eq!
যোগ করেছি। এর জন্য একটি চমৎকার ইউনিট টেস্ট হতে পারত यह নিশ্চিত করা যে একটি ড্রাফট ব্লগ পোস্ট content
মেথড থেকে একটি খালি স্ট্রিং ফেরত দেয়, কিন্তু আমরা এই উদাহরণের জন্য টেস্ট লিখছি না।
এরপর, আমরা পোস্টের রিভিউয়ের জন্য একটি অনুরোধ সক্ষম করতে চাই, এবং রিভিউয়ের জন্য অপেক্ষারত অবস্থায় আমরা চাই content
একটি খালি স্ট্রিং ফেরত দিক। যখন পোস্টটি অনুমোদন পাবে, তখন এটি পাবলিশড হওয়া উচিত, যার মানে content
কল করা হলে পোস্টের টেক্সট ফেরত দেওয়া হবে।
লক্ষ্য করুন যে আমরা ক্রেট থেকে শুধুমাত্র Post
টাইপের সাথেই ইন্টারঅ্যাক্ট করছি। এই টাইপটি স্টেট প্যাটার্ন ব্যবহার করবে এবং একটি মান ধারণ করবে যা তিনটি স্টেট অবজেক্টের মধ্যে একটি হবে, যা একটি পোস্টের বিভিন্ন অবস্থা—ড্রাফট, রিভিউ, বা পাবলিশড—উপস্থাপন করবে। এক state থেকে অন্য state-এ পরিবর্তন Post
টাইপের অভ্যন্তরে পরিচালিত হবে। আমাদের লাইব্রেরির ব্যবহারকারীরা Post
ইনস্ট্যান্সের উপর যে মেথডগুলো কল করে তার প্রতিক্রিয়ায় state-গুলো পরিবর্তিত হয়, কিন্তু তাদের সরাসরি state পরিবর্তন পরিচালনা করতে হয় না। এছাড়াও, ব্যবহারকারীরা state-গুলো নিয়ে ভুল করতে পারে না, যেমন রিভিউয়ের আগে একটি পোস্ট পাবলিশ করা।
Post
সংজ্ঞায়িত করা এবং ড্রাফট অবস্থায় একটি নতুন ইনস্ট্যান্স তৈরি করা
চলুন লাইব্রেরির ইমপ্লিমেন্টেশন শুরু করা যাক! আমরা জানি আমাদের একটি পাবলিক Post
struct প্রয়োজন যা কিছু কন্টেন্ট ধারণ করবে, তাই আমরা struct-টির সংজ্ঞা এবং একটি Post
-এর ইনস্ট্যান্স তৈরি করার জন্য একটি সংশ্লিষ্ট পাবলিক new
ফাংশন দিয়ে শুরু করব, যেমনটি লিস্টিং ১৮-১২-তে দেখানো হয়েছে। আমরা একটি প্রাইভেট State
trait-ও তৈরি করব যা একটি Post
-এর জন্য সমস্ত স্টেট অবজেক্টের যে আচরণ থাকতে হবে তা সংজ্ঞায়িত করবে।
তারপর Post
একটি state
নামের প্রাইভেট ফিল্ডে Option<T>
-এর ভিতরে Box<dyn State>
-এর একটি ট্রেইট অবজেক্ট ধারণ করবে স্টেট অবজেক্ট রাখার জন্য। আপনি একটু পরেই দেখতে পাবেন কেন Option<T>
প্রয়োজন।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
State
trait বিভিন্ন পোস্ট state দ্বারা শেয়ার করা আচরণকে সংজ্ঞায়িত করে। স্টেট অবজেক্টগুলো হলো Draft
, PendingReview
, এবং Published
, এবং তারা সবাই State
trait ইমপ্লিমেন্ট করবে। আপাতত, trait-টির কোনো মেথড নেই, এবং আমরা কেবল Draft
state সংজ্ঞায়িত করে শুরু করব কারণ আমরা চাই একটি পোস্ট এই state-এ শুরু হোক।
যখন আমরা একটি নতুন Post
তৈরি করি, আমরা এর state
ফিল্ডটিকে একটি Some
মান দিয়ে সেট করি যা একটি Box
ধারণ করে। এই Box
একটি Draft
struct-এর নতুন ইনস্ট্যান্সের দিকে নির্দেশ করে। এটি নিশ্চিত করে যে যখনই আমরা Post
-এর একটি নতুন ইনস্ট্যান্স তৈরি করব, এটি একটি ড্রাফট হিসাবে শুরু হবে। যেহেতু Post
-এর state
ফিল্ডটি প্রাইভেট, তাই অন্য কোনো state-এ একটি Post
তৈরি করার কোনো উপায় নেই! Post::new
ফাংশনে, আমরা content
ফিল্ডটিকে একটি নতুন, খালি String
-এ সেট করি।
পোস্ট কন্টেন্টের টেক্সট সংরক্ষণ করা
আমরা লিস্টিং ১৮-১১-তে দেখেছি যে আমরা add_text
নামের একটি মেথড কল করতে এবং এটিকে একটি &str
পাস করতে সক্ষম হতে চাই যা ব্লগ পোস্টের টেক্সট কন্টেন্ট হিসাবে যোগ করা হবে। আমরা এটি একটি মেথড হিসাবে ইমপ্লিমেন্ট করি, content
ফিল্ডটিকে pub
হিসাবে প্রকাশ করার পরিবর্তে, যাতে পরে আমরা এমন একটি মেথড ইমপ্লিমেন্ট করতে পারি যা content
ফিল্ডের ডেটা কীভাবে পড়া হবে তা নিয়ন্ত্রণ করবে। add_text
মেথডটি বেশ সহজবোধ্য, তাই চলুন লিস্টিং ১৮-১৩-এর ইমপ্লিমেন্টেশনটি impl Post
ব্লকে যোগ করি।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
trait State {}
struct Draft {}
impl State for Draft {}
add_text
মেথডটি self
-এর একটি মিউটেবল রেফারেন্স নেয় কারণ আমরা যে Post
ইনস্ট্যান্সের উপর add_text
কল করছি তা পরিবর্তন করছি। তারপর আমরা content
-এর String
-এর উপর push_str
কল করি এবং সংরক্ষিত content
-এ যোগ করার জন্য text
আর্গুমেন্টটি পাস করি। এই আচরণটি পোস্টটি কোন state-এ আছে তার উপর নির্ভর করে না, তাই এটি স্টেট প্যাটার্নের অংশ নয়। add_text
মেথডটি state
ফিল্ডের সাথে একেবারেই ইন্টারঅ্যাক্ট করে না, তবে এটি আমরা যে আচরণ সমর্থন করতে চাই তার অংশ।
একটি ড্রাফট পোস্টের কন্টেন্ট খালি নিশ্চিত করা
এমনকি আমরা add_text
কল করে আমাদের পোস্টে কিছু কন্টেন্ট যোগ করার পরেও, আমরা চাই content
মেথডটি একটি খালি স্ট্রিং স্লাইস ফেরত দিক কারণ পোস্টটি এখনও ড্রাফট state-এ আছে, যেমনটি লিস্টিং ১৮-১১-এর ৭ নম্বর লাইনে দেখানো হয়েছে। আপাতত, চলুন content
মেথডটি সবচেয়ে সহজ জিনিস দিয়ে ইমপ্লিমেন্ট করি যা এই প্রয়োজনীয়তা পূরণ করবে: সর্বদা একটি খালি স্ট্রিং স্লাইস ফেরত দেওয়া। আমরা এটি পরে পরিবর্তন করব যখন আমরা একটি পোস্টের state পরিবর্তন করার ক্ষমতা ইমপ্লিমেন্ট করব যাতে এটি পাবলিশড হতে পারে। এখন পর্যন্ত, পোস্টগুলো কেবল ড্রাফট state-এ থাকতে পারে, তাই পোস্ট কন্টেন্ট সর্বদা খালি থাকা উচিত। লিস্টিং ১৮-১৪ এই স্থানধারক (placeholder) ইমপ্লিমেন্টেশনটি দেখায়।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
}
trait State {}
struct Draft {}
impl State for Draft {}
এই content
মেথডটি যোগ করার সাথে, লিস্টিং ১৮-১১-এর ৭ নম্বর লাইন পর্যন্ত সবকিছু উদ্দেশ্য অনুযায়ী কাজ করে।
একটি রিভিউয়ের অনুরোধ পোস্টের স্টেট পরিবর্তন করে
এরপরে, আমাদের একটি পোস্টের রিভিউয়ের অনুরোধ করার জন্য কার্যকারিতা যোগ করতে হবে, যা এর state Draft
থেকে PendingReview
-এ পরিবর্তন করবে। লিস্টিং ১৮-১৫ এই কোডটি দেখায়।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
আমরা Post
-কে request_review
নামে একটি পাবলিক মেথড দিই যা self
-এর একটি মিউটেবল রেফারেন্স নেবে। তারপর আমরা Post
-এর বর্তমান state-এর উপর একটি অভ্যন্তরীণ request_review
মেথড কল করি, এবং এই দ্বিতীয় request_review
মেথডটি বর্তমান state-কে কনজিউম (consume) করে এবং একটি নতুন state ফেরত দেয়।
আমরা State
trait-এ request_review
মেথডটি যোগ করি; যে সমস্ত টাইপ trait-টি ইমপ্লিমেন্ট করে তাদের এখন request_review
মেথড ইমপ্লিমেন্ট করতে হবে। লক্ষ্য করুন যে মেথডের প্রথম প্যারামিটার হিসাবে self
, &self
, বা &mut self
থাকার পরিবর্তে, আমাদের আছে self: Box<Self>
। এই সিনট্যাক্সটির মানে হলো মেথডটি কেবল তখনই বৈধ যখন এটি টাইপ ধারণকারী একটি Box
-এর উপর কল করা হয়। এই সিনট্যাক্সটি Box<Self>
-এর মালিকানা নেয়, পুরানো state-কে অবৈধ করে দেয় যাতে Post
-এর state ভ্যালুটি একটি নতুন state-এ রূপান্তরিত হতে পারে।
পুরানো state কনজিউম করার জন্য, request_review
মেথডটিকে state ভ্যালুর মালিকানা নিতে হবে। এখানেই Post
-এর state
ফিল্ডে Option
-এর ভূমিকা আসে: আমরা state
ফিল্ড থেকে Some
ভ্যালুটি বের করে নিতে এবং তার জায়গায় একটি None
রেখে দিতে take
মেথড কল করি, কারণ Rust আমাদের struct-এ খালি ফিল্ড রাখতে দেয় না। এটি আমাদের Post
থেকে state
ভ্যালুটি borrow করার পরিবর্তে move করার সুযোগ দেয়। তারপর আমরা পোস্টের state
ভ্যালুটি এই অপারেশনের ফলাফলে সেট করব।
আমাদের state
ভ্যালুর মালিকানা পেতে self.state = self.state.request_review();
-এর মতো কোড দিয়ে সরাসরি সেট করার পরিবর্তে state
-কে সাময়িকভাবে None
হিসাবে সেট করতে হবে। এটি নিশ্চিত করে যে আমরা পুরানো state
ভ্যালুটি একটি নতুন state-এ রূপান্তরিত করার পরে Post
আর সেটি ব্যবহার করতে পারবে না।
Draft
-এর উপর request_review
মেথডটি একটি নতুন PendingReview
struct-এর একটি নতুন, বক্সড ইনস্ট্যান্স ফেরত দেয়, যা সেই state-কে প্রতিনিধিত্ব করে যখন একটি পোস্ট রিভিউয়ের জন্য অপেক্ষা করছে। PendingReview
struct-টিও request_review
মেথড ইমপ্লিমেন্ট করে কিন্তু কোনো রূপান্তর করে না। বরং, এটি নিজেকেই ফেরত দেয় কারণ যখন আমরা এমন একটি পোস্টে রিভিউয়ের অনুরোধ করি যা ইতিমধ্যে PendingReview
state-এ আছে, তখন এটি PendingReview
state-এ থাকা উচিত।
এখন আমরা স্টেট প্যাটার্নের সুবিধা দেখতে শুরু করতে পারি: Post
-এর উপর request_review
মেথডটি তার state
ভ্যালু যাই হোক না কেন একই থাকে। প্রতিটি state তার নিজের নিয়মের জন্য দায়ী।
আমরা Post
-এর content
মেথডটি যেমন আছে তেমনই রাখব, একটি খালি স্ট্রিং স্লাইস ফেরত দিয়ে। আমরা এখন একটি Post
-কে Draft
state-এর পাশাপাশি PendingReview
state-এও রাখতে পারি, কিন্তু আমরা PendingReview
state-এও একই আচরণ চাই। লিস্টিং ১৮-১১ এখন ১০ নম্বর লাইন পর্যন্ত কাজ করে!
content
-এর আচরণ পরিবর্তনকারী approve
মেথড যোগ করা
approve
মেথডটি request_review
মেথডের মতোই হবে: এটি state
-কে সেই মানে সেট করবে যা বর্তমান state বলে যে অনুমোদিত হলে তার থাকা উচিত, যেমনটি লিস্টিং ১৮-১৬-তে দেখানো হয়েছে।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
আমরা State
trait-এ approve
মেথড যোগ করি এবং State
ইমপ্লিমেন্টকারী একটি নতুন struct, Published
state, যোগ করি।
PendingReview
-এর উপর request_review
যেভাবে কাজ করে তার মতোই, যদি আমরা একটি Draft
-এর উপর approve
মেথড কল করি, তবে এর কোনো প্রভাব থাকবে না কারণ approve
self
ফেরত দেবে। যখন আমরা PendingReview
-এর উপর approve
কল করি, তখন এটি Published
struct-এর একটি নতুন, বক্সড ইনস্ট্যান্স ফেরত দেয়। Published
struct-টি State
trait ইমপ্লিমেন্ট করে, এবং request_review
ও approve
উভয় মেথডের জন্য, এটি নিজেকেই ফেরত দেয় কারণ পোস্টটি সেইসব ক্ষেত্রে Published
state-এ থাকা উচিত।
এখন আমাদের Post
-এর content
মেথডটি আপডেট করতে হবে। আমরা চাই content
থেকে ফেরত আসা মানটি Post
-এর বর্তমান state-এর উপর নির্ভর করুক, তাই আমরা Post
-কে তার state
-এর উপর সংজ্ঞায়িত একটি content
মেথডে কাজটা सौंप (delegate) দেব, যেমনটি লিস্টিং ১৮-১৭-তে দেখানো হয়েছে।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
যেহেতু লক্ষ্য হলো এই সমস্ত নিয়ম State
ইমপ্লিমেন্টকারী struct-গুলোর ভিতরে রাখা, তাই আমরা state
-এর মানের উপর একটি content
মেথড কল করি এবং পোস্ট ইনস্ট্যান্সটি (অর্থাৎ self
) একটি আর্গুমেন্ট হিসাবে পাস করি। তারপর আমরা state
মানের উপর content
মেথড ব্যবহার করে যে মান ফেরত আসে তা ফেরত দিই।
আমরা Option
-এর উপর as_ref
মেথড কল করি কারণ আমরা মানের মালিকানার পরিবর্তে Option
-এর ভিতরের মানের একটি রেফারেন্স চাই। যেহেতু state
একটি Option<Box<dyn State>>
, যখন আমরা as_ref
কল করি, তখন একটি Option<&Box<dyn State>>
ফেরত আসে। যদি আমরা as_ref
কল না করতাম, আমরা একটি ত্রুটি পেতাম কারণ আমরা ফাংশন প্যারামিটারের ধার করা &self
থেকে state
মুভ করতে পারি না।
তারপর আমরা unwrap
মেথড কল করি, যা আমরা জানি কখনও প্যানিক করবে না কারণ আমরা জানি Post
-এর মেথডগুলো নিশ্চিত করে যে সেই মেথডগুলো শেষ হলে state
-এ সর্বদা একটি Some
মান থাকবে। এটি সেই случайগুলোর মধ্যে একটি যা আমরা ৯ অধ্যায়ে "যেসব ক্ষেত্রে আপনার কাছে কম্পাইলারের চেয়ে বেশি তথ্য থাকে" অংশে আলোচনা করেছি যখন আমরা জানি যে একটি None
মান কখনও সম্ভব নয়, যদিও কম্পাইলার এটি বুঝতে সক্ষম নয়।
এই মুহুর্তে, যখন আমরা &Box<dyn State>
-এর উপর content
কল করি, তখন &
এবং Box
-এর উপর ডিরেফ কোয়ার্সন (deref coercion) কার্যকর হবে যাতে content
মেথডটি শেষ পর্যন্ত State
trait ইমপ্লিমেন্টকারী টাইপের উপর কল করা হয়। এর মানে হলো আমাদের State
trait সংজ্ঞায় content
যোগ করতে হবে, এবং সেখানেই আমরা কোন state আছে তার উপর নির্ভর করে কোন কন্টেন্ট ফেরত দিতে হবে তার লজিক রাখব, যেমনটি লিস্টিং ১৮-১৮-তে দেখানো হয়েছে।
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
আমরা content
মেথডের জন্য একটি ডিফল্ট ইমপ্লিমেন্টেশন যোগ করি যা একটি খালি স্ট্রিং স্লাইস ফেরত দেয়। এর মানে আমাদের Draft
এবং PendingReview
struct-এ content
ইমপ্লিমেন্ট করার প্রয়োজন নেই। Published
struct content
মেথডকে ওভাররাইড করবে এবং post.content
-এর মান ফেরত দেবে। যদিও সুবিধাজনক, State
-এর content
মেথড Post
-এর content
নির্ধারণ করা State
এবং Post
-এর দায়িত্বের মধ্যেকার সীমানাকে অস্পষ্ট করে তুলছে।
লক্ষ্য করুন যে আমাদের এই মেথডে লাইফটাইম অ্যানোটেশন প্রয়োজন, যেমনটি আমরা ১০ অধ্যায়ে আলোচনা করেছি। আমরা একটি post
-এর রেফারেন্স একটি আর্গুমেন্ট হিসাবে নিচ্ছি এবং সেই post
-এর একটি অংশের রেফারেন্স ফেরত দিচ্ছি, তাই ফেরত দেওয়া রেফারেন্সের লাইফটাইম post
আর্গুমেন্টের লাইফটাইমের সাথে সম্পর্কিত।
এবং আমরা শেষ করেছি—লিস্টিং ১৮-১১ এখন পুরোটাই কাজ করে! আমরা ব্লগ পোস্ট ওয়ার্কফ্লোর নিয়মাবলী দিয়ে স্টেট প্যাটার্ন ইমপ্লিমেন্ট করেছি। নিয়ম সম্পর্কিত লজিক Post
-এ ছড়িয়ে ছিটিয়ে থাকার পরিবর্তে স্টেট অবজেক্টগুলোর মধ্যে থাকে।
কেন একটি Enum নয়? (Why Not An Enum?)
আপনি হয়তো ভাবছেন কেন আমরা বিভিন্ন সম্ভাব্য পোস্ট state-কে ভ্যারিয়েন্ট হিসাবে নিয়ে একটি
enum
ব্যবহার করিনি। এটি অবশ্যই একটি সম্ভাব্য সমাধান; চেষ্টা করে দেখুন এবং চূড়ান্ত ফলাফল তুলনা করে দেখুন কোনটি আপনার পছন্দ! একটি enum ব্যবহারের একটি অসুবিধা হলো, enum-এর মান পরীক্ষা করে এমন প্রতিটি জায়গায় প্রতিটি সম্ভাব্য ভ্যারিয়েন্ট পরিচালনা করার জন্য একটিmatch
এক্সপ্রেশন বা অনুরূপ কিছুর প্রয়োজন হবে। এটি এই ট্রেইট অবজেক্ট সমাধানের চেয়ে বেশি পুনরাবৃত্তিমূলক (repetitive) হতে পারে।
স্টেট প্যাটার্নের সুবিধা-অসুবিধা (Trade-offs of the State Pattern)
আমরা দেখিয়েছি যে Rust অবজেক্ট-ওরিয়েন্টেড স্টেট প্যাটার্ন ইমপ্লিমেন্ট করতে সক্ষম, যাতে প্রতিটি state-এ একটি পোস্টের বিভিন্ন ধরণের আচরণকে এনক্যাপসুলেট করা যায়। Post
-এর মেথডগুলো বিভিন্ন আচরণ সম্পর্কে কিছুই জানে না। আমরা যেভাবে কোডটি সাজিয়েছি, তাতে একটি পাবলিশড পোস্টের বিভিন্ন আচরণ জানার জন্য আমাদের কেবল একটি জায়গায় তাকাতে হবে: Published
struct-এর উপর State
trait-এর ইমপ্লিমেন্টেশন।
যদি আমরা একটি বিকল্প ইমপ্লিমেন্টেশন তৈরি করতাম যা স্টেট প্যাটার্ন ব্যবহার করে না, আমরা হয়তো Post
-এর মেথডগুলোতে বা এমনকি main
কোডেও match
এক্সপ্রেশন ব্যবহার করতাম যা পোস্টের state পরীক্ষা করে এবং সেই জায়গাগুলোতে আচরণ পরিবর্তন করে। এর মানে হতো একটি পোস্ট পাবলিশড অবস্থায় থাকার সমস্ত প্রভাব বোঝার জন্য আমাদের বেশ কয়েকটি জায়গায় তাকাতে হতো।
স্টেট প্যাটার্নের সাথে, Post
মেথড এবং যেখানে আমরা Post
ব্যবহার করি সেখানে match
এক্সপ্রেশনের প্রয়োজন হয় না, এবং একটি নতুন state যোগ করার জন্য, আমাদের কেবল একটি নতুন struct যোগ করতে হবে এবং সেই একটি struct-এর উপর trait মেথডগুলো এক জায়গায় ইমপ্লিমেন্ট করতে হবে।
স্টেট প্যাটার্ন ব্যবহার করে ইমপ্লিমেন্টেশনটি আরও কার্যকারিতা যোগ করার জন্য প্রসারিত করা সহজ। স্টেট প্যাটার্ন ব্যবহার করে কোড রক্ষণাবেক্ষণের সরলতা দেখতে, এই পরামর্শগুলোর কয়েকটি চেষ্টা করুন:
- একটি
reject
মেথড যোগ করুন যা পোস্টের statePendingReview
থেকেDraft
-এ ফিরিয়ে আনে। - state
Published
-এ পরিবর্তন করার আগেapprove
-এর দুটি কল প্রয়োজন করুন। - ব্যবহারকারীদের শুধুমাত্র
Draft
state-এ থাকা অবস্থায় টেক্সট কন্টেন্ট যোগ করার অনুমতি দিন। ইঙ্গিত: স্টেট অবজেক্টকে কন্টেন্ট সম্পর্কে কী পরিবর্তন হতে পারে তার জন্য দায়ী করুন, কিন্তুPost
পরিবর্তন করার জন্য দায়ী নয়।
স্টেট প্যাটার্নের একটি অসুবিধা হলো, যেহেতু state-গুলো state-গুলোর মধ্যে রূপান্তর ইমপ্লিমেন্ট করে, তাই কিছু state একে অপরের সাথে সংযুক্ত (coupled)। যদি আমরা PendingReview
এবং Published
-এর মধ্যে Scheduled
-এর মতো আরেকটি state যোগ করি, তবে আমাদের PendingReview
-এর কোড পরিবর্তন করে Scheduled
-এ রূপান্তর করতে হবে। যদি একটি নতুন state যোগ করার সাথে PendingReview
-কে পরিবর্তন করার প্রয়োজন না হতো তবে কম কাজ হতো, কিন্তু তার মানে হতো অন্য একটি ডিজাইন প্যাটার্নে স্যুইচ করা।
আরেকটি অসুবিধা হলো আমরা কিছু লজিক ডুপ্লিকেট করেছি। কিছু ডুপ্লিকেশন দূর করার জন্য, আমরা State
trait-এ request_review
এবং approve
মেথডগুলোর জন্য ডিফল্ট ইমপ্লিমেন্টেশন তৈরি করার চেষ্টা করতে পারি যা self
ফেরত দেয়। তবে, এটি কাজ করবে না: State
-কে একটি ট্রেইট অবজেক্ট হিসাবে ব্যবহার করার সময়, trait জানে না যে সুনির্দিষ্ট self
ঠিক কী হবে, তাই রিটার্ন টাইপ কম্পাইল টাইমে জানা যায় না। (এটি পূর্বে উল্লিখিত dyn
কম্প্যাটিবিলিটি নিয়মগুলোর মধ্যে একটি।)
অন্যান্য ডুপ্লিকেশনের মধ্যে রয়েছে Post
-এর উপর request_review
এবং approve
মেথডগুলোর অনুরূপ ইমপ্লিমেন্টেশন। উভয় মেথডই Post
-এর state
ফিল্ডের সাথে Option::take
ব্যবহার করে, এবং যদি state
Some
হয়, তবে তারা র্যাপড (wrapped) ভ্যালুর একই মেথডের ইমপ্লিমেন্টেশনে কাজটা सौंप দেয় এবং state
ফিল্ডের নতুন মানটি ফলাফলে সেট করে। যদি Post
-এ এই প্যাটার্ন অনুসরণকারী অনেক মেথড থাকতো, তবে আমরা পুনরাবৃত্তি দূর করার জন্য একটি ম্যাক্রো সংজ্ঞায়িত করার কথা বিবেচনা করতে পারতাম (২০তম অধ্যায়ে "ম্যাক্রো" দেখুন)।
অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজের জন্য ঠিক যেভাবে স্টেট প্যাটার্ন সংজ্ঞায়িত করা হয়েছে সেভাবে ইমপ্লিমেন্ট করে, আমরা Rust-এর শক্তিশালী দিকগুলোর ততটা পূর্ণ সুবিধা নিচ্ছি না যতটা আমরা নিতে পারতাম। চলুন blog
ক্রেটে কিছু পরিবর্তন দেখি যা অবৈধ state এবং রূপান্তরকে কম্পাইল-টাইম ত্রুটিতে পরিণত করতে পারে।
State এবং আচরণকে টাইপ হিসাবে এনকোড করা (Encoding States and Behavior as Types)
আমরা আপনাকে দেখাব কীভাবে স্টেট প্যাটার্নটি নতুনভাবে চিন্তা করে একটি ভিন্ন ধরনের সুবিধা-অসুবিধা (trade-offs) পাওয়া যায়। state এবং রূপান্তরগুলোকে সম্পূর্ণরূপে এনক্যাপসুলেট করার পরিবর্তে যাতে বাইরের কোডের তাদের সম্পর্কে কোনো জ্ঞান না থাকে, আমরা state-গুলোকে বিভিন্ন টাইপে এনকোড করব। ফলস্বরূপ, Rust-এর টাইপ চেকিং সিস্টেম একটি কম্পাইলার ত্রুটি জারি করে এমন জায়গায় ড্রাফট পোস্ট ব্যবহার করার প্রচেষ্টা প্রতিরোধ করবে যেখানে কেবল পাবলিশড পোস্ট অনুমোদিত।
চলুন লিস্টিং ১৮-১১-এর main
-এর প্রথম অংশটি বিবেচনা করি:
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
আমরা এখনও Post::new
ব্যবহার করে ড্রাফট state-এ নতুন পোস্ট তৈরি এবং পোস্টের কন্টেন্টে টেক্সট যোগ করার ক্ষমতা সক্ষম করি। কিন্তু একটি ড্রাফট পোস্টের উপর একটি content
মেথড থাকার পরিবর্তে যা একটি খালি স্ট্রিং ফেরত দেয়, আমরা এমনভাবে তৈরি করব যাতে ড্রাফট পোস্টের content
মেথডটি একেবারেই না থাকে। এভাবে, যদি আমরা একটি ড্রাফট পোস্টের কন্টেন্ট পাওয়ার চেষ্টা করি, আমরা একটি কম্পাইলার ত্রুটি পাব যা আমাদের বলবে যে মেথডটি বিদ্যমান নেই। ফলস্বরূপ, আমাদের জন্য প্রোডাকশনে ভুলবশত ড্রাফট পোস্টের কন্টেন্ট প্রদর্শন করা অসম্ভব হয়ে যাবে কারণ সেই কোড কম্পাইলই হবে না। লিস্টিং ১৮-১৯ একটি Post
struct এবং একটি DraftPost
struct-এর সংজ্ঞা এবং প্রতিটির উপর মেথড দেখায়।
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Post
এবং DraftPost
উভয় struct-এরই একটি প্রাইভেট content
ফিল্ড আছে যা ব্লগ পোস্টের টেক্সট সংরক্ষণ করে। struct-গুলোর আর state
ফিল্ড নেই কারণ আমরা state-এর এনকোডিং struct-গুলোর টাইপে স্থানান্তরিত করছি। Post
struct একটি পাবলিশড পোস্টকে প্রতিনিধিত্ব করবে, এবং এর একটি content
মেথড আছে যা content
ফেরত দেয়।
আমাদের এখনও একটি Post::new
ফাংশন আছে, কিন্তু Post
-এর একটি ইনস্ট্যান্স ফেরত দেওয়ার পরিবর্তে, এটি DraftPost
-এর একটি ইনস্ট্যান্স ফেরত দেয়। যেহেতু content
প্রাইভেট এবং Post
ফেরত দেয় এমন কোনো ফাংশন নেই, তাই এই মুহূর্তে Post
-এর একটি ইনস্ট্যান্স তৈরি করা সম্ভব নয়।
DraftPost
struct-এর একটি add_text
মেথড আছে, তাই আমরা আগের মতোই content
-এ টেক্সট যোগ করতে পারি, কিন্তু লক্ষ্য করুন যে DraftPost
-এর কোনো content
মেথড সংজ্ঞায়িত নেই! তাই এখন প্রোগ্রাম নিশ্চিত করে যে সমস্ত পোস্ট ড্রাফট পোস্ট হিসাবে শুরু হয়, এবং ড্রাফট পোস্টগুলোর কন্টেন্ট প্রদর্শনের জন্য উপলব্ধ থাকে না। এই সীমাবদ্ধতাগুলো এড়ানোর যেকোনো প্রচেষ্টা একটি কম্পাইলার ত্রুটির কারণ হবে।
তাহলে আমরা কীভাবে একটি পাবলিশড পোস্ট পাব? আমরা এই নিয়মটি প্রয়োগ করতে চাই যে একটি ড্রাফট পোস্ট পাবলিশড হওয়ার আগে অবশ্যই রিভিউ এবং অনুমোদিত হতে হবে। পেন্ডিং রিভিউ state-এ থাকা একটি পোস্টেরও কোনো কন্টেন্ট প্রদর্শন করা উচিত নয়। চলুন এই সীমাবদ্ধতাগুলো ইমপ্লিমেন্ট করি একটি নতুন struct, PendingReviewPost
যোগ করে, DraftPost
-এর উপর request_review
মেথড সংজ্ঞায়িত করে যা একটি PendingReviewPost
ফেরত দেবে এবং PendingReviewPost
-এর উপর একটি approve
মেথড সংজ্ঞায়িত করে যা একটি Post
ফেরত দেবে, যেমনটি লিস্টিং ১৮-২০-তে দেখানো হয়েছে।
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
request_review
এবং approve
মেথডগুলো self
-এর মালিকানা নেয়, ফলে DraftPost
এবং PendingReviewPost
ইনস্ট্যান্সগুলো কনজিউম করে এবং সেগুলোকে যথাক্রমে একটি PendingReviewPost
এবং একটি পাবলিশড Post
-এ রূপান্তরিত করে। এভাবে, আমরা request_review
কল করার পরে কোনো অবশিষ্ট DraftPost
ইনস্ট্যান্স রাখব না, এবং এভাবেই চলতে থাকবে। PendingReviewPost
struct-এর কোনো content
মেথড সংজ্ঞায়িত নেই, তাই এর কন্টেন্ট পড়ার চেষ্টা করলে DraftPost
-এর মতো একটি কম্পাইলার ত্রুটি হবে। যেহেতু একটি পাবলিশড Post
ইনস্ট্যান্স পাওয়ার একমাত্র উপায় হলো PendingReviewPost
-এর উপর approve
মেথড কল করা, এবং PendingReviewPost
পাওয়ার একমাত্র উপায় হলো DraftPost
-এর উপর request_review
মেথড কল করা, তাই আমরা এখন ব্লগ পোস্ট ওয়ার্কফ্লোটিকে টাইপ সিস্টেমে এনকোড করেছি।
কিন্তু আমাদের main
-এও কিছু ছোট পরিবর্তন করতে হবে। request_review
এবং approve
মেথডগুলো যে struct-এর উপর কল করা হয় তা পরিবর্তন না করে নতুন ইনস্ট্যান্স ফেরত দেয়, তাই আমাদের ফেরত আসা ইনস্ট্যান্সগুলো সংরক্ষণ করার জন্য আরও let post =
শ্যাডোইং অ্যাসাইনমেন্ট যোগ করতে হবে। আমরা ড্রাফট এবং পেন্ডিং রিভিউ পোস্টগুলোর কন্টেন্ট খালি স্ট্রিং হওয়ার ব্যাপারে অ্যাসারশনও রাখতে পারব না, আর আমাদের সেগুলোর প্রয়োজনও নেই: আমরা আর এমন কোড কম্পাইল করতে পারব না যা সেই state-গুলোতে পোস্টের কন্টেন্ট ব্যবহার করার চেষ্টা করে। main
-এর আপডেট করা কোড লিস্টিং ১৮-২১-এ দেখানো হয়েছে।
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
post
-কে পুনরায় অ্যাসাইন করার জন্য main
-এ আমাদের যে পরিবর্তনগুলো করতে হয়েছে তার মানে হলো এই ইমপ্লিমেন্টেশনটি আর ঠিক অবজেক্ট-ওরিয়েন্টেড স্টেট প্যাটার্ন অনুসরণ করে না: state-গুলোর মধ্যে রূপান্তর আর সম্পূর্ণরূপে Post
ইমপ্লিমেন্টেশনের মধ্যে এনক্যাপসুলেট করা নেই। তবে, আমাদের লাভ হলো যে অবৈধ state-গুলো এখন টাইপ সিস্টেম এবং কম্পাইল টাইমে ঘটে যাওয়া টাইপ চেকিংয়ের কারণে অসম্ভব! এটি নিশ্চিত করে যে কিছু বাগ, যেমন একটি অপ্রকাশিত পোস্টের কন্টেন্ট প্রদর্শন, প্রোডাকশনে যাওয়ার আগেই আবিষ্কৃত হবে।
এই সেকশনের শুরুতে প্রস্তাবিত কাজগুলো লিস্টিং ১৮-২১-এর পরের blog
ক্রেটের উপর চেষ্টা করে দেখুন এই ভার্সনের কোডের ডিজাইন সম্পর্কে আপনার কী মনে হয়। লক্ষ্য করুন যে কিছু কাজ এই ডিজাইনে ইতিমধ্যে সম্পন্ন হয়ে থাকতে পারে।
আমরা দেখেছি যে যদিও Rust অবজেক্ট-ওরিয়েন্টেড ডিজাইন প্যাটার্ন ইমপ্লিমেন্ট করতে সক্ষম, তবে Rust-এ অন্যান্য প্যাটার্নও উপলব্ধ আছে, যেমন টাইপ সিস্টেমে state এনকোড করা। এই প্যাটার্নগুলোর ভিন্ন ভিন্ন সুবিধা-অসুবিধা রয়েছে। যদিও আপনি অবজেক্ট-ওরিয়েন্টেড প্যাটার্নগুলোর সাথে খুব পরিচিত হতে পারেন, Rust-এর বৈশিষ্ট্যগুলোর সুবিধা নেওয়ার জন্য সমস্যাটি নতুনভাবে চিন্তা করা সুবিধা প্রদান করতে পারে, যেমন কম্পাইল টাইমে কিছু বাগ প্রতিরোধ করা। অবজেক্ট-ওরিয়েন্টেড প্যাটার্নগুলো Rust-এ সর্বদা সেরা সমাধান হবে না কারণ কিছু বৈশিষ্ট্য, যেমন মালিকানা (ownership), যা অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজগুলোতে নেই।
সারাংশ (Summary)
এই অধ্যায়টি পড়ার পরে আপনি Rust-কে একটি অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজ মনে করেন কি না তা নির্বিশেষে, আপনি এখন জানেন যে আপনি Rust-এ কিছু অবজেক্ট-ওরিয়েন্টেড বৈশিষ্ট্য পেতে ট্রেইট অবজেক্ট ব্যবহার করতে পারেন। ডাইনামিক ডিসপ্যাচ আপনার কোডকে কিছুটা রানটাইম পারফরম্যান্সের বিনিময়ে কিছু নমনীয়তা দিতে পারে। আপনি এই নমনীয়তা ব্যবহার করে অবজেক্ট-ওরিয়েন্টেড প্যাটার্ন ইমপ্লিমেন্ট করতে পারেন যা আপনার কোডের রক্ষণাবেক্ষণে সহায়তা করতে পারে। Rust-এর অন্যান্য বৈশিষ্ট্যও রয়েছে, যেমন মালিকানা, যা অবজেক্ট-ওরিয়েন্টেড ল্যাঙ্গুয়েজগুলোতে নেই। একটি অবজেক্ট-ওরিয়েন্টেড প্যাটার্ন সর্বদা Rust-এর শক্তিশালী দিকগুলোর সুবিধা নেওয়ার সেরা উপায় হবে না, তবে এটি একটি উপলব্ধ বিকল্প।
এরপরে, আমরা প্যাটার্নগুলো দেখব, যা Rust-এর আরেকটি বৈশিষ্ট্য যা প্রচুর নমনীয়তা সক্ষম করে। আমরা বই জুড়ে সংক্ষেপে সেগুলোর দিকে নজর দিয়েছি কিন্তু এখনও তাদের সম্পূর্ণ ক্ষমতা দেখিনি। চলুন শুরু করা যাক!
প্যাটার্ন এবং ম্যাচিং (Patterns and Matching)
প্যাটার্ন (Patterns) হলো রাস্টের একটি বিশেষ সিনট্যাক্স যা দিয়ে বিভিন্ন ধরণের টাইপের (complex এবং simple উভয়ই) গঠন বা স্ট্রাকচারের সাথে মেলানো বা ম্যাচিং করা হয়। match
এক্সপ্রেশন এবং অন্যান্য কনস্ট্রাক্ট-এর সাথে প্যাটার্ন ব্যবহার করে প্রোগ্রামের কন্ট্রোল ফ্লো-এর (control flow) উপর আরও বেশি নিয়ন্ত্রণ পাওয়া যায়। একটি প্যাটার্ন নিম্নলিখিত জিনিসগুলির কোনো একটি সংমিশ্রণে গঠিত:
- লিটারেলস (Literals)
- ডিস্ট্রাকচার্ড অ্যারে, ইনাম, স্ট্রাক্ট বা টুপল (Destructured arrays, enums, structs, or tuples)
- ভেরিয়েবল (Variables)
- ওয়াইল্ডকার্ড (Wildcards)
- প্লেসহোল্ডার (Placeholders)
কিছু উদাহরণ প্যাটার্ন হলো x
, (a, 3)
, এবং Some(Color::Red)
। যে সব ক্ষেত্রে প্যাটার্ন ব্যবহার করা বৈধ, সেখানে এই উপাদানগুলো ডেটার আকৃতি বর্ণনা করে। আমাদের প্রোগ্রাম তখন এই প্যাটার্নগুলোর সাথে বিভিন্ন ভ্যালু বা মান মিলিয়ে দেখে যে, কোডের একটি নির্দিষ্ট অংশ চালানোর জন্য ডেটার সঠিক আকৃতি আছে কিনা।
একটি প্যাটার্ন ব্যবহার করার জন্য, আমরা সেটিকে কোনো একটি ভ্যালুর সাথে তুলনা করি। যদি প্যাটার্নটি ভ্যালুর সাথে মিলে যায়, তাহলে আমরা আমাদের কোডে সেই ভ্যালুর অংশগুলো ব্যবহার করি। ৬ষ্ঠ অধ্যায়ের match
এক্সপ্রেশনগুলো স্মরণ করুন যা প্যাটার্ন ব্যবহার করেছিল, যেমন মুদ্রা বাছাই করার মেশিনের উদাহরণ। যদি ভ্যালুটি প্যাটার্নের আকৃতির সাথে খাপ খায়, তাহলে আমরা নাম দেওয়া অংশগুলো ব্যবহার করতে পারি। যদি তা না হয়, প্যাটার্নের সাথে যুক্ত কোডটি চালানো হবে না।
এই অধ্যায়টি প্যাটার্ন সম্পর্কিত সমস্ত বিষয়ের একটি রেফারেন্স। আমরা প্যাটার্ন ব্যবহারের বৈধ স্থান, খণ্ডনযোগ্য (refutable) এবং অখণ্ডনযোগ্য (irrefutable) প্যাটার্নের মধ্যে পার্থক্য এবং আপনার দেখতে পাওয়া বিভিন্ন ধরণের প্যাটার্ন সিনট্যাক্স নিয়ে আলোচনা করব। এই অধ্যায়ের শেষে, আপনি জানতে পারবেন কীভাবে প্যাটার্ন ব্যবহার করে অনেক ধারণা পরিষ্কারভাবে প্রকাশ করা যায়।
যে সব জায়গায় প্যাটার্ন ব্যবহার করা যায় (All the Places Patterns Can Be Used)
রাস্টের বিভিন্ন জায়গায় প্যাটার্ন ব্যবহার করা হয়, এবং আপনি হয়তো না জেনেই ইতোমধ্যেই এগুলো অনেকবার ব্যবহার করেছেন! এই সেকশনে সেই সব জায়গা নিয়ে আলোচনা করা হবে যেখানে প্যাটার্ন ব্যবহার করা বৈধ।
match
Arms
৬ষ্ঠ অধ্যায়ে যেমন আলোচনা করা হয়েছে, আমরা match
এক্সপ্রেশনের প্রতিটি arm-এ প্যাটার্ন ব্যবহার করি। আনুষ্ঠানিকভাবে, match
এক্সপ্রেশনকে এভাবে সংজ্ঞায়িত করা হয়: match
কীওয়ার্ড, যার উপর ম্যাচ করা হবে সেই ভ্যালু, এবং এক বা একাধিক ম্যাচ arm যা একটি প্যাটার্ন এবং একটি এক্সপ্রেশন নিয়ে গঠিত। যদি ভ্যালুটি সেই arm-এর প্যাটার্নের সাথে মিলে যায়, তবে এক্সপ্রেশনটি রান হয়, যেমন:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
উদাহরণস্বরূপ, এখানে লিস্টিং ৬-৫ থেকে match
এক্সপ্রেশনটি দেওয়া হলো যা x
ভেরিয়েবলের একটি Option<i32>
ভ্যালুর উপর ম্যাচ করে:
match x {
None => None,
Some(i) => Some(i + 1),
}
এই match
এক্সপ্রেশনের প্যাটার্নগুলো হলো প্রতিটি অ্যারো (=>
) চিহ্নের বাম দিকের None
এবং Some(i)
।
match
এক্সপ্রেশনের জন্য একটি আবশ্যিক শর্ত হলো সেগুলোকে এক্সহস্টিভ (exhaustive) বা সর্বগ্রাসী হতে হবে, অর্থাৎ match
এক্সপ্রেশনের ভ্যালুর জন্য সমস্ত সম্ভাব্য মান বিবেচনা করতে হবে। আপনি যে সমস্ত সম্ভাবনা কভার করেছেন তা নিশ্চিত করার একটি উপায় হলো শেষ arm-এর জন্য একটি ক্যাচ-অল (catch-all) প্যাটার্ন রাখা: উদাহরণস্বরূপ, যেকোনো মানের সাথে মেলে এমন একটি ভেরিয়েবলের নাম কখনও ব্যর্থ হতে পারে না এবং এভাবে বাকি সমস্ত কেইস কভার করে।
নির্দিষ্ট প্যাটার্ন _
যেকোনো কিছুর সাথে মিলবে, কিন্তু এটি কখনও কোনো ভেরিয়েবলের সাথে বাইন্ড হয় না, তাই এটি প্রায়শই শেষ ম্যাচ arm-এ ব্যবহৃত হয়। _
প্যাটার্নটি তখন কার্যকর হতে পারে যখন আপনি নির্দিষ্ট করা হয়নি এমন কোনো মান উপেক্ষা করতে চান। আমরা এই অধ্যায়ের পরে "একটি প্যাটার্নে মান উপেক্ষা করা" অংশে _
প্যাটার্ন সম্পর্কে আরও বিস্তারিত আলোচনা করব।
let
Statements
এই অধ্যায়ের আগে, আমরা কেবল match
এবং if let
-এর সাথে প্যাটার্ন ব্যবহার নিয়ে স্পষ্টভাবে আলোচনা করেছি, কিন্তু আসলে, আমরা let
স্টেটমেন্ট সহ অন্যান্য জায়গাতেও প্যাটার্ন ব্যবহার করেছি। উদাহরণস্বরূপ, let
দিয়ে এই সহজ ভেরিয়েবল অ্যাসাইনমেন্ট বিবেচনা করুন:
#![allow(unused)] fn main() { let x = 5; }
যতবার আপনি এই ধরনের একটি let
স্টেটমেন্ট ব্যবহার করেছেন, ততবার আপনি প্যাটার্ন ব্যবহার করেছেন, যদিও আপনি হয়তো তা বুঝতে পারেননি! আরও আনুষ্ঠানিকভাবে, একটি let
স্টেটমেন্ট দেখতে এইরকম:
let PATTERN = EXPRESSION;
let x = 5;
এর মতো স্টেটমেন্টে PATTERN স্লটে একটি ভেরিয়েবলের নাম থাকে, এই ভেরিয়েবলের নামটি আসলে প্যাটার্নের একটি অতি সরল রূপ। রাস্ট এক্সপ্রেশনটিকে প্যাটার্নের সাথে তুলনা করে এবং এটি যে নামগুলো খুঁজে পায় তা অ্যাসাইন করে। সুতরাং, let x = 5;
উদাহরণে, x
একটি প্যাটার্ন যার অর্থ "এখানে যা মিলবে তা x
ভেরিয়েবলে বাইন্ড করো"। যেহেতু x
নামটি পুরো প্যাটার্ন, তাই এই প্যাটার্নটির কার্যকর অর্থ হলো "মান যাই হোক না কেন, সবকিছু x
ভেরিয়েবলে বাইন্ড করো"।
let
-এর প্যাটার্ন-ম্যাচিং দিকটি আরও স্পষ্টভাবে দেখতে, লিস্টিং ১৯-১ বিবেচনা করুন, যা একটি টুপলকে ডিস্ট্রাকচার (destructure) বা ভাঙতে let
-এর সাথে একটি প্যাটার্ন ব্যবহার করে।
fn main() { let (x, y, z) = (1, 2, 3); }
এখানে, আমরা একটি টুপলকে একটি প্যাটার্নের সাথে ম্যাচ করাই। রাস্ট (1, 2, 3)
ভ্যালুটিকে (x, y, z)
প্যাটার্নের সাথে তুলনা করে এবং দেখে যে ভ্যালুটি প্যাটার্নের সাথে মিলেছে, কারণ এটি দেখে যে দুটিতেই উপাদানের সংখ্যা সমান, তাই রাস্ট 1
-কে x
-এ, 2
-কে y
-তে এবং 3
-কে z
-এ বাইন্ড করে। আপনি এই টুপল প্যাটার্নটিকে তিনটি পৃথক ভেরিয়েবল প্যাটার্নের নেস্টেড রূপ হিসাবে ভাবতে পারেন।
যদি প্যাটার্নের উপাদানের সংখ্যা টুপলের উপাদানের সংখ্যার সাথে না মেলে, তাহলে সামগ্রিক টাইপ মিলবে না এবং আমরা একটি কম্পাইলার এরর পাব। উদাহরণস্বরূপ, লিস্টিং ১৯-২ দেখায় তিনটি উপাদান সহ একটি টুপলকে দুটি ভেরিয়েবলে ডিস্ট্রাকচার করার একটি প্রচেষ্টা, যা কাজ করবে না।
fn main() {
let (x, y) = (1, 2, 3);
}
এই কোডটি কম্পাইল করার চেষ্টা করলে এই টাইপ এররটি ঘটে:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
এররটি ঠিক করতে, আমরা টুপলের এক বা একাধিক মান উপেক্ষা করতে _
বা ..
ব্যবহার করতে পারতাম, যেমনটি আপনি "একটি প্যাটার্নে মান উপেক্ষা করা" সেকশনে দেখবেন। যদি সমস্যাটি প্যাটার্নে অনেক বেশি ভেরিয়েবল থাকা হয়, তবে সমাধান হলো ভেরিয়েবল সরিয়ে টাইপগুলো মেলানো যাতে ভেরিয়েবলের সংখ্যা টুপলের উপাদানের সংখ্যার সমান হয়।
Conditional if let
Expressions
৬ষ্ঠ অধ্যায়ে, আমরা if let
এক্সপ্রেশন নিয়ে আলোচনা করেছি, যা মূলত একটি match
-এর সংক্ষিপ্ত রূপ যা কেবল একটি কেইস ম্যাচ করে। ঐচ্ছিকভাবে, if let
-এর একটি সংশ্লিষ্ট else
থাকতে পারে, যেখানে if let
-এর প্যাটার্নটি না মিললে রান করার জন্য কোড থাকে।
লিস্টিং ১৯-৩ দেখায় যে if let
, else if
, এবং else if let
এক্সপ্রেশনগুলো মিশ্রিত করা এবং মেলানোও সম্ভব। এটি আমাদের match
এক্সপ্রেশনের চেয়ে বেশি নমনীয়তা দেয় যেখানে আমরা প্যাটার্নগুলোর সাথে তুলনা করার জন্য কেবল একটি মান প্রকাশ করতে পারি। এছাড়াও, রাস্টের জন্য এটি আবশ্যক নয় যে if let
, else if
, এবং else if let
arm-এর একটি সিরিজের শর্তাবলী একে অপরের সাথে সম্পর্কিত হতে হবে।
লিস্টিং ১৯-৩-এর কোডটি বেশ কয়েকটি শর্ত পরীক্ষা করে আপনার ব্যাকগ্রাউন্ডের জন্য কোন রঙ তৈরি করতে হবে তা নির্ধারণ করে। এই উদাহরণের জন্য, আমরা হার্ডকোডেড মান সহ ভেরিয়েবল তৈরি করেছি যা একটি বাস্তব প্রোগ্রাম ব্যবহারকারীর ইনপুট থেকে পেতে পারে।
fn main() { let favorite_color: Option<&str> = None; let is_tuesday = false; let age: Result<u8, _> = "34".parse(); if let Some(color) = favorite_color { println!("Using your favorite color, {color}, as the background"); } else if is_tuesday { println!("Tuesday is green day!"); } else if let Ok(age) = age { if age > 30 { println!("Using purple as the background color"); } else { println!("Using orange as the background color"); } } else { println!("Using blue as the background color"); } }
যদি ব্যবহারকারী একটি প্রিয় রঙ নির্দিষ্ট করে, তবে সেই রঙটি ব্যাকগ্রাউন্ড হিসাবে ব্যবহৃত হয়। যদি কোনো প্রিয় রঙ নির্দিষ্ট না করা হয় এবং আজ মঙ্গলবার হয়, তবে ব্যাকগ্রাউন্ডের রঙ সবুজ হবে। অন্যথায়, যদি ব্যবহারকারী তাদের বয়স একটি স্ট্রিং হিসাবে নির্দিষ্ট করে এবং আমরা সফলভাবে এটিকে একটি সংখ্যা হিসাবে পার্স করতে পারি, তবে সংখ্যার মানের উপর নির্ভর করে রঙটি বেগুনি বা কমলা হবে। যদি এই শর্তগুলোর কোনোটিই প্রযোজ্য না হয়, তবে ব্যাকগ্রাউন্ডের রঙ নীল হবে।
এই শর্তাধীন কাঠামোটি আমাদের জটিল প্রয়োজনীয়তা সমর্থন করতে দেয়। এখানে আমাদের হার্ডকোডেড মান দিয়ে, এই উদাহরণটি Using purple as the background color
প্রিন্ট করবে।
আপনি দেখতে পাচ্ছেন যে if let
নতুন ভেরিয়েবলও প্রবর্তন করতে পারে যা বিদ্যমান ভেরিয়েবলকে শ্যাডো (shadow) করে, ঠিক যেমন match
arm করতে পারে: if let Ok(age) = age
লাইনটি একটি নতুন age
ভেরিয়েবল প্রবর্তন করে যা Ok
ভ্যারিয়েন্টের ভিতরের মান ধারণ করে, বিদ্যমান age
ভেরিয়েবলটিকে শ্যাডো করে। এর মানে হলো আমাদের if age > 30
শর্তটি সেই ব্লকের মধ্যে রাখতে হবে: আমরা এই দুটি শর্তকে if let Ok(age) = age && age > 30
-তে একত্রিত করতে পারি না। নতুন age
যা আমরা 30-এর সাথে তুলনা করতে চাই তা নতুন স্কোপ কার্লি ব্র্যাকেট দিয়ে শুরু না হওয়া পর্যন্ত বৈধ নয়।
if let
এক্সপ্রেশন ব্যবহারের অসুবিধা হলো কম্পাইলার এক্সহস্টিভনেস বা সর্বগ্রাসীতার জন্য পরীক্ষা করে না, যেখানে match
এক্সপ্রেশনের ক্ষেত্রে তা করে। যদি আমরা শেষ else
ব্লকটি বাদ দিতাম এবং তাই কিছু কেইস হ্যান্ডেল করতে মিস করতাম, কম্পাইলার আমাদের সম্ভাব্য লজিক বাগ সম্পর্কে সতর্ক করত না।
while let
Conditional Loops
if let
-এর মতো গঠনে, while let
কন্ডিশনাল লুপ একটি while
লুপকে ততক্ষণ চলতে দেয় যতক্ষণ একটি প্যাটার্ন মিলতে থাকে। লিস্টিং ১৯-৪-এ আমরা একটি while let
লুপ দেখাই যা থ্রেডগুলোর মধ্যে পাঠানো মেসেজের জন্য অপেক্ষা করে, কিন্তু এক্ষেত্রে Option
-এর পরিবর্তে একটি Result
পরীক্ষা করে।
fn main() { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { for val in [1, 2, 3] { tx.send(val).unwrap(); } }); while let Ok(value) = rx.recv() { println!("{value}"); } }
এই উদাহরণটি 1
, 2
, এবং তারপর 3
প্রিন্ট করে। recv
মেথডটি চ্যানেলের রিসিভার সাইড থেকে প্রথম মেসেজটি নেয় এবং একটি Ok(value)
রিটার্ন করে। যখন আমরা প্রথমবার ১৬ অধ্যায়ে recv
দেখেছিলাম, আমরা সরাসরি এররটি আনর্যাপ করেছিলাম, অথবা for
লুপ ব্যবহার করে একটি ইটারেটর হিসাবে এর সাথে ইন্টারঅ্যাক্ট করেছিলাম। তবে, লিস্টিং ১৯-৪ যেমন দেখাচ্ছে, আমরা while let
ব্যবহার করতে পারি, কারণ recv
মেথডটি প্রতিবার একটি মেসেজ আসলে একটি Ok
রিটার্ন করে, যতক্ষণ প্রেরক বিদ্যমান থাকে, এবং প্রেরক পক্ষ সংযোগ বিচ্ছিন্ন হয়ে গেলে একটি Err
তৈরি করে।
for
Loops
একটি for
লুপে, for
কীওয়ার্ডের সরাসরি পরে যে ভ্যালুটি আসে তা একটি প্যাটার্ন। উদাহরণস্বরূপ, for x in y
-তে, x
হলো প্যাটার্ন। লিস্টিং ১৯-৫ দেখায় কিভাবে for
লুপের অংশ হিসেবে একটি টুপলকে ডিস্ট্রাকচার বা ভাঙার জন্য for
লুপে একটি প্যাটার্ন ব্যবহার করতে হয়।
fn main() { let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!("{value} is at index {index}"); } }
লিস্টিং ১৯-৫-এর কোডটি নিম্নলিখিত আউটপুট প্রিন্ট করবে:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2
আমরা enumerate
মেথড ব্যবহার করে একটি ইটারেটরকে অভিযোজিত করি যাতে এটি একটি মান এবং সেই মানের জন্য সূচক (index) তৈরি করে, যা একটি টুপলে রাখা হয়। প্রথম উৎপাদিত মানটি হলো টুপল (0, 'a')
। যখন এই মানটি (index, value)
প্যাটার্নের সাথে ম্যাচ করানো হয়, তখন index
হবে 0
এবং value
হবে 'a'
, যা আউটপুটের প্রথম লাইনটি প্রিন্ট করে।
Function Parameters
ফাংশনের প্যারামিটারগুলোও প্যাটার্ন হতে পারে। লিস্টিং ১৯-৬-এর কোড, যা foo
নামের একটি ফাংশন ঘোষণা করে যা i32
টাইপের x
নামের একটি প্যারামিটার নেয়, এতক্ষণে পরিচিত মনে হওয়া উচিত।
fn foo(x: i32) { // code goes here } fn main() {}
x
অংশটি একটি প্যাটার্ন! যেমন আমরা let
-এর সাথে করেছি, আমরা একটি ফাংশনের আর্গুমেন্টে একটি টুপলকে প্যাটার্নের সাথে ম্যাচ করতে পারি। লিস্টিং ১৯-৭ একটি টুপলের মানগুলোকে ফাংশনে পাস করার সময় বিভক্ত করে।
fn print_coordinates(&(x, y): &(i32, i32)) { println!("Current location: ({x}, {y})"); } fn main() { let point = (3, 5); print_coordinates(&point); }
এই কোডটি Current location: (3, 5)
প্রিন্ট করে। &(3, 5)
মানগুলো &(x, y)
প্যাটার্নের সাথে মেলে, তাই x
-এর মান 3
এবং y
-এর মান 5
হয়।
আমরা ফাংশন প্যারামিটার তালিকার মতো একইভাবে ক্লোজার প্যারামিটার তালিকাতেও প্যাটার্ন ব্যবহার করতে পারি কারণ ক্লোজারগুলো ফাংশনের মতোই, যেমনটি ১৩ অধ্যায়ে আলোচনা করা হয়েছে।
এই মুহুর্তে, আপনি প্যাটার্ন ব্যবহার করার বেশ কয়েকটি উপায় দেখেছেন, কিন্তু প্যাটার্নগুলো আমরা যে সব জায়গায় ব্যবহার করতে পারি সেখানে একইভাবে কাজ করে না। কিছু জায়গায়, প্যাটার্নগুলো অবশ্যই অখণ্ডনযোগ্য (irrefutable) হতে হবে; অন্য পরিস্থিতিতে, তারা খণ্ডনযোগ্য (refutable) হতে পারে। আমরা এই দুটি ধারণা নিয়ে পরবর্তীতে আলোচনা করব।
খণ্ডনযোগ্যতা: একটি প্যাটার্ন ম্যাচ করতে ব্যর্থ হতে পারে কিনা (Refutability: Whether a Pattern Might Fail to Match)
প্যাটার্ন দুটি রূপে আসে: খণ্ডনযোগ্য (refutable) এবং অখণ্ডনযোগ্য (irrefutable)। যে প্যাটার্নগুলো পাস করা যেকোনো সম্ভাব্য মানের জন্য মিলবে, সেগুলো হলো অখণ্ডনযোগ্য (irrefutable)। একটি উদাহরণ হলো let x = 5;
স্টেটমেন্টে x
, কারণ x
যেকোনো কিছুর সাথে মেলে এবং তাই ম্যাচ করতে ব্যর্থ হতে পারে না। যে প্যাটার্নগুলো কিছু সম্ভাব্য মানের জন্য ম্যাচ করতে ব্যর্থ হতে পারে, সেগুলো হলো খণ্ডনযোগ্য (refutable)। একটি উদাহরণ হলো if let Some(x) = a_value
এক্সপ্রেশনে Some(x)
, কারণ যদি a_value
ভেরিয়েবলের মান Some
-এর পরিবর্তে None
হয়, তবে Some(x)
প্যাটার্নটি মিলবে না।
ফাংশন প্যারামিটার, let
স্টেটমেন্ট এবং for
লুপ কেবল অখণ্ডনযোগ্য প্যাটার্ন গ্রহণ করতে পারে, কারণ মানগুলো না মিললে প্রোগ্রামটি অর্থপূর্ণ কিছু করতে পারে না। if let
এবং while let
এক্সপ্রেশন এবং let...else
স্টেটমেন্ট খণ্ডনযোগ্য এবং অখণ্ডনযোগ্য উভয় প্যাটার্ন গ্রহণ করে, কিন্তু কম্পাইলার অখণ্ডনযোগ্য প্যাটার্নের বিরুদ্ধে সতর্ক করে কারণ, সংজ্ঞা অনুসারে, এগুলো সম্ভাব্য ব্যর্থতা পরিচালনা করার জন্য উদ্দিষ্ট: একটি কন্ডিশনালের কার্যকারিতা তার সফলতা বা ব্যর্থতার উপর নির্ভর করে ভিন্নভাবে কাজ করার ক্ষমতার মধ্যে নিহিত।
সাধারণত, আপনাকে খণ্ডনযোগ্য এবং অখণ্ডনযোগ্য প্যাটার্নের মধ্যে পার্থক্য নিয়ে চিন্তা করতে হবে না; তবে, খণ্ডনযোগ্যতার ধারণাটির সাথে আপনার পরিচিত থাকা প্রয়োজন যাতে আপনি যখন কোনো এরর মেসেজে এটি দেখেন তখন প্রতিক্রিয়া জানাতে পারেন। সেইসব ক্ষেত্রে, কোডের উদ্দিষ্ট আচরণের উপর নির্ভর করে আপনাকে হয় প্যাটার্ন বা আপনি যে কনস্ট্রাক্টটি প্যাটার্নের সাথে ব্যবহার করছেন তা পরিবর্তন করতে হবে।
চলুন একটি উদাহরণ দেখি যখন আমরা একটি খণ্ডনযোগ্য প্যাটার্ন এমন জায়গায় ব্যবহার করার চেষ্টা করি যেখানে রাস্ট একটি অখণ্ডনযোগ্য প্যাটার্ন চায় এবং এর বিপরীতটি। লিস্টিং ১৯-৮ একটি let
স্টেটমেন্ট দেখায়, কিন্তু প্যাটার্নের জন্য, আমরা Some(x)
নির্দিষ্ট করেছি, যা একটি খণ্ডনযোগ্য প্যাটার্ন। যেমনটি আপনি আশা করতে পারেন, এই কোডটি কম্পাইল হবে না।
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}```
</Listing>
যদি `some_option_value` একটি `None` মান হতো, তবে এটি `Some(x)` প্যাটার্নের সাথে ম্যাচ করতে ব্যর্থ হতো, যার মানে প্যাটার্নটি খণ্ডনযোগ্য। তবে, `let` স্টেটমেন্ট কেবল একটি অখণ্ডনযোগ্য প্যাটার্ন গ্রহণ করতে পারে কারণ কোডের একটি `None` মান নিয়ে করার মতো বৈধ কিছু নেই। কম্পাইল টাইমে, রাস্ট অভিযোগ করবে যে আমরা একটি খণ্ডনযোগ্য প্যাটার্ন ব্যবহার করার চেষ্টা করেছি যেখানে একটি অখণ্ডনযোগ্য প্যাটার্ন প্রয়োজন:
```console
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
যেহেতু আমরা Some(x)
প্যাটার্ন দিয়ে প্রতিটি বৈধ মান কভার করিনি (এবং করতে পারতাম না!), তাই রাস্ট সঠিকভাবে একটি কম্পাইলার এরর তৈরি করে।
যদি আমাদের এমন একটি খণ্ডনযোগ্য প্যাটার্ন থাকে যেখানে একটি অখণ্ডনযোগ্য প্যাটার্ন প্রয়োজন, আমরা প্যাটার্ন ব্যবহারকারী কোড পরিবর্তন করে এটি ঠিক করতে পারি: let
ব্যবহার করার পরিবর্তে, আমরা let else
ব্যবহার করতে পারি। তারপর, যদি প্যাটার্নটি না মেলে, কোডটি কেবল কোঁকড়া বন্ধনীর (curly brackets) ভেতরের কোডটি এড়িয়ে যাবে, যা এটিকে বৈধভাবে চালিয়ে যাওয়ার একটি উপায় দেয়। লিস্টিং ১৯-৯ দেখায় কীভাবে লিস্টিং ১৯-৮-এর কোডটি ঠিক করতে হয়।
fn main() { let some_option_value: Option<i32> = None; let Some(x) = some_option_value else { return; }; }
আমরা কোডটিকে একটি মুক্তির পথ দিয়েছি! এই কোডটি সম্পূর্ণ বৈধ, যদিও এর মানে হলো আমরা একটি সতর্কতা না পেয়ে একটি অখণ্ডনযোগ্য প্যাটার্ন ব্যবহার করতে পারি না। যদি আমরা let...else
-কে এমন একটি প্যাটার্ন দিই যা সর্বদা মিলবে, যেমন x
, যেমনটি লিস্টিং ১৯-১০-এ দেখানো হয়েছে, কম্পাইলার একটি সতর্কতা দেবে।
fn main() { let x = 5 else { return; }; }``` </Listing> রাস্ট অভিযোগ করে যে একটি অখণ্ডনযোগ্য প্যাটার্নের সাথে `let...else` ব্যবহার করা অর্থহীন: ```console $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) warning: irrefutable `let...else` pattern --> src/main.rs:2:5 | 2 | let x = 5 else { | ^^^^^^^^^ | = note: this pattern will always match, so the `else` clause is useless = help: consider removing the `else` clause = note: `#[warn(irrefutable_let_patterns)]` on by default warning: `patterns` (bin "patterns") generated 1 warning Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s Running `target/debug/patterns`
এই কারণে, ম্যাচ arm-গুলোকে অবশ্যই খণ্ডনযোগ্য প্যাটার্ন ব্যবহার করতে হবে, শেষ arm ব্যতীত, যা যেকোনো অবশিষ্ট মানকে একটি অখণ্ডনযোগ্য প্যাটার্ন দিয়ে ম্যাচ করা উচিত। রাস্ট আমাদের কেবল একটি arm সহ একটি match
-এ একটি অখণ্ডনযোগ্য প্যাটার্ন ব্যবহার করার অনুমতি দেয়, কিন্তু এই সিনট্যাক্সটি বিশেষভাবে কার্যকর নয় এবং একটি সহজ let
স্টেটমেন্ট দ্বারা প্রতিস্থাপিত হতে পারে।
এখন আপনি জানেন কোথায় প্যাটার্ন ব্যবহার করতে হয় এবং খণ্ডনযোগ্য ও অখণ্ডনযোগ্য প্যাটার্নের মধ্যে পার্থক্য কী, চলুন আমরা প্যাটার্ন তৈরি করার জন্য যে সমস্ত সিনট্যাক্স ব্যবহার করতে পারি তা কভার করি।
প্যাটার্ন সিনট্যাক্স (Pattern Syntax)
এই সেকশনে, আমরা প্যাটার্নে বৈধ সমস্ত সিনট্যাক্স একত্রিত করব এবং আলোচনা করব কেন এবং কখন আপনি প্রতিটি ব্যবহার করতে চাইতে পারেন।
লিটারেলের সাথে ম্যাচিং (Matching Literals)
যেমনটি আপনি ৬ষ্ঠ অধ্যায়ে দেখেছেন, আপনি সরাসরি লিটারেলের (literals) সাথে প্যাটার্ন ম্যাচ করতে পারেন। নিচের কোডটি কিছু উদাহরণ দেয়:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
এই কোডটি one
প্রিন্ট করবে কারণ x
এর মান 1
। এই সিনট্যাক্সটি তখন কার্যকর যখন আপনি চান আপনার কোড একটি নির্দিষ্ট সুনির্দিষ্ট (concrete) মান পেলে কোনো একটি কাজ করুক।
নামযুক্ত ভেরিয়েবলের সাথে ম্যাচিং (Matching Named Variables)
নামযুক্ত ভেরিয়েবলগুলো হলো অখণ্ডনযোগ্য (irrefutable) প্যাটার্ন যা যেকোনো মানের সাথে মেলে, এবং আমরা এই বইয়ে অনেকবার এগুলো ব্যবহার করেছি। তবে, আপনি যখন match
, if let
, বা while let
এক্সপ্রেশনে নামযুক্ত ভেরিয়েবল ব্যবহার করেন তখন একটি জটিলতা দেখা দেয়। যেহেতু এই ধরনের প্রতিটি এক্সপ্রেশন একটি নতুন স্কোপ (scope) শুরু করে, তাই এই এক্সপ্রেশনগুলোর ভিতরে একটি প্যাটার্নের অংশ হিসাবে ঘোষিত ভেরিয়েবলগুলো কনস্ট্রাক্টের বাইরের একই নামের ভেরিয়েবলগুলোকে শ্যাডো (shadow) করবে, যেমনটি সমস্ত ভেরিয়েবলের ক্ষেত্রে হয়। লিস্টিং ১৯-১১-এ, আমরা x
নামে একটি ভেরিয়েবল ঘোষণা করি যার মান Some(5)
এবং y
নামে একটি ভেরিয়েবল যার মান 10
। তারপর আমরা x
মানের উপর একটি match
এক্সপ্রেশন তৈরি করি। ম্যাচ arm-গুলোর প্যাটার্ন এবং শেষের println!
দেখুন, এবং এই কোডটি চালানোর আগে বা আরও পড়ার আগে অনুমান করার চেষ্টা করুন কোডটি কী প্রিন্ট করবে।
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {y}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
চলুন দেখি match
এক্সপ্রেশনটি চলার সময় কী ঘটে। প্রথম ম্যাচ arm-এর প্যাটার্নটি x
-এর সংজ্ঞায়িত মানের সাথে মেলে না, তাই কোডটি চলতে থাকে।
দ্বিতীয় ম্যাচ arm-এর প্যাটার্নটি y
নামে একটি নতুন ভেরিয়েবল প্রবর্তন করে যা একটি Some
মানের ভিতরের যেকোনো মানের সাথে মিলবে। যেহেতু আমরা match
এক্সপ্রেশনের ভিতরে একটি নতুন স্কোপে আছি, এটি একটি নতুন y
ভেরিয়েবল, শুরুতে 10
মান দিয়ে ঘোষণা করা y
নয়। এই নতুন y
বাইন্ডিং একটি Some
-এর ভিতরের যেকোনো মানের সাথে মিলবে, যা আমাদের x
-এ আছে। সুতরাং, এই নতুন y
, x
-এর Some
-এর ভিতরের মানের সাথে বাইন্ড হয়। সেই মানটি হলো 5
, তাই সেই arm-এর এক্সপ্রেশনটি কার্যকর হয় এবং Matched, y = 5
প্রিন্ট করে।
যদি x
Some(5)
-এর পরিবর্তে একটি None
মান হতো, তবে প্রথম দুটি arm-এর প্যাটার্ন মিলত না, তাই মানটি আন্ডারস্কোরের সাথে মিলত। আমরা আন্ডারস্কোর arm-এর প্যাটার্নে x
ভেরিয়েবল প্রবর্তন করিনি, তাই এক্সপ্রেশনের x
এখনও বাইরের x
যা শ্যাডো হয়নি। এই কাল্পনিক ক্ষেত্রে, match
প্রিন্ট করত Default case, x = None
।
match
এক্সপ্রেশন শেষ হয়ে গেলে, এর স্কোপও শেষ হয়ে যায়, এবং ভেতরের y
-এর স্কোপও শেষ হয়ে যায়। শেষ println!
at the end: x = Some(5), y = 10
তৈরি করে।
একটি match
এক্সপ্রেশন তৈরি করতে যা বাইরের x
এবং y
-এর মান তুলনা করে, বিদ্যমান y
ভেরিয়েবলকে শ্যাডো করে এমন একটি নতুন ভেরিয়েবল প্রবর্তন করার পরিবর্তে, আমাদের পরিবর্তে একটি ম্যাচ গার্ড কন্ডিশনাল (match guard conditional) ব্যবহার করতে হবে। আমরা ম্যাচ গার্ড সম্পর্কে পরে "ম্যাচ গার্ডের সাথে অতিরিক্ত কন্ডিশনাল" অংশে আলোচনা করব।
একাধিক প্যাটার্ন (Multiple Patterns)
match
এক্সপ্রেশনে, আপনি |
সিনট্যাক্স ব্যবহার করে একাধিক প্যাটার্ন ম্যাচ করতে পারেন, যা প্যাটার্নের or অপারেটর। উদাহরণস্বরূপ, নিম্নলিখিত কোডে আমরা x
-এর মানকে ম্যাচ arm-গুলোর সাথে মেলাই, যার প্রথমটিতে একটি or বিকল্প রয়েছে, যার মানে যদি x
-এর মান সেই arm-এর যেকোনো একটি মানের সাথে মেলে, তবে সেই arm-এর কোডটি চলবে:
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
এই কোডটি one or two
প্রিন্ট করে।
..=
দিয়ে মানের রেঞ্জ ম্যাচিং (Matching Ranges of Values with ..=
)
..=
সিনট্যাক্স আমাদের একটি অন্তর্ভুক্তিমূলক (inclusive) মানের রেঞ্জের সাথে ম্যাচ করতে দেয়। নিম্নলিখিত কোডে, যখন একটি প্যাটার্ন প্রদত্ত রেঞ্জের মধ্যে যেকোনো মানের সাথে মেলে, তখন সেই arm-টি কার্যকর হবে:
fn main() { let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } }
যদি x
1
, 2
, 3
, 4
, বা 5
হয়, তবে প্রথম arm-টি মিলবে। এই সিনট্যাক্সটি একাধিক ম্যাচ মানের জন্য |
অপারেটর ব্যবহার করে একই ধারণা প্রকাশ করার চেয়ে বেশি সুবিধাজনক; যদি আমরা |
ব্যবহার করতাম, তবে আমাদের 1 | 2 | 3 | 4 | 5
নির্দিষ্ট করতে হতো। একটি রেঞ্জ নির্দিষ্ট করা অনেক ছোট, বিশেষ করে যদি আমরা, ধরা যাক, 1 থেকে 1,000 এর মধ্যে যেকোনো সংখ্যা মেলাতে চাই!
কম্পাইলার কম্পাইল টাইমে পরীক্ষা করে যে রেঞ্জটি খালি নয়, এবং যেহেতু রাস্ট কেবল char
এবং সাংখ্যিক (numeric) মানের জন্য বলতে পারে একটি রেঞ্জ খালি কিনা, তাই রেঞ্জ কেবল সাংখ্যিক বা char
মানের সাথে অনুমোদিত।
এখানে char
মানের রেঞ্জ ব্যবহার করে একটি উদাহরণ দেওয়া হলো:
fn main() { let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } }
রাস্ট বলতে পারে যে 'c'
প্রথম প্যাটার্নের রেঞ্জের মধ্যে রয়েছে এবং early ASCII letter
প্রিন্ট করে।
মান ভাঙার জন্য ডিস্ট্রাকচারিং (Destructuring to Break Apart Values)
আমরা struct, enum এবং tuple-কে ডিস্ট্রাকচার (destructure) বা ভাঙতে প্যাটার্ন ব্যবহার করতে পারি যাতে এই মানগুলোর বিভিন্ন অংশ ব্যবহার করা যায়। চলুন প্রতিটি মান নিয়ে আলোচনা করি।
Struct ডিস্ট্রাকচারিং (Destructuring Structs)
লিস্টিং ১৯-১২ একটি Point
struct দেখায় যার দুটি ফিল্ড, x
এবং y
, আছে যা আমরা একটি let
স্টেটমেন্টের সাথে একটি প্যাটার্ন ব্যবহার করে ভাঙতে পারি।
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b); }
এই কোডটি a
এবং b
ভেরিয়েবল তৈরি করে যা p
struct-এর x
এবং y
ফিল্ডের মানের সাথে মেলে। এই উদাহরণটি দেখায় যে প্যাটার্নের ভেরিয়েবলের নামগুলো struct-এর ফিল্ডের নামের সাথে মিলতে হবে এমন কোনো কথা নেই। তবে, কোন ভেরিয়েবল কোন ফিল্ড থেকে এসেছে তা মনে রাখা সহজ করার জন্য ভেরিয়েবলের নামগুলো ফিল্ডের নামের সাথে মেলানো একটি সাধারণ অভ্যাস। এই সাধারণ ব্যবহারের কারণে, এবং কারণ let Point { x: x, y: y } = p;
লেখাতে অনেক পুনরাবৃত্তি রয়েছে, তাই রাস্টের struct ফিল্ড ম্যাচ করা প্যাটার্নগুলোর জন্য একটি শর্টহ্যান্ড আছে: আপনার কেবল struct ফিল্ডের নাম তালিকাভুক্ত করতে হবে, এবং প্যাটার্ন থেকে তৈরি ভেরিয়েবলগুলোর একই নাম থাকবে। লিস্টিং ১৯-১৩ লিস্টিং ১৯-১২-এর কোডের মতোই আচরণ করে, কিন্তু let
প্যাটার্নে তৈরি ভেরিয়েবলগুলো a
এবং b
-এর পরিবর্তে x
এবং y
।
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y); }
এই কোডটি x
এবং y
ভেরিয়েবল তৈরি করে যা p
ভেরিয়েবলের x
এবং y
ফিল্ডের সাথে মেলে। ফলাফল হলো x
এবং y
ভেরিয়েবলগুলো p
struct থেকে মান ধারণ করে।
আমরা struct প্যাটার্নের অংশ হিসাবে সমস্ত ফিল্ডের জন্য ভেরিয়েবল তৈরি করার পরিবর্তে লিটারেল মান দিয়েও ডিস্ট্রাকচার করতে পারি। এটি আমাদের কিছু ফিল্ডকে নির্দিষ্ট মানের জন্য পরীক্ষা করার সুযোগ দেয় এবং অন্য ফিল্ডগুলো ডিস্ট্রাকচার করার জন্য ভেরিয়েবল তৈরি করতে দেয়।
লিস্টিং ১৯-১৪-এ, আমাদের একটি match
এক্সপ্রেশন আছে যা Point
মানগুলোকে তিনটি ক্ষেত্রে বিভক্ত করে: যে পয়েন্টগুলো সরাসরি x
অক্ষের উপর অবস্থিত (যা y = 0
হলে সত্য), y
অক্ষের উপর (x = 0
), বা কোনো অক্ষের উপরেই নয়।
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => { println!("On neither axis: ({x}, {y})"); } } }
প্রথম arm টি x
অক্ষের উপর অবস্থিত যেকোনো পয়েন্টের সাথে মিলবে, এটি নির্দিষ্ট করে যে y
ফিল্ডটি মিলবে যদি এর মান লিটারেল 0
এর সাথে মেলে। প্যাটার্নটি এখনও একটি x
ভেরিয়েবল তৈরি করে যা আমরা এই arm-এর কোডে ব্যবহার করতে পারি।
একইভাবে, দ্বিতীয় arm টি y
অক্ষের উপর যেকোনো পয়েন্টের সাথে মেলে, এটি নির্দিষ্ট করে যে x
ফিল্ডটি মিলবে যদি এর মান 0
হয় এবং y
ফিল্ডের মানের জন্য একটি y
ভেরিয়েবল তৈরি করে। তৃতীয় arm টি কোনো লিটারেল নির্দিষ্ট করে না, তাই এটি অন্য যেকোনো Point
-এর সাথে মেলে এবং x
ও y
উভয় ফিল্ডের জন্য ভেরিয়েবল তৈরি করে।
এই উদাহরণে, p
মানটি দ্বিতীয় arm-এর সাথে মেলে কারণ x
-এ 0
রয়েছে, তাই এই কোডটি On the y axis at 7
প্রিন্ট করবে।
মনে রাখবেন যে একটি match
এক্সপ্রেশন প্রথম ম্যাচিং প্যাটার্ন খুঁজে পাওয়ার সাথে সাথে arm পরীক্ষা করা বন্ধ করে দেয়, তাই যদিও Point { x: 0, y: 0 }
x
অক্ষ এবং y
অক্ষ উভয়তেই রয়েছে, এই কোডটি কেবল On the x axis at 0
প্রিন্ট করত।
Enum ডিস্ট্রাকচারিং (Destructuring Enums)
আমরা এই বইয়ে enum ডিস্ট্রাকচার করেছি (উদাহরণস্বরূপ, ৬ষ্ঠ অধ্যায়ের লিস্টিং ৬-৫), কিন্তু এখনো স্পষ্টভাবে আলোচনা করিনি যে একটি enum ডিস্ট্রাকচার করার প্যাটার্নটি enum-এর মধ্যে সংরক্ষিত ডেটা যেভাবে সংজ্ঞায়িত করা হয়েছে তার সাথে সামঞ্জস্যপূর্ণ। উদাহরণ হিসাবে, লিস্টিং ১৯-১৫-এ আমরা লিস্টিং ৬-২ থেকে Message
enum ব্যবহার করি এবং একটি match
লিখি এমন প্যাটার্ন দিয়ে যা প্রতিটি ভেতরের মান ডিস্ট্রাকচার করবে।
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!("The Quit variant has no data to destructure."); } Message::Move { x, y } => { println!("Move in the x direction {x} and in the y direction {y}"); } Message::Write(text) => { println!("Text message: {text}"); } Message::ChangeColor(r, g, b) => { println!("Change color to red {r}, green {g}, and blue {b}"); } } }
এই কোডটি Change color to red 0, green 160, and blue 255
প্রিন্ট করবে। msg
-এর মান পরিবর্তন করে অন্য arm-গুলোর কোড চলতে দেখুন।
Message::Quit
-এর মতো কোনো ডেটা ছাড়া enum ভ্যারিয়েন্টগুলোর জন্য, আমরা মানটি আর ডিস্ট্রাকচার করতে পারি না। আমরা কেবল লিটারেল Message::Quit
মানের সাথে ম্যাচ করতে পারি, এবং সেই প্যাটার্নে কোনো ভেরিয়েবল নেই।
Message::Move
-এর মতো struct-এর মতো enum ভ্যারিয়েন্টগুলোর জন্য, আমরা struct ম্যাচ করার জন্য যে প্যাটার্ন নির্দিষ্ট করি তার মতো একটি প্যাটার্ন ব্যবহার করতে পারি। ভ্যারিয়েন্টের নামের পরে, আমরা কোঁকড়া বন্ধনী রাখি এবং তারপর ভেরিয়েবল সহ ফিল্ডগুলো তালিকাভুক্ত করি যাতে আমরা এই arm-এর কোডে ব্যবহারের জন্য টুকরোগুলো ভাঙতে পারি। এখানে আমরা লিস্টিং ১৯-১৩-এর মতো শর্টহ্যান্ড ফর্ম ব্যবহার করি।
Message::Write
-এর মতো tuple-এর মতো enum ভ্যারিয়েন্টগুলোর জন্য, যা একটি উপাদান সহ একটি tuple ধারণ করে এবং Message::ChangeColor
যা তিনটি উপাদান সহ একটি tuple ধারণ করে, প্যাটার্নটি tuple ম্যাচ করার জন্য আমরা যে প্যাটার্ন নির্দিষ্ট করি তার অনুরূপ। প্যাটার্নের ভেরিয়েবলের সংখ্যা আমরা যে ভ্যারিয়েন্টটি ম্যাচ করছি তার উপাদানের সংখ্যার সাথে অবশ্যই মিলতে হবে।
নেস্টেড Struct এবং Enum ডিস্ট্রাকচারিং (Destructuring Nested Structs and Enums)
এখন পর্যন্ত, আমাদের উদাহরণগুলো সবই এক স্তরে struct বা enum ম্যাচিং করেছে, কিন্তু ম্যাচিং নেস্টেড আইটেমগুলোতেও কাজ করতে পারে! উদাহরণস্বরূপ, আমরা ChangeColor
মেসেজে RGB এবং HSV রঙ সমর্থন করার জন্য লিস্টিং ১৯-১৫-এর কোডটি রিফ্যাক্টর করতে পারি, যেমনটি লিস্টিং ১৯-১৬-তে দেখানো হয়েছে।
enum Color { Rgb(i32, i32, i32), Hsv(i32, i32, i32), } enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(Color), } fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!("Change color to red {r}, green {g}, and blue {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("Change color to hue {h}, saturation {s}, value {v}"); } _ => (), } }
match
এক্সপ্রেশনের প্রথম arm-এর প্যাটার্নটি একটি Message::ChangeColor
enum ভ্যারিয়েন্টের সাথে মেলে যা একটি Color::Rgb
ভ্যারিয়েন্ট ধারণ করে; তারপর প্যাটার্নটি তিনটি ভেতরের i32
মানের সাথে বাইন্ড হয়। দ্বিতীয় arm-এর প্যাটার্নটিও একটি Message::ChangeColor
enum ভ্যারিয়েন্টের সাথে মেলে, কিন্তু ভেতরের enum Color::Hsv
-এর সাথে মেলে। আমরা এই জটিল শর্তগুলো একটি match
এক্সপ্রেশনে নির্দিষ্ট করতে পারি, যদিও দুটি enum জড়িত।
Struct এবং Tuple ডিস্ট্রাকচারিং (Destructuring Structs and Tuples)
আমরা আরও জটিল উপায়ে ডিস্ট্রাকচারিং প্যাটার্নগুলো মিশ্রিত, ম্যাচ এবং নেস্ট করতে পারি। নিম্নলিখিত উদাহরণটি একটি জটিল ডিস্ট্রাকচার দেখায় যেখানে আমরা একটি tuple-এর ভিতরে struct এবং tuple নেস্ট করি এবং সমস্ত প্রিমিটিভ (primitive) মান বের করে আনি:
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
এই কোডটি আমাদের জটিল টাইপগুলোকে তাদের উপাদান অংশে ভাঙতে দেয় যাতে আমরা আগ্রহী মানগুলো আলাদাভাবে ব্যবহার করতে পারি।
প্যাটার্ন দিয়ে ডিস্ট্রাকচার করা মানগুলোর টুকরো ব্যবহার করার একটি সুবিধাজনক উপায়, যেমন একটি struct-এর প্রতিটি ফিল্ড থেকে মান, একে অপরের থেকে আলাদাভাবে।
একটি প্যাটার্নে মান উপেক্ষা করা (Ignoring Values in a Pattern)
আপনি দেখেছেন যে কখনও কখনও একটি প্যাটার্নে মান উপেক্ষা করা কার্যকর, যেমন একটি match
-এর শেষ arm-এ, একটি ক্যাচ-অল পাওয়ার জন্য যা আসলে কিছুই করে না কিন্তু বাকি সমস্ত সম্ভাব্য মান বিবেচনা করে। একটি প্যাটার্নে সম্পূর্ণ মান বা মানের অংশ উপেক্ষা করার কয়েকটি উপায় রয়েছে: _
প্যাটার্ন ব্যবহার করে (যা আপনি দেখেছেন), অন্য প্যাটার্নের মধ্যে _
প্যাটার্ন ব্যবহার করে, একটি আন্ডারস্কোর দিয়ে শুরু হওয়া নাম ব্যবহার করে, অথবা ..
ব্যবহার করে একটি মানের বাকি অংশ উপেক্ষা করার জন্য। চলুন দেখি কিভাবে এবং কেন এই প্রতিটি প্যাটার্ন ব্যবহার করতে হয়।
_
দিয়ে একটি সম্পূর্ণ মান (An Entire Value with _
)
আমরা আন্ডারস্কোরকে একটি ওয়াইল্ডকার্ড প্যাটার্ন হিসাবে ব্যবহার করেছি যা যেকোনো মানের সাথে মিলবে কিন্তু মানের সাথে বাইন্ড হবে না। এটি একটি match
এক্সপ্রেশনের শেষ arm হিসাবে বিশেষভাবে কার্যকর, তবে আমরা এটি ফাংশন প্যারামিটার সহ যেকোনো প্যাটার্নেও ব্যবহার করতে পারি, যেমনটি লিস্টিং ১৯-১৭-তে দেখানো হয়েছে।
fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {y}"); } fn main() { foo(3, 4); }
এই কোডটি প্রথম আর্গুমেন্ট হিসাবে পাস করা 3
মানটিকে সম্পূর্ণরূপে উপেক্ষা করবে, এবং This code only uses the y parameter: 4
প্রিন্ট করবে।
বেশিরভাগ ক্ষেত্রে যখন আপনার আর কোনো নির্দিষ্ট ফাংশন প্যারামিটারের প্রয়োজন হয় না, তখন আপনি সিগনেচারটি পরিবর্তন করে অব্যবহৃত প্যারামিটারটি সরিয়ে ফেলবেন। একটি ফাংশন প্যারামিটার উপেক্ষা করা বিশেষভাবে কার্যকর হতে পারে এমন ক্ষেত্রে যখন, উদাহরণস্বরূপ, আপনি একটি trait ইমপ্লিমেন্ট করছেন যেখানে আপনার একটি নির্দিষ্ট টাইপ সিগনেচার প্রয়োজন কিন্তু আপনার ইমপ্লিমেন্টেশনের ফাংশন বডিতে প্যারামিটারগুলোর একটির প্রয়োজন নেই। তখন আপনি অব্যবহৃত ফাংশন প্যারামিটার সম্পর্কে কম্পাইলারের সতর্কতা এড়াতে পারেন, যেমনটি আপনি একটি নাম ব্যবহার করলে পেতেন।
নেস্টেড _
দিয়ে একটি মানের অংশ (Parts of a Value with a Nested _
)
আমরা একটি মানের কেবল একটি অংশ উপেক্ষা করার জন্য অন্য প্যাটার্নের ভিতরে _
ব্যবহার করতে পারি, উদাহরণস্বরূপ, যখন আমরা একটি মানের কেবল একটি অংশ পরীক্ষা করতে চাই কিন্তু সংশ্লিষ্ট কোডে অন্য অংশগুলোর কোনো ব্যবহার নেই। লিস্টিং ১৯-১৮ একটি সেটিং-এর মান পরিচালনার জন্য দায়ী কোড দেখায়। ব্যবসায়িক প্রয়োজনীয়তা হলো ব্যবহারকারীকে একটি বিদ্যমান কাস্টমাইজেশন ওভাররাইট করার অনুমতি দেওয়া উচিত নয় কিন্তু সেটিংটি আনসেট করতে এবং বর্তমানে আনসেট থাকলে একটি মান দিতে পারে।
fn main() { let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {setting_value:?}"); }
এই কোডটি Can't overwrite an existing customized value
এবং তারপর setting is Some(5)
প্রিন্ট করবে। প্রথম ম্যাচ arm-এ, আমাদের Some
ভ্যারিয়েন্টের কোনোটির ভিতরের মানের সাথে ম্যাচ বা ব্যবহার করার প্রয়োজন নেই, কিন্তু আমাদের সেই কেসটি পরীক্ষা করতে হবে যখন setting_value
এবং new_setting_value
উভয়ই Some
ভ্যারিয়েন্ট। সেই ক্ষেত্রে, আমরা setting_value
পরিবর্তন না করার কারণ প্রিন্ট করি, এবং এটি পরিবর্তন হয় না।
অন্য সব ক্ষেত্রে (যদি setting_value
বা new_setting_value
যেকোনো একটি None
হয়) যা দ্বিতীয় arm-এর _
প্যাটার্ন দ্বারা প্রকাশ করা হয়, আমরা new_setting_value
-কে setting_value
হতে দিতে চাই।
আমরা নির্দিষ্ট মান উপেক্ষা করার জন্য একটি প্যাটার্নের মধ্যে একাধিক জায়গায় আন্ডারস্কোর ব্যবহার করতে পারি। লিস্টিং ১৯-১৯ পাঁচটি আইটেমের একটি tuple-এর দ্বিতীয় এবং চতুর্থ মান উপেক্ষা করার একটি উদাহরণ দেখায়।
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}"); } } }
এই কোডটি Some numbers: 2, 8, 32
প্রিন্ট করবে, এবং 4
ও 16
মানগুলো উপেক্ষা করা হবে।
_
দিয়ে নাম শুরু করে একটি অব্যবহৃত ভেরিয়েবল (An Unused Variable by Starting Its Name with _
)
আপনি যদি একটি ভেরিয়েবল তৈরি করেন কিন্তু কোথাও ব্যবহার না করেন, রাস্ট সাধারণত একটি সতর্কতা জারি করবে কারণ একটি অব্যবহৃত ভেরিয়েবল একটি বাগ হতে পারে। তবে, কখনও কখনও এমন একটি ভেরিয়েবল তৈরি করা কার্যকর হয় যা আপনি এখনো ব্যবহার করবেন না, যেমন যখন আপনি প্রোটোটাইপিং করছেন বা সবেমাত্র একটি প্রকল্প শুরু করছেন। এই পরিস্থিতিতে, আপনি ভেরিয়েবলের নামটি একটি আন্ডারস্কোর দিয়ে শুরু করে রাস্টকে বলতে পারেন যাতে সে আপনাকে অব্যবহৃত ভেরিয়েবল সম্পর্কে সতর্ক না করে। লিস্টিং ১৯-২০-এ, আমরা দুটি অব্যবহৃত ভেরিয়েবল তৈরি করি, কিন্তু যখন আমরা এই কোডটি কম্পাইল করি, তখন আমাদের কেবল একটি সম্পর্কে সতর্কতা পাওয়া উচিত।
fn main() { let _x = 5; let y = 10; }
এখানে, আমরা y
ভেরিয়েবল ব্যবহার না করার জন্য একটি সতর্কতা পাই, কিন্তু _x
ব্যবহার না করার জন্য কোনো সতর্কতা পাই না।
লক্ষ্য করুন যে কেবল _
ব্যবহার করা এবং একটি আন্ডারস্কোর দিয়ে শুরু হওয়া নাম ব্যবহার করার মধ্যে একটি সূক্ষ্ম পার্থক্য রয়েছে। _x
সিনট্যাক্সটি এখনও মানটিকে ভেরিয়েবলের সাথে বাইন্ড করে, যেখানে _
মোটেই বাইন্ড করে না। এই পার্থক্যটি গুরুত্বপূর্ণ এমন একটি কেস দেখানোর জন্য, লিস্টিং ১৯-২১ আমাদের একটি এরর দেবে।
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
আমরা একটি এরর পাব কারণ s
মানটি এখনও _s
-এ মুভ করা হবে, যা আমাদের s
পুনরায় ব্যবহার করতে বাধা দেয়। তবে, আন্ডারস্কোর নিজে থেকে ব্যবহার করলে তা কখনও মানের সাথে বাইন্ড করে না। লিস্টিং ১৯-২২ কোনো এরর ছাড়াই কম্পাইল হবে কারণ s
_
-এ মুভ হয় না।
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }``` </Listing> এই কোডটি ঠিকঠাক কাজ করে কারণ আমরা কখনও `s`-কে কিছুর সাথে বাইন্ড করি না; এটি মুভ হয় না। <a id="ignoring-remaining-parts-of-a-value-with-"></a> ### `..` দিয়ে একটি মানের বাকি অংশ (_Remaining Parts of a Value with `..`_) অনেক অংশ সহ মানগুলোর জন্য, আমরা নির্দিষ্ট অংশ ব্যবহার করতে এবং বাকিগুলো উপেক্ষা করতে `..` সিনট্যাক্স ব্যবহার করতে পারি, প্রতিটি উপেক্ষা করা মানের জন্য আন্ডারস্কোর তালিকাভুক্ত করার প্রয়োজন এড়িয়ে। `..` প্যাটার্নটি একটি মানের যেকোনো অংশ উপেক্ষা করে যা আমরা প্যাটার্নের বাকি অংশে স্পষ্টভাবে ম্যাচ করিনি। লিস্টিং ১৯-২৩-এ, আমাদের একটি `Point` struct আছে যা ত্রি-মাত্রিক স্থানে একটি স্থানাঙ্ক ধারণ করে। `match` এক্সপ্রেশনে, আমরা কেবল `x` স্থানাঙ্কের উপর কাজ করতে চাই এবং `y` ও `z` ফিল্ডের মানগুলো উপেক্ষা করতে চাই। <Listing number="19-23" caption="`..` ব্যবহার করে `x` ব্যতীত একটি `Point`-এর সমস্ত ফিল্ড উপেক্ষা করা"> ```rust fn main() { struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {x}"), } }
আমরা x
মানটি তালিকাভুক্ত করি এবং তারপর কেবল ..
প্যাটার্নটি অন্তর্ভুক্ত করি। এটি y: _
এবং z: _
তালিকাভুক্ত করার চেয়ে দ্রুততর, বিশেষ করে যখন আমরা এমন struct নিয়ে কাজ করি যার অনেক ফিল্ড আছে এবং যেখানে কেবল এক বা দুটি ফিল্ড প্রাসঙ্গিক।
..
সিনট্যাক্সটি যতগুলো মান প্রয়োজন ততগুলো পর্যন্ত প্রসারিত হবে। লিস্টিং ১৯-২৪ দেখায় কিভাবে একটি tuple-এর সাথে ..
ব্যবহার করতে হয়।
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }
এই কোডে, প্রথম এবং শেষ মান first
এবং last
দিয়ে ম্যাচ করা হয়। ..
মাঝের সবকিছু ম্যাচ এবং উপেক্ষা করবে।
তবে, ..
ব্যবহার অবশ্যই দ্ব্যর্থহীন হতে হবে। যদি এটি অস্পষ্ট হয় যে কোন মানগুলো ম্যাচ করার জন্য এবং কোনগুলো উপেক্ষা করা উচিত, রাস্ট আমাদের একটি এরর দেবে। লিস্টিং ১৯-২৫ ..
দ্ব্যর্থকভাবে ব্যবহার করার একটি উদাহরণ দেখায়, তাই এটি কম্পাইল হবে না।
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
যখন আমরা এই উদাহরণটি কম্পাইল করি, আমরা এই এররটি পাই:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
রাস্টের পক্ষে নির্ধারণ করা অসম্ভব যে tuple-এ second
-এর সাথে একটি মান ম্যাচ করার আগে কতগুলো মান উপেক্ষা করতে হবে এবং তারপর আরও কতগুলো মান উপেক্ষা করতে হবে। এই কোডের মানে হতে পারে যে আমরা 2
উপেক্ষা করতে চাই, second
-কে 4
-এ বাইন্ড করতে চাই, এবং তারপর 8
, 16
, এবং 32
উপেক্ষা করতে চাই; অথবা আমরা 2
এবং 4
উপেক্ষা করতে চাই, second
-কে 8
-এ বাইন্ড করতে চাই, এবং তারপর 16
এবং 32
উপেক্ষা করতে চাই; ইত্যাদি। second
ভেরিয়েবলের নামটি রাস্টের কাছে কোনো বিশেষ অর্থ বহন করে না, তাই আমরা একটি কম্পাইলার এরর পাই কারণ দুটি জায়গায় এভাবে ..
ব্যবহার করা দ্ব্যর্থক।
ম্যাচ গার্ডের সাথে অতিরিক্ত কন্ডিশনাল (Extra Conditionals with Match Guards)
একটি ম্যাচ গার্ড (match guard) হলো একটি অতিরিক্ত if
শর্ত, যা একটি match
arm-এর প্যাটার্নের পরে নির্দিষ্ট করা হয়, যা সেই arm-টি বেছে নেওয়ার জন্য অবশ্যই মিলতে হবে। ম্যাচ গার্ডগুলো একটি প্যাটার্ন একাই যা প্রকাশ করতে পারে তার চেয়ে জটিল ধারণা প্রকাশ করার জন্য কার্যকর। তবে, লক্ষ্য করুন যে এগুলো কেবল match
এক্সপ্রেশনে উপলব্ধ, if let
বা while let
এক্সপ্রেশনে নয়।
শর্তটি প্যাটার্নে তৈরি ভেরিয়েবল ব্যবহার করতে পারে। লিস্টিং ১৯-২৬ একটি match
দেখায় যেখানে প্রথম arm-এর প্যাটার্ন Some(x)
এবং একটি ম্যাচ গার্ড if x % 2 == 0
(যা সংখ্যাটি জোড় হলে true
হবে) রয়েছে।
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {x} is even"), Some(x) => println!("The number {x} is odd"), None => (), } }
এই উদাহরণটি The number 4 is even
প্রিন্ট করবে। যখন num
-কে প্রথম arm-এর প্যাটার্নের সাথে তুলনা করা হয়, তখন এটি মেলে কারণ Some(4)
Some(x)
-এর সাথে মেলে। তারপর ম্যাচ গার্ড পরীক্ষা করে যে x
-কে 2 দিয়ে ভাগ করার ভাগশেষ 0 এর সমান কিনা, এবং যেহেতু তা হয়, তাই প্রথম arm-টি নির্বাচন করা হয়।
যদি num
Some(5)
হতো, তবে প্রথম arm-এর ম্যাচ গার্ডটি false
হতো কারণ 5-কে 2 দিয়ে ভাগ করার ভাগশেষ 1, যা 0 এর সমান নয়। রাস্ট তখন দ্বিতীয় arm-এ যেত, যা মিলত কারণ দ্বিতীয় arm-এর কোনো ম্যাচ গার্ড নেই এবং তাই যেকোনো Some
ভ্যারিয়েন্টের সাথে মেলে।
if x % 2 == 0
শর্তটি একটি প্যাটার্নের মধ্যে প্রকাশ করার কোনো উপায় নেই, তাই ম্যাচ গার্ড আমাদের এই লজিকটি প্রকাশ করার ক্ষমতা দেয়। এই অতিরিক্ত প্রকাশক্ষমতার অসুবিধা হলো কম্পাইলার ম্যাচ গার্ড এক্সপ্রেশন জড়িত থাকলে এক্সহস্টিভনেস (exhaustiveness) পরীক্ষা করার চেষ্টা করে না।
লিস্টিং ১৯-১১-এ, আমরা উল্লেখ করেছি যে আমরা আমাদের প্যাটার্ন-শ্যাডোইং সমস্যা সমাধানের জন্য ম্যাচ গার্ড ব্যবহার করতে পারি। মনে করুন, আমরা match
এক্সপ্রেশনের বাইরের ভেরিয়েবল ব্যবহার না করে প্যাটার্নের ভিতরে একটি নতুন ভেরিয়েবল তৈরি করেছিলাম। সেই নতুন ভেরিয়েবলের মানে হলো আমরা বাইরের ভেরিয়েবলের মানের বিরুদ্ধে পরীক্ষা করতে পারিনি। লিস্টিং ১৯-২৭ দেখায় কিভাবে আমরা এই সমস্যাটি সমাধান করার জন্য একটি ম্যাচ গার্ড ব্যবহার করতে পারি।
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(n) if n == y => println!("Matched, n = {n}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
এই কোডটি এখন Default case, x = Some(5)
প্রিন্ট করবে। দ্বিতীয় ম্যাচ arm-এর প্যাটার্নটি একটি নতুন y
ভেরিয়েবল প্রবর্তন করে না যা বাইরের y
-কে শ্যাডো করবে, যার মানে আমরা ম্যাচ গার্ডে বাইরের y
ব্যবহার করতে পারি। প্যাটার্নটিকে Some(y)
হিসাবে নির্দিষ্ট করার পরিবর্তে, যা বাইরের y
-কে শ্যাডো করত, আমরা Some(n)
নির্দিষ্ট করি। এটি একটি নতুন n
ভেরিয়েবল তৈরি করে যা কিছুই শ্যাডো করে না কারণ match
-এর বাইরে কোনো n
ভেরিয়েবল নেই।
ম্যাচ গার্ড if n == y
একটি প্যাটার্ন নয় এবং তাই নতুন ভেরিয়েবল প্রবর্তন করে না। এই y
হলো বাইরের y
এবং এটি শ্যাডো করা নতুন y
নয়, এবং আমরা n
-কে y
-এর সাথে তুলনা করে বাইরের y
-এর সমান মান খুঁজতে পারি।
আপনি একাধিক প্যাটার্ন নির্দিষ্ট করার জন্য একটি ম্যাচ গার্ডে or অপারেটর |
ব্যবহার করতে পারেন; ম্যাচ গার্ড শর্তটি সমস্ত প্যাটার্নে প্রযোজ্য হবে। লিস্টিং ১৯-২৮ |
ব্যবহারকারী একটি প্যাটার্নকে একটি ম্যাচ গার্ডের সাথে একত্রিত করার সময় প্রেসিডেন্স (precedence) দেখায়। এই উদাহরণের গুরুত্বপূর্ণ অংশ হলো if y
ম্যাচ গার্ডটি 4
, 5
, এবং 6
সবগুলোর ক্ষেত্রেই প্রযোজ্য, যদিও মনে হতে পারে if y
কেবল 6
-এর ক্ষেত্রেই প্রযোজ্য।
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
ম্যাচ শর্তটি বলে যে arm-টি কেবল তখনই মিলবে যদি x
-এর মান 4
, 5
, বা 6
-এর সমান হয় এবং যদি y
true
হয়। যখন এই কোডটি চলে, প্রথম arm-এর প্যাটার্নটি মেলে কারণ x
4
, কিন্তু ম্যাচ গার্ড if y
false
, তাই প্রথম arm-টি বেছে নেওয়া হয় না। কোডটি দ্বিতীয় arm-এ চলে যায়, যা মেলে, এবং এই প্রোগ্রামটি no
প্রিন্ট করে। কারণ হলো if
শর্তটি পুরো প্যাটার্ন 4 | 5 | 6
-এর উপর প্রযোজ্য, কেবল শেষ মান 6
-এর উপর নয়। অন্য কথায়, একটি প্যাটার্নের সাথে একটি ম্যাচ গার্ডের প্রেসিডেন্স এভাবে আচরণ করে:
(4 | 5 | 6) if y => ...
এর পরিবর্তে:
4 | 5 | (6 if y) => ...
কোডটি চালানোর পরে, প্রেসিডেন্স আচরণটি স্পষ্ট: যদি ম্যাচ গার্ডটি কেবল |
অপারেটর ব্যবহার করে নির্দিষ্ট করা মানগুলোর তালিকার চূড়ান্ত মানের উপর প্রয়োগ করা হতো, তবে arm-টি মিলত এবং প্রোগ্রামটি yes
প্রিন্ট করত।
@
বাইন্ডিং (@
Bindings)
at অপারেটর @
আমাদের একটি ভেরিয়েবল তৈরি করতে দেয় যা একটি মান ধারণ করে একই সময়ে যখন আমরা সেই মানটিকে একটি প্যাটার্ন ম্যাচের জন্য পরীক্ষা করছি। লিস্টিং ১৯-২৯-এ, আমরা পরীক্ষা করতে চাই যে একটি Message::Hello
id
ফিল্ডটি 3..=7
রেঞ্জের মধ্যে আছে কিনা। আমরা মানটিকে id
ভেরিয়েবলে বাইন্ড করতে চাই যাতে আমরা arm-এর সাথে যুক্ত কোডে এটি ব্যবহার করতে পারি।
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id @ 3..=7 } => { println!("Found an id in range: {id}") } Message::Hello { id: 10..=12 } => { println!("Found an id in another range") } Message::Hello { id } => println!("Found some other id: {id}"), } }
এই উদাহরণটি Found an id in range: 5
প্রিন্ট করবে। id @
নির্দিষ্ট করে 3..=7
রেঞ্জের আগে, আমরা রেঞ্জ প্যাটার্নের সাথে মিলে যাওয়া যেকোনো মানকে id
নামের একটি ভেরিয়েবলে ক্যাপচার করছি এবং একই সাথে পরীক্ষা করছি যে মানটি রেঞ্জ প্যাটার্নের সাথে মিলেছে কিনা।
দ্বিতীয় arm-এ, যেখানে আমরা কেবল প্যাটার্নে একটি রেঞ্জ নির্দিষ্ট করেছি, arm-এর সাথে যুক্ত কোডে এমন কোনো ভেরিয়েবল নেই যা id
ফিল্ডের আসল মান ধারণ করে। id
ফিল্ডের মান 10, 11, বা 12 হতে পারত, কিন্তু সেই প্যাটার্নের সাথে যাওয়া কোড জানে না কোনটি। প্যাটার্ন কোডটি id
ফিল্ড থেকে মান ব্যবহার করতে সক্ষম নয়, কারণ আমরা id
মানটি একটি ভেরিয়েবলে সংরক্ষণ করিনি।
শেষ arm-এ, যেখানে আমরা একটি রেঞ্জ ছাড়াই একটি ভেরিয়েবল নির্দিষ্ট করেছি, আমাদের arm-এর কোডে ব্যবহারের জন্য id
নামের একটি ভেরিয়েবলে মানটি উপলব্ধ রয়েছে। কারণ হলো আমরা struct ফিল্ড শর্টহ্যান্ড সিনট্যাক্স ব্যবহার করেছি। কিন্তু আমরা এই arm-এ id
ফিল্ডের মানের উপর কোনো পরীক্ষা প্রয়োগ করিনি, যেমনটি আমরা প্রথম দুটি arm-এর সাথে করেছি: যেকোনো মান এই প্যাটার্নের সাথে মিলবে।
@
ব্যবহার করা আমাদের একটি মান পরীক্ষা করতে এবং এটি একটি ভেরিয়েবলে একটি প্যাটার্নের মধ্যে সংরক্ষণ করতে দেয়।
সারাংশ (Summary)
রাস্টের প্যাটার্নগুলো বিভিন্ন ধরণের ডেটার মধ্যে পার্থক্য করার জন্য খুব কার্যকর। match
এক্সপ্রেশনে ব্যবহার করা হলে, রাস্ট নিশ্চিত করে যে আপনার প্যাটার্নগুলো প্রতিটি সম্ভাব্য মান কভার করে, নতুবা আপনার প্রোগ্রাম কম্পাইল হবে না। let
স্টেটমেন্ট এবং ফাংশন প্যারামিটারে প্যাটার্নগুলো সেই কনস্ট্রাক্টগুলোকে আরও কার্যকর করে তোলে, মানগুলোকে ছোট অংশে ডিস্ট্রাকচার করতে এবং সেই অংশগুলোকে ভেরিয়েবলে অ্যাসাইন করতে সক্ষম করে। আমরা আমাদের প্রয়োজন অনুসারে সহজ বা জটিল প্যাটার্ন তৈরি করতে পারি।
এরপর, বইয়ের উপশেষ অধ্যায়ের জন্য, আমরা রাস্টের বিভিন্ন বৈশিষ্ট্যের কিছু উন্নত দিক দেখব।
অ্যাডভান্সড ফিচার (Advanced Features)
আপনারা এতক্ষণে রাস্ট প্রোগ্রামিং ল্যাঙ্গুয়েজের সবচেয়ে বেশি ব্যবহৃত অংশগুলো শিখে ফেলেছেন। চ্যাপ্টার ২১-এ আমরা আরও একটি প্রজেক্ট করার আগে, ল্যাঙ্গুয়েজের এমন কিছু দিক দেখব যা আপনার হয়তো মাঝে মাঝে চোখে পড়বে, কিন্তু প্রতিদিন ব্যবহার করা হবে না। যখনই কোনো অজানা বিষয়ের সম্মুখীন হবেন, তখন এই চ্যাপ্টারটিকে একটি রেফারেন্স হিসেবে ব্যবহার করতে পারবেন। এখানে আলোচনা করা ফিচারগুলো খুব নির্দিষ্ট পরিস্থিতিতে দরকারি। যদিও আপনি হয়তো এগুলো প্রায়ই ব্যবহার করবেন না, আমরা নিশ্চিত করতে চাই যে রাস্টের সমস্ত ফিচার সম্পর্কে আপনার ধারণা রয়েছে।
এই চ্যাপ্টারে আমরা আলোচনা করব:
- Unsafe Rust: কীভাবে রাস্টের কিছু গ্যারান্টি থেকে বের হয়ে আসা যায় এবং সেই গ্যারান্টিগুলো ম্যানুয়ালি বজায় রাখার দায়িত্ব নেওয়া যায়।
- Advanced traits: associated types, default type parameters, fully qualified syntax, supertraits, এবং traits সম্পর্কিত newtype pattern।
- Advanced types: newtype pattern, type aliases, the never type, এবং dynamically sized types সম্পর্কে আরও আলোচনা।
- Advanced functions and closures: function pointers এবং closure রিটার্ন করার কৌশল।
- Macros: এমন কোড ডিফাইন করার উপায় যা কম্পাইল টাইমে আরও কোড তৈরি করে।
এটি রাস্টের বিভিন্ন ফিচারের একটি সমাহার, যেখানে প্রত্যেকের জন্য কিছু না কিছু আছে! চলুন শুরু করা যাক
Unsafe Rust
এখন পর্যন্ত আমরা যে কোড নিয়ে আলোচনা করেছি, তার সবকিছুতেই রাস্টের memory safety গ্যারান্টি কম্পাইল টাইমে প্রয়োগ করা হয়েছে। তবে, রাস্টের ভেতরে আরও একটি দ্বিতীয় ল্যাঙ্গুয়েজ লুকিয়ে আছে যা এই memory safety গ্যারান্টিগুলো প্রয়োগ করে না: একে বলা হয় unsafe Rust। এটি সাধারণ রাস্টের মতোই কাজ করে, কিন্তু আমাদের কিছু অতিরিক্ত ক্ষমতা বা সুপারপাওয়ার দেয়।
Unsafe Rust এর অস্তিত্বের কারণ হলো, static analysis স্বভাবতই রক্ষণশীল (conservative) হয়। যখন কম্পাইলার কোনো কোড গ্যারান্টিগুলো মেনে চলে কি না তা নির্ধারণ করার চেষ্টা করে, তখন কিছু অবৈধ প্রোগ্রাম গ্রহণ করার চেয়ে কিছু বৈধ প্রোগ্রাম বাতিল করে দেওয়া শ্রেয়। যদিও কোডটি ঠিক থাকতেও পারে, কিন্তু রাস্ট কম্পাইলারের কাছে যদি আত্মবিশ্বাসী হওয়ার মতো যথেষ্ট তথ্য না থাকে, তবে এটি কোডটি বাতিল করে দেবে। এই সব ক্ষেত্রে, আপনি unsafe কোড ব্যবহার করে কম্পাইলারকে বলতে পারেন, "বিশ্বাস করো, আমি জানি আমি কী করছি।" তবে সাবধান থাকবেন যে আপনি নিজের ঝুঁকিতেই unsafe Rust ব্যবহার করছেন: যদি আপনি unsafe কোড ভুলভাবে ব্যবহার করেন, তাহলে memory unsafety-এর কারণে বিভিন্ন সমস্যা দেখা দিতে পারে, যেমন null pointer dereferencing।
রাস্টের একটি unsafe সত্তা থাকার আরেকটি কারণ হলো, কম্পিউটারের underlying হার্ডওয়্যার স্বাভাবিকভাবেই unsafe। যদি রাস্ট আপনাকে unsafe অপারেশন করতে না দিত, তবে আপনি নির্দিষ্ট কিছু কাজ করতে পারতেন না। রাস্টকে আপনাকে low-level সিস্টেম প্রোগ্রামিং করার সুবিধা দিতে হয়, যেমন সরাসরি অপারেটিং সিস্টেমের সাথে ইন্টারঅ্যাক্ট করা বা এমনকি নিজের অপারেটিং সিস্টেম লেখা। low-level সিস্টেম প্রোগ্রামিং নিয়ে কাজ করা এই ল্যাঙ্গুয়েজের অন্যতম একটি লক্ষ্য। চলুন দেখি unsafe Rust দিয়ে আমরা কী করতে পারি এবং কীভাবে তা করতে পারি।
Unsafe Superpowers
Unsafe Rust-এ সুইচ করতে, unsafe
কীওয়ার্ডটি ব্যবহার করুন এবং তারপর একটি নতুন ব্লক শুরু করুন যেখানে unsafe কোড থাকবে। আপনি unsafe Rust-এ পাঁচটি কাজ করতে পারেন যা safe Rust-এ করা যায় না, যেগুলোকে আমরা unsafe superpowers বলি। সেই সুপারপাওয়ারগুলোর মধ্যে রয়েছে:
- একটি raw pointer dereference করা
- একটি unsafe function বা method কল করা
- একটি mutable static variable অ্যাক্সেস বা মডিফাই করা
- একটি unsafe trait ইমপ্লিমেন্ট করা
union
-এর ফিল্ড অ্যাক্সেস করা
এটা বোঝা গুরুত্বপূর্ণ যে unsafe
borrow checker বন্ধ করে না বা রাস্টের অন্য কোনো সেফটি চেক নিষ্ক্রিয় করে না: আপনি যদি unsafe কোডে একটি reference ব্যবহার করেন, তবে তা 여전히 চেক করা হবে। unsafe
কীওয়ার্ডটি আপনাকে শুধুমাত্র এই পাঁচটি ফিচারের অ্যাক্সেস দেয়, যা কম্পাইলার memory safety-এর জন্য পরীক্ষা করে না। আপনি একটি unsafe ব্লকের ভেতরেও একটি নির্দিষ্ট স্তরের নিরাপত্তা পাবেন।
এছাড়াও, unsafe
মানে এই নয় যে ব্লকের ভেতরের কোডটি বিপজ্জনক বা এতে অবশ্যই memory safety সমস্যা থাকবে: এর উদ্দেশ্য হলো, প্রোগ্রামার হিসেবে আপনি নিশ্চিত করবেন যে unsafe
ব্লকের ভেতরের কোডটি বৈধ উপায়ে মেমরি অ্যাক্সেস করবে।
মানুষ ভুল করে এবং ভুল হবেই, কিন্তু এই পাঁচটি unsafe অপারেশনকে unsafe
দিয়ে চিহ্নিত ব্লকের মধ্যে রাখার ফলে আপনি জানতে পারবেন যে memory safety সম্পর্কিত যেকোনো ত্রুটি অবশ্যই একটি unsafe
ব্লকের মধ্যেই রয়েছে। unsafe
ব্লকগুলো ছোট রাখুন; পরে যখন আপনি মেমরি বাগ তদন্ত করবেন, তখন এর জন্য কৃতজ্ঞ থাকবেন।
Unsafe কোডকে যথাসম্ভব বিচ্ছিন্ন রাখতে, এই ধরনের কোডকে একটি safe abstraction-এর মধ্যে আবদ্ধ করা এবং একটি safe API সরবরাহ করা সবচেয়ে ভালো। এই বিষয়ে আমরা এই চ্যাপ্টারের পরে আলোচনা করব যখন আমরা unsafe function এবং method পরীক্ষা করব। স্ট্যান্ডার্ড লাইব্রেরির কিছু অংশ unsafe কোডের উপর safe abstraction হিসেবে প্রয়োগ করা হয়েছে যা নিরীক্ষিত (audited) হয়েছে। Unsafe কোডকে একটি safe abstraction-এ মোড়ানো হলে unsafe
-এর ব্যবহার সেই সব জায়গায় ছড়িয়ে পড়া থেকে আটকানো যায় যেখানে আপনি বা আপনার ব্যবহারকারীরা unsafe
কোড দিয়ে প্রয়োগ করা ফাংশনালিটি ব্যবহার করতে চাইতে পারেন, কারণ একটি safe abstraction ব্যবহার করা নিরাপদ।
চলুন এক এক করে পাঁচটি unsafe superpower দেখে নেওয়া যাক। আমরা এমন কিছু abstraction-ও দেখব যা unsafe কোডের জন্য একটি safe ইন্টারফেস সরবরাহ করে।
একটি Raw Pointer Dereference করা (Dereferencing a Raw Pointer)
চ্যাপ্টার ৪-এর “Dangling References” সেকশনে আমরা উল্লেখ করেছি যে কম্পাইলার নিশ্চিত করে reference-গুলো সবসময় বৈধ থাকে। Unsafe Rust-এ raw pointers নামে দুটি নতুন টাইপ আছে যা reference-এর মতোই। Reference-এর মতো, raw pointer-ও immutable বা mutable হতে পারে এবং এগুলো যথাক্রমে *const T
এবং *mut T
হিসেবে লেখা হয়। এখানে অ্যাস্টেরিস্ক (*) dereference অপারেটর নয়; এটি টাইপের নামের অংশ। Raw pointer-এর ক্ষেত্রে, immutable মানে হলো পয়েন্টারটি dereference করার পর সরাসরি অ্যাসাইন করা যাবে না।
Reference এবং smart pointer-এর থেকে raw pointer ভিন্ন কারণ:
- এদেরকে borrowing rule উপেক্ষা করার অনুমতি দেওয়া হয়, যেমন একই লোকেশনে immutable এবং mutable উভয় পয়েন্টার অথবা একাধিক mutable পয়েন্টার থাকতে পারে।
- এরা যে বৈধ মেমরিতে পয়েন্ট করবে তার কোনো গ্যারান্টি নেই।
- এদের null হওয়ার অনুমতি আছে।
- এরা কোনো স্বয়ংক্রিয় cleanup প্রয়োগ করে না।
রাস্টের এই গ্যারান্টিগুলো প্রয়োগ করা থেকে বিরত থাকার মাধ্যমে, আপনি গ্যারান্টিযুক্ত নিরাপত্তার বিনিময়ে আরও বেশি পারফরম্যান্স বা অন্য কোনো ল্যাঙ্গুয়েজ বা হার্ডওয়্যারের সাথে ইন্টারফেস করার ক্ষমতা পেতে পারেন, যেখানে রাস্টের গ্যারান্টিগুলো প্রযোজ্য নয়।
লিস্টিং ২০-১ দেখাচ্ছে কীভাবে একটি immutable এবং একটি mutable raw pointer তৈরি করতে হয়।
fn main() { let mut num = 5; let r1 = &raw const num; let r2 = &raw mut num; }
লক্ষ্য করুন যে আমরা এই কোডে unsafe
কীওয়ার্ড অন্তর্ভুক্ত করিনি। আমরা safe কোডে raw pointer তৈরি করতে পারি; কিন্তু একটি unsafe ব্লকের বাইরে raw pointer dereference করতে পারি না, যা আপনি একটু পরেই দেখতে পাবেন।
আমরা raw borrow অপারেটর ব্যবহার করে raw pointer তৈরি করেছি: &raw const num
একটি *const i32
immutable raw pointer তৈরি করে এবং &raw mut num
একটি *mut i32
mutable raw pointer তৈরি করে। যেহেতু আমরা এগুলো সরাসরি একটি লোকাল ভ্যারিয়েবল থেকে তৈরি করেছি, আমরা জানি যে এই নির্দিষ্ট raw pointer-গুলো বৈধ, কিন্তু যেকোনো raw pointer সম্পর্কে আমরা এই ধারণা করতে পারি না।
এটি দেখানোর জন্য, পরবর্তীতে আমরা এমন একটি raw pointer তৈরি করব যার বৈধতা সম্পর্কে আমরা এতটা নিশ্চিত হতে পারব না, এর জন্য as
কীওয়ার্ড ব্যবহার করে একটি মান কাস্ট করব, raw borrow অপারেটর ব্যবহার না করে। লিস্টিং ২০-২ দেখাচ্ছে কীভাবে মেমরির একটি নির্বিচারী (arbitrary) লোকেশনে একটি raw pointer তৈরি করা যায়। নির্বিচারী মেমরি ব্যবহার করার চেষ্টা করাটা undefined: সেই অ্যাড্রেসে ডেটা থাকতেও পারে বা নাও থাকতে পারে, কম্পাইলার কোডটি এমনভাবে অপটিমাইজ করতে পারে যাতে কোনো মেমরি অ্যাক্সেস না হয়, অথবা প্রোগ্রামটি একটি সেগমেন্টেশন ফল্ট (segmentation fault) দিয়ে বন্ধ হয়ে যেতে পারে। সাধারণত, এই ধরনের কোড লেখার কোনো ভালো কারণ নেই, বিশেষ করে যেখানে আপনি raw borrow অপারেটর ব্যবহার করতে পারেন, তবে এটি সম্ভব।
fn main() { let address = 0x012345usize; let r = address as *const i32; }``` </Listing> মনে রাখবেন যে আমরা safe কোডে raw pointer তৈরি করতে পারি, কিন্তু আমরা raw pointer _dereference_ করতে এবং পয়েন্ট করা ডেটা পড়তে পারি না। লিস্টিং ২০-৩ এ, আমরা একটি raw pointer-এর উপর dereference অপারেটর `*` ব্যবহার করি যার জন্য একটি `unsafe` ব্লক প্রয়োজন। <Listing number="20-3" caption="একটি `unsafe` ব্লকের মধ্যে raw pointers dereference করা"> ```rust fn main() { let mut num = 5; let r1 = &raw const num; let r2 = &raw mut num; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); } }
একটি পয়েন্টার তৈরি করা কোনো ক্ষতি করে না; শুধুমাত্র যখন আমরা এর নির্দেশিত মান অ্যাক্সেস করার চেষ্টা করি, তখনই আমরা একটি অবৈধ মানের সম্মুখীন হতে পারি।
আরও লক্ষ্য করুন যে লিস্টিং ২০-১ এবং ২০-৩ এ, আমরা *const i32
এবং *mut i32
raw pointer তৈরি করেছি যা উভয়ই একই মেমরি লোকেশনে নির্দেশ করে, যেখানে num
স্টোর করা আছে। যদি আমরা এর পরিবর্তে num
-এর জন্য একটি immutable এবং একটি mutable reference তৈরি করার চেষ্টা করতাম, কোডটি কম্পাইল হতো না কারণ রাস্টের ownership rule একই সময়ে কোনো immutable reference-এর সাথে একটি mutable reference-কে অনুমতি দেয় না। Raw pointer ব্যবহার করে, আমরা একই লোকেশনে একটি mutable পয়েন্টার এবং একটি immutable পয়েন্টার তৈরি করতে পারি এবং mutable পয়েন্টারের মাধ্যমে ডেটা পরিবর্তন করতে পারি, যা সম্ভাব্যভাবে একটি data race তৈরি করতে পারে। সাবধান!
এই সব বিপদ থাকা সত্ত্বেও, আপনি কেন raw pointer ব্যবহার করবেন? একটি বড় ব্যবহারের ক্ষেত্র হলো সি (C) কোডের সাথে ইন্টারফেসিং করার সময়, যা আপনি পরবর্তী বিভাগে দেখতে পাবেন। আরেকটি ক্ষেত্র হলো যখন এমন safe abstraction তৈরি করা হয় যা borrow checker বুঝতে পারে না। আমরা প্রথমে unsafe function পরিচিত করাব এবং তারপর unsafe কোড ব্যবহার করে এমন একটি safe abstraction-এর উদাহরণ দেখব।
একটি Unsafe Function বা Method কল করা (Calling an Unsafe Function or Method)
দ্বিতীয় যে অপারেশনটি আপনি একটি unsafe ব্লকে করতে পারেন তা হলো unsafe function কল করা। Unsafe function এবং method দেখতে সাধারণ function এবং method-এর মতোই, কিন্তু তাদের সংজ্ঞার বাকি অংশের আগে একটি অতিরিক্ত unsafe
থাকে। এই প্রসঙ্গে unsafe
কীওয়ার্ডটি নির্দেশ করে যে ফাংশনটির এমন কিছু প্রয়োজনীয়তা রয়েছে যা আমাদের এই ফাংশনটি কল করার সময় পূরণ করতে হবে, কারণ রাস্ট গ্যারান্টি দিতে পারে না যে আমরা এই প্রয়োজনীয়তাগুলো পূরণ করেছি। একটি unsafe
ব্লকের মধ্যে একটি unsafe function কল করার মাধ্যমে, আমরা বলছি যে আমরা এই ফাংশনের ডকুমেন্টেশন পড়েছি এবং আমরা ফাংশনের contract বা চুক্তিগুলো রক্ষা করার দায়িত্ব নিচ্ছি।
এখানে dangerous
নামে একটি unsafe function রয়েছে যা তার বডিতে কিছুই করে না:
fn main() { unsafe fn dangerous() {} unsafe { dangerous(); } }
আমাদের অবশ্যই dangerous
ফাংশনটিকে একটি পৃথক unsafe
ব্লকের মধ্যে কল করতে হবে। যদি আমরা unsafe
ব্লক ছাড়া dangerous
কল করার চেষ্টা করি, আমরা একটি এরর পাব:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
unsafe
ব্লকের মাধ্যমে, আমরা রাস্টকে জানাচ্ছি যে আমরা ফাংশনের ডকুমেন্টেশন পড়েছি, আমরা এটি সঠিকভাবে কীভাবে ব্যবহার করতে হয় তা বুঝি, এবং আমরা যাচাই করেছি যে আমরা ফাংশনের contract পূরণ করছি।
একটি unsafe
ফাংশনের বডিতে unsafe অপারেশন করার জন্য, আপনাকে এখনও একটি unsafe
ব্লক ব্যবহার করতে হবে, যেমনটা একটি সাধারণ ফাংশনের মধ্যে করা হয়, এবং যদি আপনি ভুলে যান তবে কম্পাইলার আপনাকে সতর্ক করবে। এটি আমাদের unsafe
ব্লকগুলিকে যতটা সম্ভব ছোট রাখতে সাহায্য করে, কারণ unsafe অপারেশনগুলি পুরো ফাংশন বডি জুড়ে প্রয়োজন নাও হতে পারে।
Unsafe কোডের উপর একটি Safe Abstraction তৈরি করা
শুধু একটি ফাংশনে unsafe কোড থাকলেই পুরো ফাংশনটিকে unsafe হিসেবে চিহ্নিত করার প্রয়োজন নেই। আসলে, unsafe কোডকে একটি safe ফাংশনে মোড়ানো একটি সাধারণ abstraction। উদাহরণস্বরূপ, চলুন স্ট্যান্ডার্ড লাইব্রেরির split_at_mut
ফাংশনটি দেখি, যার জন্য কিছু unsafe কোড প্রয়োজন। আমরা দেখব এটি কীভাবে প্রয়োগ করা যেতে পারে। এই safe মেথডটি mutable slice-এর উপর সংজ্ঞায়িত করা হয়েছে: এটি একটি slice নেয় এবং আর্গুমেন্ট হিসেবে দেওয়া ইনডেক্সে slice-টিকে ভাগ করে দুটি slice তৈরি করে। লিস্টিং ২০-৪ দেখাচ্ছে কীভাবে split_at_mut
ব্যবহার করতে হয়।
fn main() { let mut v = vec![1, 2, 3, 4, 5, 6]; let r = &mut v[..]; let (a, b) = r.split_at_mut(3); assert_eq!(a, &mut [1, 2, 3]); assert_eq!(b, &mut [4, 5, 6]); }
আমরা শুধুমাত্র safe Rust ব্যবহার করে এই ফাংশনটি প্রয়োগ করতে পারি না। একটি প্রচেষ্টা লিস্টিং ২০-৫ এর মতো হতে পারে, যা কম্পাইল হবে না। সরলতার জন্য, আমরা split_at_mut
কে একটি মেথডের পরিবর্তে একটি ফাংশন হিসেবে প্রয়োগ করব এবং শুধুমাত্র i32
মানের slice-এর জন্য, জেনেরিক টাইপ T
-এর জন্য নয়।
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
এই ফাংশনটি প্রথমে slice-টির মোট দৈর্ঘ্য (length) নেয়। তারপর এটি নিশ্চিত করে যে প্যারামিটার হিসেবে দেওয়া ইনডেক্সটি slice-এর মধ্যে আছে কি না, এটি দৈর্ঘ্যের চেয়ে কম বা সমান কি না তা পরীক্ষা করে। এই assertion-এর অর্থ হলো যদি আমরা slice ভাগ করার জন্য দৈর্ঘ্যের চেয়ে বড় একটি ইনডেক্স পাস করি, তাহলে ফাংশনটি সেই ইনডেক্স ব্যবহার করার চেষ্টা করার আগেই প্যানিক (panic) করবে।
তারপর আমরা একটি টাপলে (tuple) দুটি mutable slice রিটার্ন করি: একটি আসল slice-এর শুরু থেকে mid
ইনডেক্স পর্যন্ত এবং অন্যটি mid
থেকে slice-এর শেষ পর্যন্ত।
যখন আমরা লিস্টিং ২০-৫ এর কোড কম্পাইল করার চেষ্টা করব, আমরা একটি এরর পাব:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
রাস্টের borrow checker বুঝতে পারে না যে আমরা slice-এর বিভিন্ন অংশ borrow করছি; এটি কেবল জানে যে আমরা একই slice থেকে দুবার borrow করছি। একটি slice-এর বিভিন্ন অংশ borrow করা মৌলিকভাবে ঠিক আছে কারণ দুটি slice ওভারল্যাপিং নয়, কিন্তু রাস্ট এটি জানার মতো যথেষ্ট স্মার্ট নয়। যখন আমরা জানি কোডটি ঠিক আছে, কিন্তু রাস্ট তা জানে না, তখন unsafe কোড ব্যবহার করার সময় আসে।
লিস্টিং ২০-৬ দেখাচ্ছে কীভাবে একটি unsafe
ব্লক, একটি raw pointer, এবং unsafe ফাংশনের কিছু কল ব্যবহার করে split_at_mut
এর প্রয়োগ কাজ করানো যায়।
use std::slice; fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } fn main() { let mut vector = vec![1, 2, 3, 4, 5, 6]; let (left, right) = split_at_mut(&mut vector, 3); }
চ্যাপ্টার ৪-এর “The Slice Type” সেকশন থেকে মনে করুন যে একটি slice হলো কিছু ডেটার একটি পয়েন্টার এবং slice-টির দৈর্ঘ্য। আমরা একটি slice-এর দৈর্ঘ্য পেতে len
মেথড এবং একটি slice-এর raw pointer অ্যাক্সেস করতে as_mut_ptr
মেথড ব্যবহার করি। এই ক্ষেত্রে, যেহেতু আমাদের কাছে i32
মানের একটি mutable slice আছে, as_mut_ptr
একটি *mut i32
টাইপের raw pointer রিটার্ন করে, যা আমরা ptr
ভ্যারিয়েবলে সংরক্ষণ করেছি।
আমরা mid
ইনডেক্সটি slice-এর মধ্যে থাকার assertion টি বজায় রাখি। তারপর আমরা unsafe কোডে আসি: slice::from_raw_parts_mut
ফাংশনটি একটি raw pointer এবং একটি দৈর্ঘ্য নেয়, এবং এটি একটি slice তৈরি করে। আমরা এই ফাংশনটি ব্যবহার করে একটি slice তৈরি করি যা ptr
থেকে শুরু হয় এবং mid
আইটেম দীর্ঘ। তারপর আমরা ptr
-এর উপর add
মেথড কল করি mid
কে আর্গুমেন্ট হিসেবে দিয়ে একটি raw pointer পেতে যা mid
থেকে শুরু হয়, এবং আমরা সেই পয়েন্টার এবং mid
-এর পরে থাকা বাকি আইটেমের সংখ্যাকে দৈর্ঘ্য হিসেবে ব্যবহার করে একটি slice তৈরি করি।
slice::from_raw_parts_mut
ফাংশনটি unsafe কারণ এটি একটি raw pointer নেয় এবং বিশ্বাস করতে হয় যে এই পয়েন্টারটি বৈধ। Raw pointer-এর উপর add
মেথডটিও unsafe কারণ এটিকে বিশ্বাস করতে হয় যে অফসেট লোকেশনটিও একটি বৈধ পয়েন্টার। তাই, আমাদের slice::from_raw_parts_mut
এবং add
-এ আমাদের কলগুলোর চারপাশে একটি unsafe
ব্লক রাখতে হয়েছিল যাতে আমরা সেগুলি কল করতে পারি। কোডটি দেখে এবং mid
অবশ্যই len
-এর চেয়ে কম বা সমান হতে হবে এই assertion যোগ করে, আমরা বলতে পারি যে unsafe
ব্লকের মধ্যে ব্যবহৃত সমস্ত raw pointer slice-এর মধ্যে থাকা ডেটার বৈধ পয়েন্টার হবে। এটি unsafe
-এর একটি গ্রহণযোগ্য এবং উপযুক্ত ব্যবহার।
লক্ষ্য করুন যে আমাদের ফলস্বরূপ split_at_mut
ফাংশনটিকে unsafe
হিসেবে চিহ্নিত করার প্রয়োজন নেই, এবং আমরা এই ফাংশনটিকে safe Rust থেকে কল করতে পারি। আমরা unsafe কোডের জন্য একটি safe abstraction তৈরি করেছি এমন একটি ফাংশনের প্রয়োগের সাথে যা unsafe
কোডকে নিরাপদ উপায়ে ব্যবহার করে, কারণ এটি কেবল সেই ডেটা থেকে বৈধ পয়েন্টার তৈরি করে যা এই ফাংশনটির অ্যাক্সেস আছে।
বিপরীতে, লিস্টিং ২০-৭-এ slice::from_raw_parts_mut
-এর ব্যবহার সম্ভবত slice ব্যবহার করার সময় ক্র্যাশ করবে। এই কোডটি একটি নির্বিচারী মেমরি লোকেশন নেয় এবং ১০,০০০ আইটেম দীর্ঘ একটি slice তৈরি করে।
fn main() { use std::slice; let address = 0x01234usize; let r = address as *mut i32; let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; }
আমরা এই নির্বিচারী লোকেশনের মেমরির মালিক নই, এবং এমন কোনো গ্যারান্টি নেই যে এই কোডটি যে slice তৈরি করে তাতে বৈধ i32
মান রয়েছে। values
-কে একটি বৈধ slice হিসেবে ব্যবহার করার চেষ্টা করলে undefined behavior ঘটে।
এক্সটার্নাল কোড কল করার জন্য extern
ফাংশন ব্যবহার করা
কখনও কখনও আপনার রাস্ট কোডকে অন্য ভাষায় লেখা কোডের সাথে ইন্টারঅ্যাক্ট করার প্রয়োজন হতে পারে। এর জন্য, রাস্টের extern
কীওয়ার্ড রয়েছে যা একটি Foreign Function Interface (FFI) তৈরি এবং ব্যবহারে সহায়তা করে। FFI হল একটি প্রোগ্রামিং ভাষার জন্য ফাংশন সংজ্ঞায়িত করার একটি উপায়, যা একটি ভিন্ন (বিদেশী) প্রোগ্রামিং ভাষাকে সেই ফাংশনগুলিকে কল করতে সক্ষম করে।
লিস্টিং ২০-৮ দেখাচ্ছে কীভাবে সি স্ট্যান্ডার্ড লাইব্রেরি থেকে abs
ফাংশনের সাথে ইন্টিগ্রেশন সেট আপ করতে হয়। extern
ব্লকের মধ্যে ঘোষিত ফাংশনগুলি রাস্ট কোড থেকে কল করা সাধারণত unsafe হয়, তাই extern
ব্লকগুলিকেও unsafe
হিসাবে চিহ্নিত করতে হবে। এর কারণ হল অন্যান্য ভাষা রাস্টের নিয়ম এবং গ্যারান্টি প্রয়োগ করে না, এবং রাস্ট সেগুলি পরীক্ষা করতে পারে না, তাই নিরাপত্তা নিশ্চিত করার দায়িত্ব প্রোগ্রামারের উপর বর্তায়।
unsafe extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } }
unsafe extern "C"
ব্লকের মধ্যে, আমরা অন্য ভাষা থেকে যে এক্সটার্নাল ফাংশনগুলি কল করতে চাই তাদের নাম এবং সিগনেচার তালিকাভুক্ত করি। "C"
অংশটি সংজ্ঞায়িত করে যে এক্সটার্নাল ফাংশনটি কোন application binary interface (ABI) ব্যবহার করে: ABI অ্যাসেম্বলি লেভেলে ফাংশনটি কীভাবে কল করতে হয় তা নির্ধারণ করে। "C"
ABI সবচেয়ে সাধারণ এবং সি প্রোগ্রামিং ভাষার ABI অনুসরণ করে। রাস্টের সমর্থিত সমস্ত ABI সম্পর্কে তথ্য the Rust Reference-এ পাওয়া যায়।
একটি unsafe extern
ব্লকের মধ্যে ঘোষিত প্রতিটি আইটেম অন্তর্নিহিতভাবে unsafe। তবে, কিছু FFI ফাংশন কল করা safe। উদাহরণস্বরূপ, সি-এর স্ট্যান্ডার্ড লাইব্রেরির abs
ফাংশনে কোনো memory safety সংক্রান্ত বিবেচনা নেই এবং আমরা জানি এটি যেকোনো i32
দিয়ে কল করা যেতে পারে। এই ধরনের ক্ষেত্রে, আমরা safe
কীওয়ার্ড ব্যবহার করে বলতে পারি যে এই নির্দিষ্ট ফাংশনটি কল করা safe যদিও এটি একটি unsafe extern
ব্লকের মধ্যে রয়েছে। একবার আমরা সেই পরিবর্তনটি করলে, এটি কল করার জন্য আর একটি unsafe
ব্লকের প্রয়োজন হয় না, যেমনটি লিস্টিং ২০-৯-এ দেখানো হয়েছে।
unsafe extern "C" { safe fn abs(input: i32) -> i32; } fn main() { println!("Absolute value of -3 according to C: {}", abs(-3)); }
একটি ফাংশনকে safe
হিসাবে চিহ্নিত করা এটিকে অন্তর্নিহিতভাবে safe করে তোলে না! পরিবর্তে, এটি রাস্টের কাছে আপনার করা একটি প্রতিশ্রুতির মতো যে এটি safe। সেই প্রতিশ্রুতি রক্ষা করা হয়েছে কিনা তা নিশ্চিত করার দায়িত্ব এখনও আপনার!
অন্য ভাষা থেকে রাস্ট ফাংশন কল করা
আমরা extern
ব্যবহার করে একটি ইন্টারফেস তৈরি করতে পারি যা অন্য ভাষাকে রাস্ট ফাংশন কল করার অনুমতি দেয়। একটি সম্পূর্ণ extern
ব্লক তৈরি করার পরিবর্তে, আমরা extern
কীওয়ার্ড যোগ করি এবং সংশ্লিষ্ট ফাংশনের fn
কীওয়ার্ডের ঠিক আগে ব্যবহার করার জন্য ABI নির্দিষ্ট করি। আমাদের এই ফাংশনের নামটি যাতে রাস্ট কম্পাইলার ম্যাঙ্গল (mangle) না করে তা বলার জন্য একটি #[unsafe(no_mangle)]
টীকাও যোগ করতে হবে। Mangling হল যখন একটি কম্পাইলার আমাদের দেওয়া একটি ফাংশনের নামকে একটি ভিন্ন নামে পরিবর্তন করে যা কম্পাইলেশন প্রক্রিয়ার অন্যান্য অংশগুলির জন্য আরও তথ্য ধারণ করে কিন্তু মানুষের জন্য কম পাঠযোগ্য হয়। প্রতিটি প্রোগ্রামিং ভাষার কম্পাইলার নামগুলিকে সামান্য ভিন্নভাবে ম্যাঙ্গল করে, তাই একটি রাস্ট ফাংশনকে অন্য ভাষা দ্বারা নামকরণযোগ্য করার জন্য, আমাদের অবশ্যই রাস্ট কম্পাইলারের নাম ম্যাংলিং নিষ্ক্রিয় করতে হবে। এটি unsafe কারণ বিল্ট-ইন ম্যাংলিং ছাড়া লাইব্রেরি জুড়ে নামের সংঘর্ষ হতে পারে, তাই আমরা যে নামটি বেছে নিই তা ম্যাংলিং ছাড়াই এক্সপোর্ট করার জন্য safe কিনা তা নিশ্চিত করা আমাদের দায়িত্ব।
নিম্নলিখিত উদাহরণে, আমরা call_from_c
ফাংশনটিকে সি কোড থেকে অ্যাক্সেসযোগ্য করে তুলি, এটি একটি শেয়ার্ড লাইব্রেরিতে কম্পাইল হওয়ার এবং সি থেকে লিঙ্ক করার পরে:
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
extern
-এর এই ব্যবহারে শুধুমাত্র অ্যাট্রিবিউটে unsafe
প্রয়োজন, extern
ব্লকে নয়।
একটি Mutable Static Variable অ্যাক্সেস বা মডিফাই করা
এই বইয়ে আমরা এখনও গ্লোবাল ভ্যারিয়েবল নিয়ে কথা বলিনি, যা রাস্ট সমর্থন করে কিন্তু রাস্টের ownership rules-এর সাথে সমস্যাযুক্ত হতে পারে। যদি দুটি থ্রেড একই mutable গ্লোবাল ভ্যারিয়েবল অ্যাক্সেস করে, তবে এটি একটি data race ঘটাতে পারে।
রাস্টে, গ্লোবাল ভ্যারিয়েবলকে static ভ্যারিয়েবল বলা হয়। লিস্টিং ২০-১০ একটি স্ট্রিং স্লাইস মান সহ একটি স্ট্যাটিক ভেরিয়েবলের ঘোষণা এবং ব্যবহারের উদাহরণ দেখায়।
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("value is: {HELLO_WORLD}"); }
Static ভ্যারিয়েবলগুলো ধ্রুবকের (constants) মতো, যা আমরা চ্যাপ্টার ৩-এর “Constants” সেকশনে আলোচনা করেছি। Static ভ্যারিয়েবলের নাম প্রথা অনুযায়ী SCREAMING_SNAKE_CASE
-এ থাকে। Static ভ্যারিয়েবলগুলো কেবল 'static
লাইফটাইম সহ reference সংরক্ষণ করতে পারে, যার মানে হল রাস্ট কম্পাইলার লাইফটাইম বের করতে পারে এবং আমাদের স্পষ্টভাবে এটি চিহ্নিত করার প্রয়োজন নেই। একটি immutable static variable অ্যাক্সেস করা নিরাপদ।
ধ্রুবক এবং immutable static ভ্যারিয়েবলের মধ্যে একটি সূক্ষ্ম পার্থক্য হল যে একটি static ভ্যারিয়েবলের মান মেমরিতে একটি নির্দিষ্ট ঠিকানা থাকে। মানটি ব্যবহার করলে সর্বদা একই ডেটা অ্যাক্সেস করা হবে। অন্যদিকে, ধ্রুবকগুলি যখনই ব্যবহার করা হয় তখন তাদের ডেটা নকল করার অনুমতি দেওয়া হয়। আরেকটি পার্থক্য হল static ভ্যারিয়েবলগুলো mutable হতে পারে। Mutable static ভ্যারিয়েবল অ্যাক্সেস এবং মডিফাই করা unsafe। লিস্টিং ২০-১১ দেখায় কীভাবে COUNTER
নামের একটি mutable static ভ্যারিয়েবল ঘোষণা, অ্যাক্সেস এবং মডিফাই করতে হয়।
static mut COUNTER: u32 = 0; /// SAFETY: Calling this from more than a single thread at a time is undefined /// behavior, so you *must* guarantee you only call it from a single thread at /// a time. unsafe fn add_to_count(inc: u32) { unsafe { COUNTER += inc; } } fn main() { unsafe { // SAFETY: This is only called from a single thread in `main`. add_to_count(3); println!("COUNTER: {}", *(&raw const COUNTER)); } }
সাধারণ ভ্যারিয়েবলের মতো, আমরা mut
কীওয়ার্ড ব্যবহার করে মিউটেবিলিটি নির্দিষ্ট করি। COUNTER
থেকে পড়া বা লেখার যেকোনো কোড অবশ্যই একটি unsafe
ব্লকের মধ্যে থাকতে হবে। লিস্টিং ২০-১১-এর কোডটি কম্পাইল হয় এবং COUNTER: 3
প্রিন্ট করে যেমনটি আমরা আশা করি কারণ এটি একক-থ্রেডেড (single-threaded)। একাধিক থ্রেড COUNTER
অ্যাক্সেস করলে সম্ভবত data race হবে, তাই এটি undefined behavior। অতএব, আমাদের পুরো ফাংশনটিকে unsafe
হিসাবে চিহ্নিত করতে হবে এবং সেফটি সীমাবদ্ধতা নথিভুক্ত করতে হবে, যাতে যে কেউ ফাংশনটি কল করে তারা জানে যে তারা নিরাপদে কী করতে পারে এবং কী করতে পারে না।
যখনই আমরা একটি unsafe ফাংশন লিখি, তখন SAFETY
দিয়ে শুরু হওয়া একটি মন্তব্য লেখা এবং কলারকে ফাংশনটি নিরাপদে কল করার জন্য কী করতে হবে তা ব্যাখ্যা করা একটি প্রথা। একইভাবে, যখনই আমরা একটি unsafe অপারেশন সম্পাদন করি, তখন SAFETY
দিয়ে শুরু হওয়া একটি মন্তব্য লেখা এবং সেফটি নিয়মগুলি কীভাবে বজায় রাখা হয় তা ব্যাখ্যা করা একটি প্রথা।
অতিরিক্তভাবে, কম্পাইলার একটি কম্পাইলার লিন্টের মাধ্যমে একটি mutable static ভ্যারিয়েবলের রেফারেন্স তৈরির যেকোনো প্রচেষ্টা ডিফল্টরূপে অস্বীকার করবে। আপনাকে অবশ্যই #[allow(static_mut_refs)]
টীকা যোগ করে সেই লিন্টের সুরক্ষা থেকে স্পষ্টভাবে অপ্ট-আউট করতে হবে অথবা raw borrow অপারেটরগুলির একটি দিয়ে তৈরি একটি raw pointer-এর মাধ্যমে mutable static ভ্যারিয়েবলটি অ্যাক্সেস করতে হবে। এর মধ্যে এমন ক্ষেত্রগুলিও অন্তর্ভুক্ত রয়েছে যেখানে রেফারেন্সটি অদৃশ্যভাবে তৈরি হয়, যেমন এই কোড তালিকায় println!
-এ ব্যবহৃত হলে। static mutable ভ্যারিয়েবলের রেফারেন্সগুলিকে raw pointer-এর মাধ্যমে তৈরি করার প্রয়োজনীয়তা তাদের ব্যবহারের জন্য সেফটি প্রয়োজনীয়তাগুলিকে আরও স্পষ্ট করতে সহায়তা করে।
বিশ্বব্যাপী অ্যাক্সেসযোগ্য mutable ডেটার সাথে, কোনো data race নেই তা নিশ্চিত করা কঠিন, যে কারণে রাস্ট mutable static ভ্যারিয়েবলগুলিকে unsafe বলে মনে করে। যেখানে সম্ভব, চ্যাপ্টার ১৬-এ আলোচনা করা concurrency কৌশল এবং থ্রেড-সেফ স্মার্ট পয়েন্টারগুলি ব্যবহার করা বাঞ্ছনীয় যাতে কম্পাইলার পরীক্ষা করে যে বিভিন্ন থ্রেড থেকে ডেটা অ্যাক্সেস নিরাপদে করা হয়েছে।
একটি Unsafe Trait ইমপ্লিমেন্ট করা (Implementing an Unsafe Trait)
আমরা একটি unsafe trait ইমপ্লিমেন্ট করতে unsafe
ব্যবহার করতে পারি। একটি trait unsafe হয় যখন এর অন্তত একটি মেথডের এমন কোনো invariant থাকে যা কম্পাইলার যাচাই করতে পারে না। আমরা trait
-এর আগে unsafe
কীওয়ার্ড যোগ করে একটি trait-কে unsafe
ঘোষণা করি এবং trait-এর ইমপ্লিমেন্টেশনটিকেও unsafe
হিসেবে চিহ্নিত করি, যেমনটি লিস্টিং ২০-১২-এ দেখানো হয়েছে।
unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } fn main() {}
unsafe impl
ব্যবহার করে, আমরা প্রতিশ্রুতি দিচ্ছি যে আমরা সেইসব invariant বজায় রাখব যা কম্পাইলার যাচাই করতে পারে না।
উদাহরণস্বরূপ, চ্যাপ্টার ১৬-এর “Extensible Concurrency with the Send
and Sync
Traits” সেকশনে আলোচনা করা Send
এবং Sync
মার্কার trait-গুলোর কথা মনে করুন: যদি আমাদের টাইপগুলো সম্পূর্ণরূপে Send
এবং Sync
ইমপ্লিমেন্ট করা অন্যান্য টাইপ দ্বারা গঠিত হয় তবে কম্পাইলার এই trait-গুলো স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করে। যদি আমরা এমন একটি টাইপ ইমপ্লিমেন্ট করি যাতে Send
বা Sync
ইমপ্লিমেন্ট করে না এমন একটি টাইপ থাকে, যেমন raw pointers, এবং আমরা সেই টাইপটিকে Send
বা Sync
হিসেবে চিহ্নিত করতে চাই, তবে আমাদের অবশ্যই unsafe
ব্যবহার করতে হবে। রাস্ট যাচাই করতে পারে না যে আমাদের টাইপটি থ্রেড জুড়ে নিরাপদে পাঠানো বা একাধিক থ্রেড থেকে অ্যাক্সেস করার গ্যারান্টি বজায় রাখে; তাই, আমাদের সেইসব পরীক্ষা ম্যানুয়ালি করতে হবে এবং unsafe
দিয়ে তা নির্দেশ করতে হবে।
একটি Union-এর ফিল্ড অ্যাক্সেস করা (Accessing Fields of a Union)
শেষ যে কাজটি শুধুমাত্র unsafe
দিয়ে করা যায় তা হল একটি union-এর ফিল্ড অ্যাক্সেস করা। একটি union struct
-এর মতোই, কিন্তু একটি নির্দিষ্ট ইনস্ট্যান্সে একবারে শুধুমাত্র একটি ঘোষিত ফিল্ড ব্যবহার করা হয়। Union প্রধানত সি কোডে union-এর সাথে ইন্টারফেস করার জন্য ব্যবহৃত হয়। Union ফিল্ড অ্যাক্সেস করা unsafe কারণ রাস্ট গ্যারান্টি দিতে পারে না যে union ইনস্ট্যান্সে বর্তমানে কোন ধরনের ডেটা সংরক্ষণ করা হচ্ছে। আপনি the Rust Reference-এ union সম্পর্কে আরও জানতে পারেন।
Unsafe কোড পরীক্ষা করার জন্য Miri ব্যবহার করা (Using Miri to Check Unsafe Code)
Unsafe কোড লেখার সময়, আপনি যা লিখেছেন তা আসলে নিরাপদ এবং সঠিক কিনা তা পরীক্ষা করতে চাইতে পারেন। এটি করার অন্যতম সেরা উপায় হল Miri ব্যবহার করা, যা undefined behavior সনাক্ত করার জন্য একটি অফিসিয়াল রাস্ট টুল। যেখানে borrow checker একটি static টুল যা কম্পাইল টাইমে কাজ করে, Miri একটি dynamic টুল যা রানটাইমে কাজ করে। এটি আপনার প্রোগ্রাম বা এর টেস্ট স্যুট চালিয়ে আপনার কোড পরীক্ষা করে এবং যখন আপনি রাস্টের নিয়ম লঙ্ঘন করেন তখন তা সনাক্ত করে।
Miri ব্যবহার করার জন্য রাস্টের একটি নাইটলি বিল্ড প্রয়োজন (যা আমরা Appendix G: How Rust is Made and “Nightly Rust”-এ আরও আলোচনা করি)। আপনি rustup +nightly component add miri
টাইপ করে রাস্টের একটি নাইটলি সংস্করণ এবং Miri টুল উভয়ই ইনস্টল করতে পারেন। এটি আপনার প্রোজেক্ট কোন রাস্ট সংস্করণ ব্যবহার করে তা পরিবর্তন করে না; এটি কেবল আপনার সিস্টেমে টুলটি যুক্ত করে যাতে আপনি যখন চান তখন এটি ব্যবহার করতে পারেন। আপনি একটি প্রোজেক্টে cargo +nightly miri run
বা cargo +nightly miri test
টাইপ করে Miri চালাতে পারেন।
এটি কতটা সহায়ক হতে পারে তার একটি উদাহরণের জন্য, লিস্টিং ২০-৭ এর বিরুদ্ধে এটি চালালে কী ঘটে তা বিবেচনা করুন।
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
--> src/main.rs:5:13
|
5 | let r = address as *mut i32;
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
= note: BACKTRACE:
= note: inside `main` at src/main.rs:5:13: 5:32
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
--> src/main.rs:7:35
|
7 | let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:7:35: 7:70
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
Miri সঠিকভাবে আমাদের সতর্ক করে যে আমরা একটি ইন্টিজারকে একটি পয়েন্টারে কাস্ট করছি, যা একটি সমস্যা হতে পারে কিন্তু Miri এটি সনাক্ত করতে পারে না কারণ এটি জানে না পয়েন্টারটি কীভাবে উদ্ভূত হয়েছে। তারপর, Miri একটি ত্রুটি প্রদান করে যেখানে লিস্টিং ২০-৭-এর undefined behavior রয়েছে কারণ আমাদের একটি ড্যাংলিং পয়েন্টার আছে। Miri-কে ধন্যবাদ, আমরা এখন জানি যে undefined behavior-এর ঝুঁকি রয়েছে, এবং আমরা কোডটিকে কীভাবে নিরাপদ করা যায় সে সম্পর্কে ভাবতে পারি। কিছু ক্ষেত্রে, Miri এমনকি ত্রুটিগুলি কীভাবে ঠিক করতে হয় সে সম্পর্কে সুপারিশও করতে পারে।
Unsafe কোড লেখার সময় আপনি যা কিছু ভুল করতে পারেন Miri তার সবকিছু ধরে না। Miri একটি ডায়নামিক বিশ্লেষণ টুল, তাই এটি কেবল সেই কোডের সমস্যাগুলি ধরে যা আসলে চালানো হয়। এর মানে হল আপনার লেখা unsafe কোড সম্পর্কে আপনার আত্মবিশ্বাস বাড়ানোর জন্য আপনাকে এটি ভাল টেস্টিং কৌশলের সাথে ব্যবহার করতে হবে। Miri আপনার কোড unsound হওয়ার সমস্ত সম্ভাব্য উপায়ও কভার করে না।
অন্যভাবে বলতে গেলে: যদি Miri একটি সমস্যা ধরে, আপনি জানেন যে একটি বাগ আছে, কিন্তু শুধু Miri একটি বাগ না ধরার মানে এই নয় যে কোনো সমস্যা নেই। তবে এটি অনেক কিছু ধরতে পারে। এই অধ্যায়ের অন্যান্য unsafe কোডের উদাহরণগুলিতে এটি চালানোর চেষ্টা করুন এবং দেখুন এটি কী বলে!
আপনি Miri সম্পর্কে আরও জানতে পারেন its GitHub repository-তে।
কখন Unsafe কোড ব্যবহার করবেন (When to Use Unsafe Code)
আলোচিত পাঁচটি সুপারপাওয়ারের একটি ব্যবহার করার জন্য unsafe
ব্যবহার করা ভুল বা এমনকি নিন্দনীয়ও নয়, তবে unsafe
কোড সঠিক করা আরও কঠিন কারণ কম্পাইলার মেমরি সেফটি বজায় রাখতে সাহায্য করতে পারে না। যখন আপনার unsafe
কোড ব্যবহার করার কোনো কারণ থাকে, আপনি তা করতে পারেন, এবং সুস্পষ্ট unsafe
টীকা থাকা সমস্যা দেখা দিলে তার উৎস খুঁজে বের করা সহজ করে তোলে। যখনই আপনি unsafe কোড লিখবেন, আপনি Miri ব্যবহার করে আপনার লেখা কোড রাস্টের নিয়ম বজায় রাখে কিনা সে সম্পর্কে আরও আত্মবিশ্বাসী হতে পারেন।
Unsafe Rust-এর সাথে কার্যকরভাবে কীভাবে কাজ করতে হয় সে সম্পর্কে আরও গভীর অনুসন্ধানের জন্য, রাস্টের এই বিষয়ে অফিসিয়াল গাইড, Rustonomicon পড়ুন।
Advanced Traits
আমরা প্রথমবার চ্যাপ্টার ১০-এর “Traits: Defining Shared Behavior”-এ trait নিয়ে আলোচনা করেছিলাম, কিন্তু তখন আমরা আরও গভীরে যাইনি। এখন যেহেতু আপনি রাস্ট সম্পর্কে আরও কিছু জানেন, আমরা খুঁটিনাটি বিষয়গুলোতে প্রবেশ করতে পারি।
Associated Types
Associated types একটি টাইপ প্লেসহোল্ডারকে একটি trait-এর সাথে সংযুক্ত করে, যাতে trait মেথডের সংজ্ঞাগুলো তাদের সিগনেচারে এই প্লেসহোল্ডার টাইপগুলো ব্যবহার করতে পারে। কোনো trait-এর ইমপ্লিমেন্টর নির্দিষ্ট ইমপ্লিমেন্টেশনের জন্য প্লেসহোল্ডার টাইপের পরিবর্তে কোন সুনির্দিষ্ট (concrete) টাইপ ব্যবহার করা হবে তা নির্দিষ্ট করে দেবে। এইভাবে, আমরা এমন একটি trait সংজ্ঞায়িত করতে পারি যা কিছু টাইপ ব্যবহার করে, কিন্তু trait টি ইমপ্লিমেন্ট না করা পর্যন্ত সেই টাইপগুলো ঠিক কী তা জানার প্রয়োজন হয় না।
আমরা এই চ্যাপ্টারের বেশিরভাগ advanced feature-কে এমনভাবে বর্ণনা করেছি যা খুব কমই প্রয়োজন হয়। Associated types মাঝামাঝি অবস্থানে রয়েছে: এগুলো বইয়ের বাকি অংশে ব্যাখ্যা করা feature-গুলোর চেয়ে কম ব্যবহৃত হয়, তবে এই চ্যাপ্টারে আলোচিত অন্যান্য অনেক feature-এর চেয়ে বেশি ব্যবহৃত হয়।
Associated type সহ একটি trait-এর উদাহরণ হলো স্ট্যান্ডার্ড লাইব্রেরির Iterator
trait। এর associated type-টির নাম Item
এবং এটি Iterator
trait ইমপ্লিমেন্ট করা টাইপটি যে মানের উপর ইটারেট করছে তার টাইপের প্রতিনিধিত্ব করে। Iterator
trait-এর সংজ্ঞাটি লিস্টিং ২০-১৩-এ দেখানো হয়েছে।
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Item
টাইপটি একটি প্লেসহোল্ডার, এবং next
মেথডের সংজ্ঞা দেখায় যে এটি Option<Self::Item>
টাইপের মান রিটার্ন করবে। Iterator
trait-এর ইমপ্লিমেন্টররা Item
-এর জন্য সুনির্দিষ্ট টাইপ নির্দিষ্ট করবে এবং next
মেথডটি সেই সুনির্দিষ্ট টাইপের মানসহ একটি Option
রিটার্ন করবে।
Associated types-কে generics-এর মতো একটি ধারণা বলে মনে হতে পারে, কারণ generics আমাদের কোনো ফাংশন সংজ্ঞায়িত করার সুযোগ দেয় যেখানে এটি কোন টাইপ পরিচালনা করতে পারে তা নির্দিষ্ট করার প্রয়োজন হয় না। এই দুটি ধারণার মধ্যে পার্থক্য পরীক্ষা করার জন্য, আমরা Counter
নামক একটি টাইপের উপর Iterator
trait-এর একটি ইমপ্লিমেন্টেশন দেখব যা নির্দিষ্ট করে যে Item
টাইপটি হলো u32
:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
এই সিনট্যাক্সটি generics-এর সিনট্যাক্সের সাথে তুলনীয় বলে মনে হয়। তাহলে কেন Iterator
trait-টিকে generics দিয়ে সংজ্ঞায়িত করা হলো না, যেমনটি লিস্টিং ২০-১৪-তে দেখানো হয়েছে?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
পার্থক্য হলো, যখন generics ব্যবহার করা হয়, যেমন লিস্টিং ২০-১৪-তে, আমাদের প্রতিটি ইমপ্লিমেন্টেশনে টাইপগুলো annotate করতে হবে; কারণ আমরা Counter
-এর জন্য Iterator<String>
বা অন্য যেকোনো টাইপও ইমপ্লিমেন্ট করতে পারতাম, ফলে Counter
-এর জন্য Iterator
-এর একাধিক ইমপ্লিমেন্টেশন থাকতে পারত। অন্য কথায়, যখন একটি trait-এর একটি জেনেরিক প্যারামিটার থাকে, তখন এটি একটি টাইপের জন্য একাধিকবার ইমপ্লিমেন্ট করা যেতে পারে, প্রতিবার জেনেরিক টাইপ প্যারামিটারের সুনির্দিষ্ট টাইপ পরিবর্তন করে। যখন আমরা Counter
-এ next
মেথড ব্যবহার করতাম, তখন আমাদের Iterator
-এর কোন ইমপ্লিমেন্টেশনটি ব্যবহার করতে চাই তা নির্দেশ করার জন্য টাইপ অ্যানোটেশন সরবরাহ করতে হতো।
Associated types-এর সাথে, আমাদের টাইপ annotate করার প্রয়োজন হয় না কারণ আমরা একটি টাইপের উপর একটি trait একাধিকবার ইমপ্লিমেন্ট করতে পারি না। লিস্টিং ২০-১৩-তে associated types ব্যবহার করা সংজ্ঞার সাথে, আমরা কেবল একবারই Item
-এর টাইপ কী হবে তা বেছে নিতে পারি কারণ impl Iterator for Counter
কেবল একটাই থাকতে পারে। আমাদের প্রতিবার Counter
-এ next
কল করার সময় নির্দিষ্ট করতে হবে না যে আমরা u32
মানের একটি iterator চাই।
Associated types trait-এর চুক্তিরও অংশ হয়ে যায়: trait-এর ইমপ্লিমেন্টরদের অবশ্যই associated type প্লেসহোল্ডারের জন্য একটি টাইপ সরবরাহ করতে হবে। Associated types-এর প্রায়শই একটি নাম থাকে যা বর্ণনা করে যে টাইপটি কীভাবে ব্যবহৃত হবে এবং API ডকুমেন্টেশনে associated type-টি নথিভুক্ত করা একটি ভাল অভ্যাস।
Default Generic Type Parameters এবং Operator Overloading
যখন আমরা জেনেরিক টাইপ প্যারামিটার ব্যবহার করি, তখন আমরা জেনেরিক টাইপের জন্য একটি ডিফল্ট সুনির্দিষ্ট (concrete) টাইপ নির্দিষ্ট করতে পারি। এর ফলে trait-এর ইমপ্লিমেন্টরদের একটি সুনির্দিষ্ট টাইপ নির্দিষ্ট করার প্রয়োজন হয় না যদি ডিফল্ট টাইপটি কাজ করে। আপনি <PlaceholderType=ConcreteType>
সিনট্যাক্স দিয়ে একটি জেনেরিক টাইপ ঘোষণা করার সময় একটি ডিফল্ট টাইপ নির্দিষ্ট করেন।
এই কৌশলটি যেখানে কার্যকর তার একটি சிறந்த উদাহরণ হলো operator overloading, যেখানে আপনি নির্দিষ্ট পরিস্থিতিতে একটি অপারেটরের (যেমন +
) আচরণ কাস্টমাইজ করেন।
রাস্ট আপনাকে নিজের অপারেটর তৈরি করতে বা ইচ্ছামত অপারেটর ওভারলোড করার অনুমতি দেয় না। কিন্তু আপনি std::ops
-এ তালিকাভুক্ত অপারেশন এবং সংশ্লিষ্ট trait-গুলো ওভারলোড করতে পারেন অপারেটরের সাথে যুক্ত trait-গুলো ইমপ্লিমেন্ট করে। উদাহরণস্বরূপ, লিস্টিং ২০-১৫-এ আমরা দুটি Point
ইনস্ট্যান্সকে একসাথে যোগ করার জন্য +
অপারেটরটিকে ওভারলোড করেছি। আমরা একটি Point
struct-এ Add
trait ইমপ্লিমেন্ট করে এটি করি।
use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); }
add
মেথডটি দুটি Point
ইনস্ট্যান্সের x
মান এবং y
মান যোগ করে একটি নতুন Point
তৈরি করে। Add
trait-টির Output
নামে একটি associated type রয়েছে যা add
মেথড থেকে রিটার্ন করা টাইপ নির্ধারণ করে।
এই কোডে ডিফল্ট জেনেরিক টাইপটি Add
trait-এর মধ্যে রয়েছে। এখানে এর সংজ্ঞা:
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
এই কোডটি সাধারণভাবে পরিচিত মনে হওয়া উচিত: একটি মেথড এবং একটি associated type সহ একটি trait। নতুন অংশটি হলো Rhs=Self
: এই সিনট্যাক্সটিকে default type parameters বলা হয়। Rhs
জেনেরিক টাইপ প্যারামিটার ("right-hand side"-এর সংক্ষিপ্ত রূপ) add
মেথডে rhs
প্যারামিটারের টাইপ সংজ্ঞায়িত করে। যদি আমরা Add
trait ইমপ্লিমেন্ট করার সময় Rhs
-এর জন্য একটি সুনির্দিষ্ট টাইপ নির্দিষ্ট না করি, তাহলে Rhs
-এর টাইপ ডিফল্ট হিসেবে Self
হবে, যা হবে সেই টাইপ যার উপর আমরা Add
ইমপ্লিমেন্ট করছি।
যখন আমরা Point
-এর জন্য Add
ইমপ্লিমেন্ট করেছিলাম, আমরা Rhs
-এর জন্য ডিফল্ট ব্যবহার করেছি কারণ আমরা দুটি Point
ইনস্ট্যান্স যোগ করতে চেয়েছিলাম। আসুন Add
trait ইমপ্লিমেন্ট করার এমন একটি উদাহরণ দেখি যেখানে আমরা ডিফল্ট ব্যবহার না করে Rhs
টাইপটি কাস্টমাইজ করতে চাই।
আমাদের দুটি struct আছে, Millimeters
এবং Meters
, যা বিভিন্ন এককে মান ধারণ করে। একটি বিদ্যমান টাইপকে অন্য একটি struct-এ এভাবে মোড়ানোর প্রক্রিয়াকে newtype pattern বলা হয়, যা আমরা “Using the Newtype Pattern to Implement External Traits” বিভাগে আরও বিস্তারিতভাবে বর্ণনা করব। আমরা মিলিমিটারের মানকে মিটারের মানের সাথে যোগ করতে চাই এবং Add
-এর ইমপ্লিমেন্টেশনটি যেন সঠিকভাবে রূপান্তরটি করে। আমরা Millimeters
-এর জন্য Add
ইমপ্লিমেন্ট করতে পারি Meters
-কে Rhs
হিসেবে ব্যবহার করে, যেমনটি লিস্টিং ২০-১৬-তে দেখানো হয়েছে।
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Millimeters
এবং Meters
যোগ করার জন্য, আমরা Rhs
টাইপ প্যারামিটারের মান সেট করতে impl Add<Meters>
নির্দিষ্ট করি, ডিফল্ট Self
ব্যবহার করার পরিবর্তে।
আপনি দুটি প্রধান উপায়ে ডিফল্ট টাইপ প্যারামিটার ব্যবহার করবেন:
১. বিদ্যমান কোড না ভেঙে একটি টাইপ প্রসারিত করতে। ২. নির্দিষ্ট ক্ষেত্রে কাস্টমাইজেশনের অনুমতি দিতে যা বেশিরভাগ ব্যবহারকারীর প্রয়োজন হবে না।
স্ট্যান্ডার্ড লাইব্রেরির Add
trait দ্বিতীয় উদ্দেশ্যের একটি উদাহরণ: সাধারণত, আপনি দুটি একই ধরনের টাইপ যোগ করবেন, কিন্তু Add
trait এর বাইরেও কাস্টমাইজ করার ক্ষমতা প্রদান করে। Add
trait-এর সংজ্ঞায় একটি ডিফল্ট টাইপ প্যারামিটার ব্যবহার করার অর্থ হলো বেশিরভাগ সময় আপনাকে অতিরিক্ত প্যারামিটার নির্দিষ্ট করতে হবে না। অন্য কথায়, সামান্য ইমপ্লিমেন্টেশন বয়লারপ্লেটের প্রয়োজন হয় না, যা trait-টি ব্যবহার করা সহজ করে তোলে।
প্রথম উদ্দেশ্যটি দ্বিতীয়টির মতোই কিন্তু বিপরীত: যদি আপনি একটি বিদ্যমান trait-এ একটি টাইপ প্যারামিটার যোগ করতে চান, আপনি trait-এর কার্যকারিতা প্রসারিত করার জন্য এটিকে একটি ডিফল্ট দিতে পারেন বিদ্যমান ইমপ্লিমেন্টেশন কোড না ভেঙে।
একই নামের মেথডগুলোর মধ্যে পার্থক্য করা
রাস্টে এমন কোনো নিয়ম নেই যা একটি trait-কে অন্য একটি trait-এর মেথডের সাথে একই নামের মেথড থাকা থেকে বিরত রাখে, বা রাস্ট আপনাকে একটি টাইপের উপর উভয় trait ইমপ্লিমেন্ট করা থেকে বিরত রাখে না। trait-এর মেথডের মতো একই নামের একটি মেথড সরাসরি টাইপের উপর ইমপ্লিমেন্ট করাও সম্ভব।
একই নামের মেথড কল করার সময়, আপনাকে রাস্টকে বলতে হবে আপনি কোনটি ব্যবহার করতে চান। লিস্টিং ২০-১৭-এর কোডটি বিবেচনা করুন যেখানে আমরা দুটি trait, Pilot
এবং Wizard
, সংজ্ঞায়িত করেছি যে দুটিরই fly
নামে একটি মেথড আছে। তারপর আমরা Human
নামক একটি টাইপের উপর উভয় trait ইমপ্লিমেন্ট করি, যার উপর ইতিমধ্যে fly
নামের একটি মেথড ইমপ্লিমেন্ট করা আছে। প্রতিটি fly
মেথড ভিন্ন কিছু করে।
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() {}
যখন আমরা Human
-এর একটি ইনস্ট্যান্সের উপর fly
কল করি, কম্পাইলার ডিফল্ট হিসেবে সেই মেথডটি কল করে যা সরাসরি টাইপের উপর ইমপ্লিমেন্ট করা হয়েছে, যেমনটি লিস্টিং ২০-১৮-তে দেখানো হয়েছে।
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; person.fly(); }
এই কোডটি চালালে *waving arms furiously*
প্রিন্ট হবে, যা দেখায় যে রাস্ট সরাসরি Human
-এর উপর ইমপ্লিমেন্ট করা fly
মেথডটি কল করেছে।
Pilot
trait বা Wizard
trait থেকে fly
মেথডগুলো কল করতে, আমাদের কোন fly
মেথডটি বোঝাতে চাই তা নির্দিষ্ট করার জন্য আরও স্পষ্ট সিনট্যাক্স ব্যবহার করতে হবে। লিস্টিং ২০-১৯ এই সিনট্যাক্সটি প্রদর্শন করে।
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
মেথডের নামের আগে trait-এর নাম নির্দিষ্ট করা রাস্টকে স্পষ্ট করে দেয় যে আমরা fly
-এর কোন ইমপ্লিমেন্টেশনটি কল করতে চাই। আমরা Human::fly(&person)
-ও লিখতে পারতাম, যা লিস্টিং ২০-১৯-এ ব্যবহৃত person.fly()
-এর সমতুল্য, কিন্তু যদি আমাদের দ্ব্যর্থতা নিরসনের প্রয়োজন না হয় তবে এটি লিখতে একটু দীর্ঘ।
এই কোডটি চালালে নিম্নলিখিতটি প্রিন্ট হয়:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
যেহেতু fly
মেথডটি একটি self
প্যারামিটার নেয়, যদি আমাদের দুটি টাইপ থাকত যা উভয়ই একটি trait ইমপ্লিমেন্ট করত, রাস্ট self
-এর টাইপের উপর ভিত্তি করে একটি trait-এর কোন ইমপ্লিমেন্টেশনটি ব্যবহার করতে হবে তা বের করতে পারত।
তবে, যে associated function-গুলো মেথড নয়, তাদের self
প্যারামিটার থাকে না। যখন একাধিক টাইপ বা trait থাকে যা একই ফাংশন নামের non-method ফাংশন সংজ্ঞায়িত করে, রাস্ট সবসময় জানে না আপনি কোন টাইপটি বোঝাতে চান যদি না আপনি fully qualified syntax ব্যবহার করেন। উদাহরণস্বরূপ, লিস্টিং ২০-২০-এ আমরা একটি পশু আশ্রয়কেন্দ্রের জন্য একটি trait তৈরি করি যা সব বাচ্চা কুকুরের নাম Spot রাখতে চায়। আমরা baby_name
নামে একটি associated non-method ফাংশন সহ একটি Animal
trait তৈরি করি। Animal
trait টি Dog
struct-এর জন্য ইমপ্লিমেন্ট করা হয়েছে, যার উপর আমরা সরাসরি baby_name
নামে একটি associated non-method ফাংশনও সরবরাহ করি।
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); }``` </Listing> আমরা `Dog`-এর উপর সংজ্ঞায়িত `baby_name` associated function-এ সব কুকুরছানার নাম Spot রাখার কোডটি ইমপ্লিমেন্ট করি। `Dog` টাইপটি `Animal` trait-ও ইমপ্লিমেন্ট করে, যা সব প্রাণীর বৈশিষ্ট্য বর্ণনা করে। বাচ্চা কুকুরকে puppy বলা হয়, এবং এটি `Animal` trait-এর সাথে যুক্ত `baby_name` ফাংশনে `Dog`-এর উপর `Animal` trait-এর ইমপ্লিমেন্টেশনে প্রকাশ করা হয়েছে। `main`-এ, আমরা `Dog::baby_name` ফাংশনটি কল করি, যা সরাসরি `Dog`-এর উপর সংজ্ঞায়িত associated function-টিকে কল করে। এই কোডটি নিম্নলিখিতটি প্রিন্ট করে: ```console $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s Running `target/debug/traits-example` A baby dog is called a Spot
এই আউটপুটটি আমরা যা চেয়েছিলাম তা নয়। আমরা Dog
-এর উপর ইমপ্লিমেন্ট করা Animal
trait-এর অংশ baby_name
ফাংশনটি কল করতে চাই যাতে কোডটি A baby dog is called a puppy
প্রিন্ট করে। লিস্টিং ২০-১৯-এ ব্যবহৃত trait-এর নাম নির্দিষ্ট করার কৌশলটি এখানে সাহায্য করবে না; যদি আমরা main
-কে লিস্টিং ২০-২১-এর কোডে পরিবর্তন করি, আমরা একটি কম্পাইলেশন এরর পাব।
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
যেহেতু Animal::baby_name
-এর কোনো self
প্যারামিটার নেই, এবং এমন অন্যান্য টাইপ থাকতে পারে যা Animal
trait ইমপ্লিমেন্ট করে, রাস্ট বের করতে পারে না আমরা Animal::baby_name
-এর কোন ইমপ্লিমেন্টেশনটি চাই। আমরা এই কম্পাইলার এররটি পাব:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
দ্ব্যর্থতা নিরসন করতে এবং রাস্টকে বলতে যে আমরা অন্য কোনো টাইপের Animal
ইমপ্লিমেন্টেশনের পরিবর্তে Dog
-এর জন্য Animal
-এর ইমপ্লিমেন্টেশন ব্যবহার করতে চাই, আমাদের fully qualified syntax ব্যবহার করতে হবে। লিস্টিং ২০-২২ fully qualified syntax কীভাবে ব্যবহার করতে হয় তা প্রদর্শন করে।
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); }
আমরা অ্যাঙ্গেল ব্র্যাকেটের মধ্যে রাস্টকে একটি টাইপ অ্যানোটেশন প্রদান করছি, যা নির্দেশ করে যে আমরা Animal
trait থেকে baby_name
মেথডটি কল করতে চাই যা Dog
-এর উপর ইমপ্লিমেন্ট করা হয়েছে, এই ফাংশন কলের জন্য Dog
টাইপটিকে একটি Animal
হিসাবে বিবেচনা করতে বলে। এই কোডটি এখন আমরা যা চাই তা প্রিন্ট করবে:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
সাধারণভাবে, fully qualified syntax নিম্নরূপ সংজ্ঞায়িত করা হয়:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
যেসব associated function মেথড নয়, তাদের জন্য কোনো receiver
থাকবে না: কেবল অন্যান্য আর্গুমেন্টের তালিকা থাকবে। আপনি ফাংশন বা মেথড কল করার সব জায়গায় fully qualified syntax ব্যবহার করতে পারতেন। তবে, এই সিনট্যাক্সের যেকোনো অংশ যা রাস্ট প্রোগ্রামের অন্যান্য তথ্য থেকে বের করতে পারে তা বাদ দেওয়ার অনুমতি আপনার আছে। আপনাকে কেবল সেইসব ক্ষেত্রে এই দীর্ঘ সিনট্যাক্স ব্যবহার করতে হবে যেখানে একই নাম ব্যবহার করে একাধিক ইমপ্লিমেন্টেশন রয়েছে এবং রাস্টকে সনাক্ত করতে সাহায্যের প্রয়োজন হয় আপনি কোন ইমপ্লিমেন্টেশনটি কল করতে চান।
Supertraits ব্যবহার করা
কখনও কখনও আপনি এমন একটি trait সংজ্ঞা লিখতে পারেন যা অন্য একটি trait-এর উপর নির্ভরশীল: একটি টাইপের জন্য প্রথম trait-টি ইমপ্লিমেন্ট করার জন্য, আপনি চাইতে পারেন যে সেই টাইপটি দ্বিতীয় trait-টিও ইমপ্লিমেন্ট করুক। আপনি এটি করবেন যাতে আপনার trait সংজ্ঞাটি দ্বিতীয় trait-এর associated item-গুলো ব্যবহার করতে পারে। যে trait-এর উপর আপনার trait সংজ্ঞাটি নির্ভর করছে তাকে আপনার trait-এর একটি supertrait বলা হয়।
উদাহরণস্বরূপ, ধরা যাক আমরা একটি OutlinePrint
trait তৈরি করতে চাই যার outline_print
মেথডটি একটি প্রদত্ত মানকে এমনভাবে ফরম্যাট করে প্রিন্ট করবে যাতে এটি তারকাচিহ্ন দ্বারা ফ্রেম করা থাকে। অর্থাৎ, একটি Point
struct যা স্ট্যান্ডার্ড লাইব্রেরি trait Display
ইমপ্লিমেন্ট করে (x, y)
ফলাফল দেয়, যখন আমরা x
-এর জন্য 1
এবং y
-এর জন্য 3
সহ একটি Point
ইনস্ট্যান্সের উপর outline_print
কল করি, তখন এটি নিম্নলিখিতটি প্রিন্ট করা উচিত:
**********
* *
* (1, 3) *
* *
**********```
`outline_print` মেথডের ইমপ্লিমেন্টেশনে, আমরা `Display` trait-এর কার্যকারিতা ব্যবহার করতে চাই। অতএব, আমাদের নির্দিষ্ট করতে হবে যে `OutlinePrint` trait-টি কেবল সেইসব টাইপের জন্য কাজ করবে যা `Display`-ও ইমপ্লিমেন্ট করে এবং `OutlinePrint`-এর প্রয়োজনীয় কার্যকারিতা সরবরাহ করে। আমরা trait সংজ্ঞায় `OutlinePrint: Display` নির্দিষ্ট করে তা করতে পারি। এই কৌশলটি trait-এ একটি trait bound যোগ করার মতো। লিস্টিং ২০-২৩ `OutlinePrint` trait-এর একটি ইমপ্লিমেন্টেশন দেখায়।
<Listing number="20-23" file-name="src/main.rs" caption="`OutlinePrint` trait ইমপ্লিমেন্ট করা যা `Display` থেকে কার্যকারিতা প্রয়োজন">
```rust
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
#
# fn main() {}
যেহেতু আমরা নির্দিষ্ট করেছি যে OutlinePrint
-এর জন্য Display
trait প্রয়োজন, আমরা to_string
ফাংশনটি ব্যবহার করতে পারি যা Display
ইমপ্লিমেন্ট করে এমন যেকোনো টাইপের জন্য স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করা হয়। যদি আমরা trait নামের পরে একটি কোলন যোগ না করে এবং Display
trait নির্দিষ্ট না করে to_string
ব্যবহার করার চেষ্টা করতাম, আমরা একটি এরর পেতাম যে বর্তমান স্কোপে &Self
টাইপের জন্য to_string
নামের কোনো মেথড পাওয়া যায়নি।
আসুন দেখি কী ঘটে যখন আমরা Display
ইমপ্লিমেন্ট করে না এমন একটি টাইপের উপর OutlinePrint
ইমপ্লিমেন্ট করার চেষ্টা করি, যেমন Point
struct:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
আমরা একটি এরর পাব যে Display
প্রয়োজন কিন্তু ইমপ্লিমেন্ট করা হয়নি:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
এটি ঠিক করার জন্য, আমরা Point
-এর উপর Display
ইমপ্লিমেন্ট করি এবং OutlinePrint
-এর প্রয়োজনীয় শর্ত পূরণ করি, এইভাবে:
trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {output} *"); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3 }; p.outline_print(); }
তারপর, Point
-এর উপর OutlinePrint
trait ইমপ্লিমেন্ট করা সফলভাবে কম্পাইল হবে, এবং আমরা একটি Point
ইনস্ট্যান্সের উপর outline_print
কল করে এটিকে তারকাচিহ্নের একটি আউটলাইনের মধ্যে প্রদর্শন করতে পারি।
External Types-এর উপর External Traits ইমপ্লিমেন্ট করার জন্য Newtype Pattern ব্যবহার করা
চ্যাপ্টার ১০-এর “Implementing a Trait on a Type”-এ, আমরা orphan rule-এর কথা উল্লেখ করেছি যা বলে যে আমরা কেবল একটি টাইপের উপর একটি trait ইমপ্লিমেন্ট করতে পারি যদি trait বা টাইপ, অথবা উভয়ই, আমাদের crate-এর জন্য লোকাল হয়। newtype pattern ব্যবহার করে এই সীমাবদ্ধতা এড়ানো সম্ভব, যা একটি tuple struct-এ একটি নতুন টাইপ তৈরি করা জড়িত। (আমরা চ্যাপ্টার ৫-এর “Using Tuple Structs Without Named Fields to Create Different Types”-এ tuple struct কভার করেছি।) tuple struct-এর একটি ফিল্ড থাকবে এবং এটি সেই টাইপের একটি পাতলা র্যাপার (thin wrapper) হবে যার জন্য আমরা একটি trait ইমপ্লিমেন্ট করতে চাই। তারপর র্যাপার টাইপটি আমাদের crate-এর জন্য লোকাল হয়, এবং আমরা র্যাপারের উপর trait-টি ইমপ্লিমেন্ট করতে পারি। Newtype শব্দটি Haskell প্রোগ্রামিং ভাষা থেকে উদ্ভূত হয়েছে। এই প্যাটার্নটি ব্যবহার করার জন্য কোনো রানটাইম পারফরম্যান্স পেনাল্টি নেই, এবং র্যাপার টাইপটি কম্পাইল টাইমে বাদ দেওয়া হয়।
উদাহরণস্বরূপ, ধরা যাক আমরা Vec<T>
-এর উপর Display
ইমপ্লিমেন্ট করতে চাই, যা orphan rule আমাদের সরাসরি করতে বাধা দেয় কারণ Display
trait এবং Vec<T>
টাইপ উভয়ই আমাদের crate-এর বাইরে সংজ্ঞায়িত। আমরা একটি Wrapper
struct তৈরি করতে পারি যা Vec<T>
-এর একটি ইনস্ট্যান্স ধারণ করে; তারপর আমরা Wrapper
-এর উপর Display
ইমপ্লিমেন্ট করতে পারি এবং Vec<T>
মানটি ব্যবহার করতে পারি, যেমনটি লিস্টিং ২০-২৪-এ দেখানো হয়েছে।
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {w}"); }
Display
-এর ইমপ্লিমেন্টেশনটি ভিতরের Vec<T>
অ্যাক্সেস করার জন্য self.0
ব্যবহার করে কারণ Wrapper
একটি tuple struct এবং Vec<T>
tuple-এর ০ ইনডেক্সের আইটেম। তারপর আমরা Wrapper
-এর উপর Display
trait-এর কার্যকারিতা ব্যবহার করতে পারি।
এই কৌশলটি ব্যবহার করার অসুবিধা হলো Wrapper
একটি নতুন টাইপ, তাই এর মধ্যে থাকা মানের মেথডগুলো এর নেই। আমাদের Vec<T>
-এর সমস্ত মেথড সরাসরি Wrapper
-এর উপর ইমপ্লিমেন্ট করতে হবে যাতে মেথডগুলো self.0
-কে ডে্লিগেট করে, যা আমাদের Wrapper
-কে ঠিক একটি Vec<T>
-এর মতো ব্যবহার করার অনুমতি দেবে। যদি আমরা চাইতাম যে নতুন টাইপটির ভিতরের টাইপের সমস্ত মেথড থাকুক, তাহলে Wrapper
-এর উপর Deref
trait ইমপ্লিমেন্ট করে ভিতরের টাইপটি রিটার্ন করা একটি সমাধান হবে (আমরা চ্যাপ্টার ১৫-এর “Treating Smart Pointers Like Regular References with Deref
”-এ Deref
trait ইমপ্লিমেন্ট করার বিষয়ে আলোচনা করেছি)। যদি আমরা না চাইতাম যে Wrapper
টাইপটির ভিতরের টাইপের সমস্ত মেথড থাকুক—উদাহরণস্বরূপ, Wrapper
টাইপের আচরণ সীমিত করার জন্য—আমাদের কেবল সেই মেথডগুলো ম্যানুয়ালি ইমপ্লিমেন্ট করতে হতো যা আমরা চাই।
এই newtype pattern-টি তখনও কার্যকর যখন কোনো trait জড়িত থাকে না। আসুন ফোকাস পরিবর্তন করি এবং রাস্টের টাইপ সিস্টেমের সাথে ইন্টারঅ্যাক্ট করার কিছু advanced উপায় দেখি।
অ্যাডভান্সড টাইপ (Advanced Types)
রাস্ট টাইপ সিস্টেমের কিছু ফিচার আছে যা আমরা এখন পর্যন্ত উল্লেখ করেছি কিন্তু আলোচনা করিনি। আমরা প্রথমে নিউটাইপ (newtype) নিয়ে সাধারণভাবে আলোচনা করে শুরু করব এবং দেখব কেন নিউটাইপ টাইপ হিসেবে উপযোগী। এরপর আমরা টাইপ অ্যালিয়াস (type alias) নিয়ে আলোচনা করব, যা নিউটাইপের মতোই একটি ফিচার কিন্তু এর শব্দার্থ কিছুটা ভিন্ন। আমরা !
টাইপ এবং ডায়নামিক্যালি সাইজড টাইপ (dynamically sized types) নিয়েও আলোচনা করব।
টাইপ সেফটি এবং অ্যাবস্ট্র্যাকশনের জন্য নিউটাইপ প্যাটার্ন ব্যবহার করা (Using the Newtype Pattern for Type Safety and Abstraction)
এই বিভাগটি পড়ার আগে ধরে নেওয়া হচ্ছে যে আপনি পূর্ববর্তী "Using the Newtype Pattern to Implement External Traits" বিভাগটি পড়েছেন। নিউটাইপ প্যাটার্নটি আমরা এখন পর্যন্ত যা আলোচনা করেছি তার বাইরেও অন্যান্য কাজের জন্য উপযোগী, যার মধ্যে রয়েছে স্ট্যাটিক্যালি নিশ্চিত করা যে মানগুলো কখনো বিভ্রান্ত হবে না এবং একটি মানের একক (unit) নির্দেশ করা। আপনি লিস্টিং ২০-১৬-তে একক নির্দেশ করার জন্য নিউটাইপ ব্যবহারের একটি উদাহরণ দেখেছেন: মনে করুন Millimeters
এবং Meters
struct দুটি u32
মানকে একটি নিউটাইপে র্যাপ (wrap) করেছিল। যদি আমরা Millimeters
টাইপের একটি প্যারামিটারসহ একটি ফাংশন লিখতাম, তাহলে আমরা এমন কোনো প্রোগ্রাম কম্পাইল করতে পারতাম না যা ভুলবশত Meters
টাইপের একটি মান বা একটি সাধারণ u32
দিয়ে সেই ফাংশনটি কল করার চেষ্টা করত।
আমরা একটি টাইপের কিছু ইমপ্লিমেন্টেশন ডিটেইলস অ্যাবস্ট্রাক্ট করার জন্যও নিউটাইপ প্যাটার্ন ব্যবহার করতে পারি: নতুন টাইপটি একটি পাবলিক API প্রকাশ করতে পারে যা প্রাইভেট ইনার টাইপের API থেকে ভিন্ন।
নিউটাইপ অভ্যন্তরীণ ইমপ্লিমেন্টেশন লুকাতেও পারে। উদাহরণস্বরূপ, আমরা একটি People
টাইপ সরবরাহ করতে পারি যা একটি HashMap<i32, String>
-কে র্যাপ করে, যা একজন ব্যক্তির নামের সাথে সম্পর্কিত তার আইডি সংরক্ষণ করে। People
ব্যবহারকারী কোড শুধুমাত্র আমাদের সরবরাহ করা পাবলিক API-এর সাথে ইন্টারঅ্যাক্ট করবে, যেমন People
কালেকশনে একটি নাম স্ট্রিং যোগ করার একটি মেথড; সেই কোডকে জানতে হবে না যে আমরা অভ্যন্তরীণভাবে নামগুলিতে একটি i32
আইডি বরাদ্দ করি। নিউটাইপ প্যাটার্নটি এনক্যাপসুলেশন (encapsulation) অর্জনের একটি হালকা উপায়, যা আমরা চ্যাপ্টার ১৮-এর "Encapsulation that Hides Implementation Details"-এ আলোচনা করেছি।
টাইপ অ্যালিয়াস দিয়ে টাইপের সমার্থক নাম তৈরি করা (Creating Type Synonyms with Type Aliases)
রাস্ট একটি বিদ্যমান টাইপকে অন্য নাম দেওয়ার জন্য একটি টাইপ অ্যালিয়াস (type alias) ঘোষণা করার সুবিধা প্রদান করে। এর জন্য আমরা type
কীওয়ার্ড ব্যবহার করি। উদাহরণস্বরূপ, আমরা i32
-এর জন্য Kilometers
অ্যালিয়াসটি এভাবে তৈরি করতে পারি:
fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
এখন Kilometers
অ্যালিয়াসটি i32
-এর একটি সমার্থক নাম (synonym); লিস্টিং ২০-১৬-তে তৈরি করা Millimeters
এবং Meters
টাইপের মতো নয়, Kilometers
একটি পৃথক, নতুন টাইপ নয়। Kilometers
টাইপের মানগুলোকে i32
টাইপের মানের মতোই ব্যবহার করা হবে:
fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
যেহেতু Kilometers
এবং i32
একই টাইপ, আমরা উভয় টাইপের মান যোগ করতে পারি এবং আমরা i32
প্যারামিটার গ্রহণকারী ফাংশনগুলিতে Kilometers
মান পাস করতে পারি। তবে, এই পদ্ধতি ব্যবহার করে, আমরা আগে আলোচনা করা নিউটাইপ প্যাটার্ন থেকে প্রাপ্ত টাইপ-চেকিং সুবিধাগুলো পাই না। অন্য কথায়, যদি আমরা কোথাও Kilometers
এবং i32
মান মিশিয়ে ফেলি, কম্পাইলার আমাদের কোনো এরর দেবে না।
টাইপ সিনোনিমের প্রধান ব্যবহার হলো পুনরাবৃত্তি কমানো। উদাহরণস্বরূপ, আমাদের এরকম একটি দীর্ঘ টাইপ থাকতে পারে:
Box<dyn Fn() + Send + 'static>
ফাংশন সিগনেচারে এবং কোডের সর্বত্র টাইপ অ্যানোটেশন হিসেবে এই দীর্ঘ টাইপটি লেখা ক্লান্তিকর এবং ভুলপ্রবণ হতে পারে। লিস্টিং ২০-২৫-এর মতো কোডে পূর্ণ একটি প্রজেক্ট কল্পনা করুন।
fn main() { let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi")); fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) { // --snip-- } fn returns_long_type() -> Box<dyn Fn() + Send + 'static> { // --snip-- Box::new(|| ()) } }
একটি টাইপ অ্যালিয়াস পুনরাবৃত্তি কমিয়ে এই কোডটিকে আরও পরিচালনাযোগ্য করে তোলে। লিস্টিং ২০-২৬-এ, আমরা দীর্ঘ টাইপের জন্য Thunk
নামে একটি অ্যালিয়াস চালু করেছি এবং টাইপের সমস্ত ব্যবহারকে সংক্ষিপ্ত অ্যালিয়াস Thunk
দিয়ে প্রতিস্থাপন করতে পারি।
fn main() { type Thunk = Box<dyn Fn() + Send + 'static>; let f: Thunk = Box::new(|| println!("hi")); fn takes_long_type(f: Thunk) { // --snip-- } fn returns_long_type() -> Thunk { // --snip-- Box::new(|| ()) } }
এই কোডটি পড়া এবং লেখা অনেক সহজ! একটি টাইপ অ্যালিয়াসের জন্য একটি অর্থপূর্ণ নাম নির্বাচন করা আপনার উদ্দেশ্য প্রকাশ করতেও সাহায্য করতে পারে (thunk শব্দটি এমন কোডের জন্য ব্যবহৃত হয় যা পরে মূল্যায়ন করা হবে, তাই এটি একটি ক্লোজারের জন্য একটি উপযুক্ত নাম যা সংরক্ষণ করা হয়)।
টাইপ অ্যালিয়াসগুলো পুনরাবৃত্তি কমানোর জন্য Result<T, E>
টাইপের সাথেও সাধারণভাবে ব্যবহৃত হয়। স্ট্যান্ডার্ড লাইব্রেরির std::io
মডিউলটি বিবেচনা করুন। I/O অপারেশনগুলো প্রায়শই একটি Result<T, E>
রিটার্ন করে যখন অপারেশনগুলো কাজ করতে ব্যর্থ হয় তখন তা পরিচালনা করার জন্য। এই লাইব্রেরিতে একটি std::io::Error
struct আছে যা সমস্ত সম্ভাব্য I/O ত্রুটির প্রতিনিধিত্ব করে। std::io
-এর অনেক ফাংশন Result<T, E>
রিটার্ন করবে যেখানে E
হলো std::io::Error
, যেমন Write
trait-এর এই ফাংশনগুলো:
use std::fmt;
use std::io::Error;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
Result<..., Error>
অনেকবার পুনরাবৃত্তি হয়। তাই, std::io
-তে এই টাইপ অ্যালিয়াস ঘোষণাটি রয়েছে:
use std::fmt;
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
যেহেতু এই ঘোষণাটি std::io
মডিউলে রয়েছে, আমরা সম্পূর্ণ কোয়ালিফাইড অ্যালিয়াস std::io::Result<T>
ব্যবহার করতে পারি; অর্থাৎ, একটি Result<T, E>
যেখানে E
-কে std::io::Error
হিসেবে পূরণ করা হয়েছে। Write
trait ফাংশন সিগনেচারগুলো শেষ পর্যন্ত এরকম দেখায়:
use std::fmt;
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
টাইপ অ্যালিয়াস দুটি উপায়ে সাহায্য করে: এটি কোড লেখা সহজ করে এবং এটি আমাদের std::io
জুড়ে একটি সামঞ্জস্যপূর্ণ ইন্টারফেস দেয়। যেহেতু এটি একটি অ্যালিয়াস, এটি কেবল আরেকটি Result<T, E>
, যার মানে আমরা Result<T, E>
-তে কাজ করে এমন যেকোনো মেথড এর সাথে ব্যবহার করতে পারি, সেইসাথে ?
অপারেটরের মতো বিশেষ সিনট্যাক্সও।
The Never Type যা কখনো রিটার্ন করে না
রাস্টের !
নামে একটি বিশেষ টাইপ রয়েছে যা টাইপ থিওরির ভাষায় এম্পটি টাইপ (empty type) নামে পরিচিত কারণ এর কোনো মান নেই। আমরা এটিকে নেভার টাইপ (never type) বলতে পছন্দ করি কারণ এটি সেই রিটার্ন টাইপের জায়গায় বসে যখন একটি ফাংশন কখনো রিটার্ন করবে না। এখানে একটি উদাহরণ:
fn bar() -> ! {
// --snip--
panic!();
}
এই কোডটি এভাবে পড়া হয়: "ফাংশন bar
কখনো রিটার্ন করে না।" যে ফাংশনগুলো কখনো রিটার্ন করে না তাদের ডাইভারজিং ফাংশন (diverging functions) বলা হয়। আমরা !
টাইপের মান তৈরি করতে পারি না, তাই bar
কখনো রিটার্ন করতে পারে না।
কিন্তু এমন একটি টাইপের কী ব্যবহার যার জন্য আপনি কখনো মান তৈরি করতে পারবেন না? লিস্টিং ২-৫ থেকে সংখ্যা-অনুমান খেলার কোডটি মনে করুন; আমরা এর কিছুটা এখানে লিস্টিং ২০-২৭-এ পুনরুৎপাদন করেছি।
use std::cmp::Ordering;
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}");
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;
}
}
}
}
তখন আমরা এই কোডের কিছু বিবরণ এড়িয়ে গিয়েছিলাম। চ্যাপ্টার ৬-এর "The match
Control Flow Construct"-এ আমরা আলোচনা করেছি যে match
arm-গুলোকে অবশ্যই একই টাইপ রিটার্ন করতে হবে। তাই, উদাহরণস্বরূপ, নিম্নলিখিত কোডটি কাজ করে না:
fn main() {
let guess = "3";
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
}
এই কোডে guess
-এর টাইপ একটি ইন্টিজার এবং একটি স্ট্রিং হতে হতো, এবং রাস্টের প্রয়োজন যে guess
-এর কেবল একটি টাইপ থাকবে। তাহলে continue
কী রিটার্ন করে? লিস্টিং ২০-২৭-এ আমরা কীভাবে একটি arm থেকে একটি u32
রিটার্ন করার অনুমতি পেয়েছিলাম এবং অন্য একটি arm continue
দিয়ে শেষ হয়েছিল?
যেমন আপনি অনুমান করতে পারেন, continue
-এর একটি !
মান রয়েছে। অর্থাৎ, যখন রাস্ট guess
-এর টাইপ গণনা করে, তখন এটি উভয় ম্যাচ arm দেখে, আগেরটি u32
মান সহ এবং পরেরটি !
মান সহ। যেহেতু !
-এর কখনো কোনো মান থাকতে পারে না, রাস্ট সিদ্ধান্ত নেয় যে guess
-এর টাইপ হলো u32
।
এই আচরণের আনুষ্ঠানিক বর্ণনা হলো যে !
টাইপের এক্সপ্রেশনগুলোকে অন্য যেকোনো টাইপে coerce করা যেতে পারে। আমরা এই match
arm-টি continue
দিয়ে শেষ করার অনুমতি পেয়েছি কারণ continue
একটি মান রিটার্ন করে না; পরিবর্তে, এটি নিয়ন্ত্রণকে লুপের শীর্ষে ফিরিয়ে নিয়ে যায়, তাই Err
ক্ষেত্রে, আমরা কখনো guess
-এ একটি মান অ্যাসাইন করি না।
নেভার টাইপটি panic!
ম্যাক্রোর সাথেও উপযোগী। Option<T>
মানের উপর আমরা unwrap
ফাংশনটি কল করি, যা একটি মান তৈরি করে বা এই সংজ্ঞা সহ প্যানিক করে:
enum Option<T> {
Some(T),
None,
}
use crate::Option::*;
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
এই কোডে, লিস্টিং ২০-২৭-এর match
-এর মতোই একই জিনিস ঘটে: রাস্ট দেখে যে val
-এর টাইপ T
এবং panic!
-এর টাইপ !
, তাই সামগ্রিক match
এক্সপ্রেশনের ফলাফল T
। এই কোডটি কাজ করে কারণ panic!
একটি মান তৈরি করে না; এটি প্রোগ্রামটি শেষ করে দেয়। None
ক্ষেত্রে, আমরা unwrap
থেকে একটি মান রিটার্ন করব না, তাই এই কোডটি বৈধ।
শেষ একটি এক্সপ্রেশন যার টাইপ !
হলো একটি loop
:
fn main() {
print!("forever ");
loop {
print!("and ever ");
}
}
এখানে, লুপটি কখনো শেষ হয় না, তাই !
হলো এক্সপ্রেশনের মান। তবে, যদি আমরা একটি break
অন্তর্ভুক্ত করতাম তবে এটি সত্য হতো না, কারণ লুপটি break
-এ পৌঁছলে শেষ হয়ে যেত।
ডায়নামিক্যালি সাইজড টাইপ এবং Sized
Trait
রাস্টকে তার টাইপ সম্পর্কে নির্দিষ্ট কিছু বিবরণ জানতে হয়, যেমন একটি নির্দিষ্ট টাইপের মানের জন্য কতটা জায়গা বরাদ্দ করতে হবে। এটি তার টাইপ সিস্টেমের একটি কোণকে প্রথমে কিছুটা বিভ্রান্তিকর করে তোলে: ডায়নামিক্যালি সাইজড টাইপ (dynamically sized types) এর ধারণা। কখনও কখনও DSTs বা আনসাইজড টাইপ (unsized types) হিসাবে উল্লেখ করা হয়, এই টাইপগুলো আমাদের এমন মান ব্যবহার করে কোড লিখতে দেয় যার আকার আমরা কেবল রানটাইমে জানতে পারি।
আসুন str
নামক একটি ডায়নামিক্যালি সাইজড টাইপের বিবরণে প্রবেশ করি, যা আমরা বই জুড়ে ব্যবহার করে আসছি। হ্যাঁ, ঠিকই, &str
নয়, বরং str
নিজেই একটি DST। অনেক ক্ষেত্রে, যেমন ব্যবহারকারীর দ্বারা প্রবেশ করা টেক্সট সংরক্ষণ করার সময়, আমরা স্ট্রিংটি কত দীর্ঘ তা রানটাইম পর্যন্ত জানতে পারি না। এর মানে হলো আমরা str
টাইপের একটি ভেরিয়েবল তৈরি করতে পারি না, বা str
টাইপের একটি আর্গুমেন্ট নিতে পারি না। নিম্নলিখিত কোডটি বিবেচনা করুন, যা কাজ করে না:
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}
রাস্টকে জানতে হয় যে একটি নির্দিষ্ট টাইপের যেকোনো মানের জন্য কতটা মেমরি বরাদ্দ করতে হবে, এবং একটি টাইপের সমস্ত মান অবশ্যই একই পরিমাণ মেমরি ব্যবহার করবে। যদি রাস্ট আমাদের এই কোডটি লিখতে দিত, তাহলে এই দুটি str
মানকে একই পরিমাণ জায়গা নিতে হতো। কিন্তু তাদের দৈর্ঘ্য ভিন্ন: s1
-এর জন্য ১২ বাইট স্টোরেজ প্রয়োজন এবং s2
-এর জন্য ১৫ বাইট। এই কারণেই একটি ডায়নামিক্যালি সাইজড টাইপ ধারণকারী একটি ভেরিয়েবল তৈরি করা সম্ভব নয়।
তাহলে আমরা কী করব? এই ক্ষেত্রে, আপনি ইতিমধ্যে উত্তরটি জানেন: আমরা s1
এবং s2
-এর টাইপকে str
-এর পরিবর্তে &str
করি। চ্যাপ্টার ৪-এর "String Slices" থেকে মনে করুন যে স্লাইস ডেটা স্ট্রাকচারটি কেবল স্লাইসের শুরুর অবস্থান এবং দৈর্ঘ্য সংরক্ষণ করে। তাই, যদিও একটি &T
একটি একক মান যা T
কোথায় অবস্থিত তার মেমরি ঠিকানা সংরক্ষণ করে, একটি &str
হলো দুটি মান: str
-এর ঠিকানা এবং তার দৈর্ঘ্য। এভাবে, আমরা কম্পাইল টাইমে একটি &str
মানের আকার জানতে পারি: এটি একটি usize
-এর দৈর্ঘ্যের দ্বিগুণ। অর্থাৎ, আমরা সর্বদা একটি &str
-এর আকার জানি, এটি যে স্ট্রিংটিকে নির্দেশ করে তা যত দীর্ঘই হোক না কেন। সাধারণভাবে, রাস্টে ডায়নামিক্যালি সাইজড টাইপগুলো এভাবেই ব্যবহৃত হয়: তাদের একটি অতিরিক্ত মেটাডেটা থাকে যা ডায়নামিক তথ্যের আকার সংরক্ষণ করে। ডায়নামিক্যালি সাইজড টাইপের গোল্ডেন রুল হলো যে আমাদের সর্বদা ডায়নামিক্যালি সাইজড টাইপের মানগুলোকে কোনো না কোনো পয়েন্টারের পিছনে রাখতে হবে।
আমরা str
-কে সব ধরনের পয়েন্টারের সাথে একত্রিত করতে পারি: উদাহরণস্বরূপ, Box<str>
বা Rc<str>
। আসলে, আপনি এটি আগে একটি ভিন্ন ডায়নামিক্যালি সাইজড টাইপের সাথে দেখেছেন: traits। প্রতিটি trait একটি ডায়নামিক্যালি সাইজড টাইপ যা আমরা trait-এর নাম ব্যবহার করে উল্লেখ করতে পারি। চ্যাপ্টার ১৮-এর "Using Trait Objects to Abstract over Shared Behavior"-এ আমরা উল্লেখ করেছি যে trait-গুলোকে trait object হিসেবে ব্যবহার করতে হলে, আমাদের সেগুলোকে একটি পয়েন্টারের পিছনে রাখতে হবে, যেমন &dyn Trait
বা Box<dyn Trait>
(Rc<dyn Trait>
-ও কাজ করবে)।
DST-এর সাথে কাজ করার জন্য, রাস্ট Sized
trait প্রদান করে যা নির্ধারণ করে যে কোনো টাইপের আকার কম্পাইল টাইমে জানা যায় কি না। এই trait-টি স্বয়ংক্রিয়ভাবে সেই সবকিছুর জন্য ইমপ্লিমেন্ট করা হয় যার আকার কম্পাইল টাইমে জানা যায়। উপরন্তু, রাস্ট প্রতিটি জেনেরিক ফাংশনে Sized
-এর উপর একটি বাউন্ড (bound) অন্তর্নিহিতভাবে যোগ করে। অর্থাৎ, একটি জেনেরিক ফাংশন সংজ্ঞা যেমন এটি:
fn generic<T>(t: T) {
// --snip--
}
আসলে এমনভাবে ব্যবহার করা হয় যেন আমরা এটি লিখেছি:
fn generic<T: Sized>(t: T) {
// --snip--
}
ডিফল্টরূপে, জেনেরিক ফাংশনগুলো কেবল সেইসব টাইপের উপর কাজ করবে যাদের কম্পাইল টাইমে একটি পরিচিত আকার রয়েছে। তবে, আপনি এই সীমাবদ্ধতা শিথিল করতে নিম্নলিখিত বিশেষ সিনট্যাক্স ব্যবহার করতে পারেন:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
-এর উপর একটি trait bound-এর অর্থ হলো "T
Sized
হতেও পারে বা নাও হতে পারে" এবং এই নোটেশনটি ডিফল্টকে ওভাররাইড করে যে জেনেরিক টাইপগুলোর কম্পাইল টাইমে একটি পরিচিত আকার থাকতে হবে। ?Trait
সিনট্যাক্সটি এই অর্থে শুধুমাত্র Sized
-এর জন্য উপলব্ধ, অন্য কোনো trait-এর জন্য নয়।
আরও লক্ষ্য করুন যে আমরা t
প্যারামিটারের টাইপ T
থেকে &T
-তে পরিবর্তন করেছি। যেহেতু টাইপটি Sized
নাও হতে পারে, আমাদের এটিকে কোনো না কোনো পয়েন্টারের পিছনে ব্যবহার করতে হবে। এই ক্ষেত্রে, আমরা একটি রেফারেন্স বেছে নিয়েছি।
পরবর্তীতে, আমরা ফাংশন এবং ক্লোজার নিয়ে কথা বলব!
অ্যাডভান্সড ফাংশন এবং ক্লোজার (Advanced Functions and Closures)
এই বিভাগে ফাংশন এবং ক্লোজার সম্পর্কিত কিছু অ্যাডভান্সড ফিচার নিয়ে আলোচনা করা হয়েছে, যার মধ্যে রয়েছে ফাংশন পয়েন্টার এবং ক্লোজার রিটার্ন করা।
ফাংশন পয়েন্টার (Function Pointers)
আমরা ফাংশনে ক্লোজার কীভাবে পাস করতে হয় তা নিয়ে কথা বলেছি; আপনি ফাংশনে সাধারণ ফাংশনও পাস করতে পারেন! এই কৌশলটি তখন কার্যকর যখন আপনি একটি নতুন ক্লোজার সংজ্ঞায়িত না করে আপনার ইতিমধ্যে সংজ্ঞায়িত একটি ফাংশন পাস করতে চান। ফাংশনগুলো fn
(ছোট হাতের f দিয়ে) টাইপে পরিণত হয়, এটিকে Fn
ক্লোজার trait-এর সাথে গুলিয়ে ফেলবেন না। fn
টাইপটিকে ফাংশন পয়েন্টার (function pointer) বলা হয়। ফাংশন পয়েন্টার দিয়ে ফাংশন পাস করা আপনাকে অন্য ফাংশনের আর্গুমেন্ট হিসেবে ফাংশন ব্যবহার করার সুযোগ দেবে।
একটি প্যারামিটার যে একটি ফাংশন পয়েন্টার, তা নির্দিষ্ট করার সিনট্যাক্সটি ক্লোজারের মতোই, যেমনটি লিস্টিং ২০-২৮-এ দেখানো হয়েছে। এখানে আমরা add_one
নামে একটি ফাংশন সংজ্ঞায়িত করেছি যা তার প্যারামিটারের সাথে ১ যোগ করে। do_twice
ফাংশনটি দুটি প্যারামিটার নেয়: যেকোনো ফাংশনের জন্য একটি ফাংশন পয়েন্টার যা একটি i32
প্যারামিটার নেয় এবং একটি i32
রিটার্ন করে, এবং একটি i32
মান। do_twice
ফাংশনটি f
ফাংশনকে দুবার কল করে, arg
মানটি পাস করে, তারপর দুটি ফাংশন কলের ফলাফল একসাথে যোগ করে। main
ফাংশনটি add_one
এবং 5
আর্গুমেন্ট দিয়ে do_twice
-কে কল করে।
fn add_one(x: i32) -> i32 { x + 1 } fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let answer = do_twice(add_one, 5); println!("The answer is: {answer}"); }
এই কোডটি The answer is: 12
প্রিন্ট করবে। আমরা নির্দিষ্ট করেছি যে do_twice
-এর f
প্যারামিটারটি একটি fn
যা একটি i32
টাইপের প্যারামিটার নেয় এবং একটি i32
রিটার্ন করে। এরপর আমরা do_twice
-এর বডিতে f
-কে কল করতে পারি। main
-এ, আমরা add_one
ফাংশনের নামটি do_twice
-এর প্রথম আর্গুমেন্ট হিসেবে পাস করতে পারি।
ক্লোজারের মতো নয়, fn
একটি টাইপ, trait নয়, তাই আমরা সরাসরি fn
-কে প্যারামিটার টাইপ হিসেবে নির্দিষ্ট করি, Fn
trait-গুলোর একটিকে trait bound হিসেবে ব্যবহার করে একটি জেনেরিক টাইপ প্যারামিটার ঘোষণা না করে।
ফাংশন পয়েন্টারগুলো তিনটি ক্লোজার trait (Fn
, FnMut
, এবং FnOnce
) সবগুলোই ইমপ্লিমেন্ট করে, যার মানে হলো আপনি সবসময় একটি ফাংশন পয়েন্টারকে এমন একটি ফাংশনের আর্গুমেন্ট হিসেবে পাস করতে পারেন যা একটি ক্লোজার আশা করে। ফাংশন লেখার সময় একটি জেনেরিক টাইপ এবং ক্লোজার trait-গুলোর একটি ব্যবহার করা সবচেয়ে ভালো, যাতে আপনার ফাংশনগুলো ফাংশন বা ক্লোজার উভয়ই গ্রহণ করতে পারে।
তবে, একটি উদাহরণ যেখানে আপনি শুধুমাত্র fn
গ্রহণ করতে চাইবেন এবং ক্লোজার নয়, তা হলো যখন এক্সটার্নাল কোডের সাথে ইন্টারফেস করছেন যার ক্লোজার নেই: সি (C) ফাংশনগুলো আর্গুমেন্ট হিসেবে ফাংশন গ্রহণ করতে পারে, কিন্তু সি-তে ক্লোজার নেই।
আপনি কোথায় একটি ইনলাইন সংজ্ঞায়িত ক্লোজার বা একটি নামযুক্ত ফাংশন ব্যবহার করতে পারেন তার একটি উদাহরণ হিসেবে, আসুন স্ট্যান্ডার্ড লাইব্রেরির Iterator
trait দ্বারা প্রদত্ত map
মেথডের একটি ব্যবহার দেখি। সংখ্যার একটি ভেক্টরকে স্ট্রিংয়ের একটি ভেক্টরে পরিণত করার জন্য map
মেথড ব্যবহার করতে, আমরা একটি ক্লোজার ব্যবহার করতে পারি, যেমনটি লিস্টিং ২০-২৯-এ দেখানো হয়েছে।
fn main() { let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(|i| i.to_string()).collect(); }
অথবা আমরা ক্লোজারের পরিবর্তে map
-এর আর্গুমেন্ট হিসেবে একটি ফাংশনের নাম দিতে পারি। লিস্টিং ২০-৩০ দেখাচ্ছে এটি দেখতে কেমন হবে।
fn main() { let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(ToString::to_string).collect(); }
লক্ষ্য করুন যে আমাদের অবশ্যই সম্পূর্ণ কোয়ালিফাইড সিনট্যাক্স (fully qualified syntax) ব্যবহার করতে হবে যা আমরা "Advanced Traits" বিভাগে আলোচনা করেছি কারণ to_string
নামে একাধিক ফাংশন উপলব্ধ রয়েছে।
এখানে, আমরা ToString
trait-এ সংজ্ঞায়িত to_string
ফাংশনটি ব্যবহার করছি, যা স্ট্যান্ডার্ড লাইব্রেরি Display
ইমপ্লিমেন্ট করে এমন যেকোনো টাইপের জন্য ইমপ্লিমেন্ট করেছে।
চ্যাপ্টার ৬-এর "Enum Values" থেকে মনে করুন যে আমরা যে প্রতিটি enum variant সংজ্ঞায়িত করি তার নামও একটি ইনিশিয়ালাইজার ফাংশন হয়ে যায়। আমরা এই ইনিশিয়ালাইজার ফাংশনগুলোকে ফাংশন পয়েন্টার হিসেবে ব্যবহার করতে পারি যা ক্লোজার trait-গুলো ইমপ্লিমেন্ট করে, যার মানে হলো আমরা ক্লোজার গ্রহণকারী মেথডগুলোর আর্গুমেন্ট হিসেবে ইনিশিয়ালাইজার ফাংশনগুলো নির্দিষ্ট করতে পারি, যেমনটি লিস্টিং ২০-৩১-এ দেখা যায়।
fn main() { enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); }
এখানে, আমরা map
কল করা রেঞ্জের প্রতিটি u32
মান ব্যবহার করে Status::Value
-এর ইনিশিয়ালাইজার ফাংশন ব্যবহার করে Status::Value
ইনস্ট্যান্স তৈরি করছি। কিছু লোক এই স্টাইল পছন্দ করে এবং কিছু লোক ক্লোজার ব্যবহার করতে পছন্দ করে। এগুলো একই কোডে কম্পাইল হয়, তাই আপনার কাছে যেটি বেশি স্পষ্ট মনে হয় সেটিই ব্যবহার করুন।
ক্লোজার রিটার্ন করা (Returning Closures)
ক্লোজারগুলো trait দ্বারা প্রতিনিধিত্ব করা হয়, যার মানে হলো আপনি সরাসরি ক্লোজার রিটার্ন করতে পারবেন না। বেশিরভাগ ক্ষেত্রে যেখানে আপনি একটি trait রিটার্ন করতে চাইতে পারেন, সেখানে আপনি ফাংশনের রিটার্ন ভ্যালু হিসেবে trait ইমপ্লিমেন্ট করে এমন সুনির্দিষ্ট (concrete) টাইপ ব্যবহার করতে পারেন। তবে, আপনি সাধারণত ক্লোজারের সাথে তা করতে পারবেন না কারণ তাদের কোনো রিটার্নযোগ্য সুনির্দিষ্ট টাইপ নেই; উদাহরণস্বরূপ, যদি ক্লোজারটি তার স্কোপ থেকে কোনো মান ক্যাপচার করে তবে আপনাকে ফাংশন পয়েন্টার fn
-কে রিটার্ন টাইপ হিসেবে ব্যবহার করার অনুমতি নেই।
পরিবর্তে, আপনি সাধারণত impl Trait
সিনট্যাক্স ব্যবহার করবেন যা আমরা চ্যাপ্টার ১০-এ শিখেছি। আপনি Fn
, FnOnce
এবং FnMut
ব্যবহার করে যেকোনো ফাংশন টাইপ রিটার্ন করতে পারেন। উদাহরণস্বরূপ, লিস্টিং ২০-৩২-এর কোডটি ঠিকঠাক কম্পাইল হবে।
#![allow(unused)] fn main() { fn returns_closure() -> impl Fn(i32) -> i32 { |x| x + 1 } }
তবে, যেমনটি আমরা চ্যাপ্টার ১৩-এর "Closure Type Inference and Annotation"-এ উল্লেখ করেছি, প্রতিটি ক্লোজারও তার নিজস্ব স্বতন্ত্র টাইপ। যদি আপনার একই সিগনেচার কিন্তু ভিন্ন ইমপ্লিমেন্টেশন সহ একাধিক ফাংশনের সাথে কাজ করার প্রয়োজন হয়, তবে আপনাকে তাদের জন্য একটি trait object ব্যবহার করতে হবে। লিস্টিং ২০-৩৩-এ দেখানো কোডের মতো লিখলে কী ঘটে তা বিবেচনা করুন।
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
এখানে আমাদের দুটি ফাংশন আছে, returns_closure
এবং returns_initialized_closure
, উভয়ই impl Fn(i32) -> i32
রিটার্ন করে। লক্ষ্য করুন যে তারা যে ক্লোজারগুলো রিটার্ন করে তা ভিন্ন, যদিও তারা একই টাইপ ইমপ্লিমেন্ট করে। যদি আমরা এটি কম্পাইল করার চেষ্টা করি, রাস্ট আমাদের জানায় যে এটি কাজ করবে না:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:9:25>)
found opaque type `impl Fn(i32) -> i32` (opaque type at <src/main.rs:13:46>)
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
এরর মেসেজটি আমাদের বলে যে যখনই আমরা একটি impl Trait
রিটার্ন করি, রাস্ট একটি অনন্য অস্বচ্ছ টাইপ (opaque type) তৈরি করে, এমন একটি টাইপ যেখানে আমরা রাস্ট আমাদের জন্য যা তৈরি করে তার বিবরণ দেখতে পারি না, বা আমরা রাস্ট যে টাইপ তৈরি করবে তা অনুমান করে নিজে লিখতে পারি না। তাই যদিও এই ফাংশনগুলো একই trait, Fn(i32) -> i32
, ইমপ্লিমেন্ট করে এমন ক্লোজার রিটার্ন করে, রাস্ট প্রতিটির জন্য যে অস্বচ্ছ টাইপ তৈরি করে তা স্বতন্ত্র। (এটি যেমন রাস্ট ভিন্ন ভিন্ন async ব্লকের জন্য ভিন্ন ভিন্ন সুনির্দিষ্ট টাইপ তৈরি করে যদিও তাদের আউটপুট টাইপ একই হয়, যেমনটি আমরা চ্যাপ্টার ১৭-এর "Working with Any Number of Futures"-এ দেখেছি।) আমরা এখন কয়েকবার এই সমস্যার একটি সমাধান দেখেছি: আমরা একটি trait object ব্যবহার করতে পারি, যেমনটি লিস্টিং ২০-৩৪-এ দেখানো হয়েছে।
fn main() { let handlers = vec![returns_closure(), returns_initialized_closure(123)]; for handler in handlers { let output = handler(5); println!("{output}"); } } fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> { Box::new(move |x| x + init) }
এই কোডটি ঠিকঠাক কম্পাইল হবে। trait object সম্পর্কে আরও জানতে, চ্যাপ্টার ১৮-এর "Using Trait Objects That Allow for Values of Different Types" বিভাগটি দেখুন।
এর পরে, চলুন ম্যাক্রো দেখি!
ম্যাক্রো (Macros)
আমরা এই বই জুড়ে println!
-এর মতো ম্যাক্রো ব্যবহার করেছি, কিন্তু একটি ম্যাক্রো কী এবং এটি কীভাবে কাজ করে তা আমরা পুরোপুরিভাবে আলোচনা করিনি। ম্যাক্রো শব্দটি রাস্টের বিভিন্ন ফিচারের একটি পরিবারকে বোঝায়: macro_rules!
সহ ডিক্লারেটিভ ম্যাক্রো এবং তিন ধরনের প্রসিডিউরাল ম্যাক্রো:
- কাস্টম
#[derive]
ম্যাক্রো যা struct এবং enum-এ ব্যবহৃতderive
অ্যাট্রিবিউটের সাথে যোগ করা কোড নির্দিষ্ট করে। - অ্যাট্রিবিউট-লাইক ম্যাক্রো যা যেকোনো আইটেমে ব্যবহারযোগ্য কাস্টম অ্যাট্রিবিউট সংজ্ঞায়িত করে।
- ফাংশন-লাইক ম্যাক্রো যা ফাংশন কলের মতো দেখায় কিন্তু তাদের আর্গুমেন্ট হিসেবে নির্দিষ্ট করা টোকেনগুলোর উপর কাজ করে।
আমরা একে একে প্রতিটি নিয়ে আলোচনা করব, কিন্তু প্রথমে, চলুন দেখি যখন আমাদের কাছে ফাংশন আছে তখন আমাদের ম্যাক্রোর প্রয়োজন কেন।
ম্যাক্রো এবং ফাংশনের মধ্যে পার্থক্য
মৌলিকভাবে, ম্যাক্রোগুলো হলো এমন কোড লেখার একটি উপায় যা অন্য কোড লেখে, যা মেটাপ্রোগ্রামিং (metaprogramming) নামে পরিচিত। পরিশিষ্ট সি-তে, আমরা derive
অ্যাট্রিবিউট নিয়ে আলোচনা করি, যা আপনার জন্য বিভিন্ন trait-এর একটি ইমপ্লিমেন্টেশন তৈরি করে। আমরা বই জুড়ে println!
এবং vec!
ম্যাক্রোগুলোও ব্যবহার করেছি। এই সমস্ত ম্যাক্রোগুলো আপনার ম্যানুয়ালি লেখা কোডের চেয়ে বেশি কোড তৈরি করতে এক্সপ্যান্ড (expand) হয়।
মেটাপ্রোগ্রামিং আপনাকে যে পরিমাণ কোড লিখতে এবং রক্ষণাবেক্ষণ করতে হয় তা কমানোর জন্য দরকারী, যা ফাংশনেরও একটি ভূমিকা। তবে, ম্যাক্রোগুলোর কিছু অতিরিক্ত ক্ষমতা রয়েছে যা ফাংশনের নেই।
একটি ফাংশন সিগনেচারকে অবশ্যই ফাংশনের প্যারামিটারের সংখ্যা এবং টাইপ ঘোষণা করতে হয়। অন্যদিকে, ম্যাক্রোগুলো পরিবর্তনশীল সংখ্যক প্যারামিটার নিতে পারে: আমরা println!("hello")
একটি আর্গুমেন্ট দিয়ে অথবা println!("hello {}", name)
দুটি আর্গুমেন্ট দিয়ে কল করতে পারি। এছাড়াও, কম্পাইলার কোডের অর্থ ব্যাখ্যা করার আগে ম্যাক্রোগুলো এক্সপ্যান্ড হয়, তাই একটি ম্যাক্রো, উদাহরণস্বরূপ, একটি প্রদত্ত টাইপের উপর একটি trait ইমপ্লিমেন্ট করতে পারে। একটি ফাংশন তা করতে পারে না, কারণ এটি রানটাইমে কল করা হয় এবং একটি trait কম্পাইল টাইমে ইমপ্লিমেন্ট করা প্রয়োজন।
একটি ফাংশনের পরিবর্তে একটি ম্যাক্রো ইমপ্লিমেন্ট করার অসুবিধা হলো ম্যাক্রো সংজ্ঞা ফাংশন সংজ্ঞার চেয়ে বেশি জটিল কারণ আপনি রাস্ট কোড লিখছেন যা রাস্ট কোড লেখে। এই পরোক্ষতার কারণে, ম্যাক্রো সংজ্ঞা সাধারণত ফাংশন সংজ্ঞার চেয়ে পড়া, বোঝা এবং রক্ষণাবেক্ষণ করা বেশি কঠিন।
ম্যাক্রো এবং ফাংশনের মধ্যে আরেকটি গুরুত্বপূর্ণ পার্থক্য হলো আপনাকে অবশ্যই একটি ফাইলে কল করার আগে ম্যাক্রোগুলোকে সংজ্ঞায়িত করতে হবে বা স্কোপে আনতে হবে, ফাংশনের বিপরীতে যা আপনি যেকোনো জায়গায় সংজ্ঞায়িত করতে এবং যেকোনো জায়গায় কল করতে পারেন।
সাধারণ মেটাপ্রোগ্রামিংয়ের জন্য macro_rules!
সহ ডিক্লারেটিভ ম্যাক্রো
রাস্টে সবচেয়ে বহুল ব্যবহৃত ম্যাক্রোর রূপ হলো ডিক্লারেটিভ ম্যাক্রো (declarative macro)। এগুলোকে কখনও কখনও "macros by example", "macro_rules!
macros", বা শুধু "macros" বলা হয়। তাদের মূল ভিত্তি হলো, ডিক্লারেটিভ ম্যাক্রোগুলো আপনাকে রাস্টের match
এক্সপ্রেশনের মতো কিছু লেখার সুযোগ দেয়। চ্যাপ্টার ৬-এ যেমন আলোচনা করা হয়েছে, match
এক্সপ্রেশনগুলো হলো কন্ট্রোল স্ট্রাকচার যা একটি এক্সপ্রেশন নেয়, এক্সপ্রেশনের ফলস্বরূপ মানকে প্যাটার্নের সাথে তুলনা করে, এবং তারপর ম্যাচিং প্যাটার্নের সাথে যুক্ত কোড চালায়। ম্যাক্রোগুলোও একটি মানকে নির্দিষ্ট কোডের সাথে যুক্ত প্যাটার্নের সাথে তুলনা করে: এই পরিস্থিতিতে, মানটি হলো ম্যাক্রোতে পাস করা আক্ষরিক রাস্ট সোর্স কোড; প্যাটার্নগুলো সেই সোর্স কোডের কাঠামোর সাথে তুলনা করা হয়; এবং প্রতিটি প্যাটার্নের সাথে যুক্ত কোড, ম্যাচ হলে, ম্যাক্রোতে পাস করা কোডকে প্রতিস্থাপন করে। এই সবকিছু কম্পাইলেশনের সময় ঘটে।
একটি ম্যাক্রো সংজ্ঞায়িত করতে, আপনি macro_rules!
কনস্ট্রাক্ট ব্যবহার করেন। চলুন vec!
ম্যাক্রোটি কীভাবে সংজ্ঞায়িত করা হয় তা দেখে macro_rules!
কীভাবে ব্যবহার করতে হয় তা অন্বেষণ করি। চ্যাপ্টার ৮-এ আমরা দেখেছি কীভাবে আমরা নির্দিষ্ট মান সহ একটি নতুন ভেক্টর তৈরি করতে vec!
ম্যাক্রো ব্যবহার করতে পারি। উদাহরণস্বরূপ, নিম্নলিখিত ম্যাক্রোটি তিনটি ইন্টিজার ধারণকারী একটি নতুন ভেক্টর তৈরি করে:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
আমরা vec!
ম্যাক্রোটি দুটি ইন্টিজারের ভেক্টর বা পাঁচটি স্ট্রিং স্লাইসের ভেক্টর তৈরি করতেও ব্যবহার করতে পারতাম। আমরা একই কাজ করার জন্য একটি ফাংশন ব্যবহার করতে পারতাম না কারণ আমরা আগে থেকে মানের সংখ্যা বা টাইপ জানতাম না।
লিস্টিং ২০-৩৫ vec!
ম্যাক্রোর একটি সামান্য সরলীকৃত সংজ্ঞা দেখায়।
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
দ্রষ্টব্য: স্ট্যান্ডার্ড লাইব্রেরিতে
vec!
ম্যাক্রোর আসল সংজ্ঞায় আগে থেকে সঠিক পরিমাণ মেমরি প্রি-অ্যালোকেট করার কোড অন্তর্ভুক্ত রয়েছে। সেই কোডটি একটি অপ্টিমাইজেশন যা আমরা এখানে উদাহরণটি সহজ করার জন্য অন্তর্ভুক্ত করিনি।
#[macro_export]
অ্যানোটেশনটি নির্দেশ করে যে এই ম্যাক্রোটি যখনই ম্যাক্রোটি সংজ্ঞায়িত করা ক্রেটটি স্কোপে আনা হবে তখনই উপলব্ধ করা উচিত। এই অ্যানোটেশন ছাড়া, ম্যাক্রোটি স্কোপে আনা যায় না।
এরপর আমরা macro_rules!
এবং আমরা যে ম্যাক্রোটি সংজ্ঞায়িত করছি তার নাম ছাড়া বিস্ময় চিহ্ন দিয়ে ম্যাক্রো সংজ্ঞা শুরু করি। নামটি, এই ক্ষেত্রে vec
, এর পরে কোঁকড়া বন্ধনী (curly brackets) থাকে যা ম্যাক্রো সংজ্ঞার বডি বোঝায়।
vec!
বডির গঠনটি একটি match
এক্সপ্রেশনের গঠনের মতোই। এখানে আমাদের একটি আর্ম আছে যার প্যাটার্ন ( $( $x:expr ),* )
, এর পরে =>
এবং এই প্যাটার্নের সাথে যুক্ত কোডের ব্লক। যদি প্যাটার্নটি ম্যাচ করে, তবে সংশ্লিষ্ট কোডের ব্লকটি নির্গত হবে। যেহেতু এটি এই ম্যাক্রোতে একমাত্র প্যাটার্ন, তাই ম্যাচ করার একমাত্র বৈধ উপায় আছে; অন্য কোনো প্যাটার্ন একটি ত্রুটির কারণ হবে। আরও জটিল ম্যাক্রোগুলোর একাধিক আর্ম থাকবে।
ম্যাক্রো সংজ্ঞায় বৈধ প্যাটার্ন সিনট্যাক্স চ্যাপ্টার ১৯-এ কভার করা প্যাটার্ন সিনট্যাক্স থেকে ভিন্ন কারণ ম্যাক্রো প্যাটার্নগুলো মানের পরিবর্তে রাস্ট কোড কাঠামোর সাথে ম্যাচ করা হয়। চলুন লিস্টিং ২০-২৯-এর প্যাটার্নের অংশগুলোর অর্থ কী তা দেখি; সম্পূর্ণ ম্যাক্রো প্যাটার্ন সিনট্যাক্সের জন্য, রাস্ট রেফারেন্স দেখুন।
প্রথমে আমরা পুরো প্যাটার্নটি ঘিরে রাখার জন্য একজোড়া বন্ধনী ব্যবহার করি। আমরা ম্যাক্রো সিস্টেমে একটি ভেরিয়েবল ঘোষণা করার জন্য একটি ডলার চিহ্ন ($
) ব্যবহার করি যা প্যাটার্নের সাথে ম্যাচ করা রাস্ট কোড ধারণ করবে। ডলার চিহ্নটি স্পষ্ট করে দেয় যে এটি একটি ম্যাক্রো ভেরিয়েবল, একটি সাধারণ রাস্ট ভেরিয়েবল নয়। এর পরে একজোড়া বন্ধনী আসে যা প্রতিস্থাপন কোডে ব্যবহারের জন্য বন্ধনীর মধ্যে থাকা প্যাটার্নের সাথে ম্যাচ করা মানগুলো ক্যাপচার করে। $()
-এর মধ্যে $x:expr
রয়েছে, যা যেকোনো রাস্ট এক্সপ্রেশনের সাথে ম্যাচ করে এবং এক্সপ্রেশনটিকে $x
নাম দেয়।
$()
-এর পরে থাকা কমাটি নির্দেশ করে যে $()
-এর মধ্যে থাকা কোডের সাথে ম্যাচ করা কোডের প্রতিটি ইনস্ট্যান্সের মধ্যে একটি আক্ষরিক কমা বিভাজক অক্ষর অবশ্যই উপস্থিত থাকতে হবে। *
নির্দিষ্ট করে যে প্যাটার্নটি *
-এর আগে যা কিছু আছে তার শূন্য বা তার বেশি সংখ্যার সাথে ম্যাচ করে।
যখন আমরা vec![1, 2, 3];
দিয়ে এই ম্যাক্রোটি কল করি, $x
প্যাটার্নটি তিনটি এক্সপ্রেশন 1
, 2
, এবং 3
-এর সাথে তিনবার ম্যাচ করে।
এখন আসুন এই আর্মের সাথে যুক্ত কোডের বডিতে প্যাটার্নটি দেখি: $()*
-এর মধ্যে temp_vec.push()
প্যাটার্নে $()
-এর সাথে ম্যাচ করা প্রতিটি অংশের জন্য তৈরি হয়, প্যাটার্নটি কতবার ম্যাচ করে তার উপর নির্ভর করে শূন্য বা তার বেশিবার। $x
প্রতিটি ম্যাচ করা এক্সপ্রেশন দিয়ে প্রতিস্থাপিত হয়। যখন আমরা vec![1, 2, 3];
দিয়ে এই ম্যাক্রোটি কল করি, তখন এই ম্যাক্রো কলটি প্রতিস্থাপনকারী কোডটি নিম্নলিখিত হবে:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
আমরা এমন একটি ম্যাক্রো সংজ্ঞায়িত করেছি যা যেকোনো সংখ্যক আর্গুমেন্ট যেকোনো টাইপের নিতে পারে এবং নির্দিষ্ট উপাদান ধারণকারী একটি ভেক্টর তৈরি করার জন্য কোড তৈরি করতে পারে।
ম্যাক্রো কীভাবে লিখতে হয় সে সম্পর্কে আরও জানতে, অনলাইন ডকুমেন্টেশন বা অন্যান্য রিসোর্স, যেমন ড্যানিয়েল কিপ দ্বারা শুরু করা এবং লুকাস ওয়ার্থ দ্বারা চালিত "The Little Book of Rust Macros" দেখুন।
অ্যাট্রিবিউট থেকে কোড জেনারেট করার জন্য প্রসিডিউরাল ম্যাক্রো
ম্যাক্রোর দ্বিতীয় রূপটি হলো প্রসিডিউরাল ম্যাক্রো, যা একটি ফাংশনের মতো কাজ করে (এবং এটি এক ধরণের প্রসিডিউর)। প্রসিডিউরাল ম্যাক্রো (Procedural macros) ইনপুট হিসাবে কিছু কোড গ্রহণ করে, সেই কোডের উপর কাজ করে এবং প্যাটার্নের সাথে ম্যাচ করে কোডকে অন্য কোড দিয়ে প্রতিস্থাপন করার পরিবর্তে আউটপুট হিসাবে কিছু কোড তৈরি করে, যেমনটা ডিক্লারেটিভ ম্যাক্রোগুলো করে। তিন ধরণের প্রসিডিউরাল ম্যাক্রো হলো কাস্টম derive
, অ্যাট্রিবিউট-লাইক এবং ফাংশন-লাইক, এবং সবগুলোই একই রকমভাবে কাজ করে।
প্রসিডিউরাল ম্যাক্রো তৈরি করার সময়, সংজ্ঞাগুলো অবশ্যই একটি বিশেষ ক্রেট টাইপ সহ তাদের নিজস্ব ক্রেটে থাকতে হবে। এটি জটিল প্রযুক্তিগত কারণে যা আমরা ভবিষ্যতে দূর করার আশা করি। লিস্টিং ২০-৩৬-এ, আমরা দেখাই কীভাবে একটি প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করতে হয়, যেখানে some_attribute
একটি নির্দিষ্ট ম্যাক্রো ভ্যারাইটি ব্যবহারের জন্য একটি প্লেসহোল্ডার।
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
যে ফাংশনটি একটি প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করে তা ইনপুট হিসাবে একটি TokenStream
নেয় এবং আউটপুট হিসাবে একটি TokenStream
তৈরি করে। TokenStream
টাইপটি proc_macro
ক্রেট দ্বারা সংজ্ঞায়িত করা হয়েছে যা রাস্টের সাথে অন্তর্ভুক্ত এবং টোকেনের একটি ক্রম প্রতিনিধিত্ব করে। এটি ম্যাক্রোর মূল: যে সোর্স কোডের উপর ম্যাক্রোটি কাজ করছে তা ইনপুট TokenStream
তৈরি করে, এবং ম্যাক্রো যে কোড তৈরি করে তা আউটপুট TokenStream
। ফাংশনটির সাথে একটি অ্যাট্রিবিউটও সংযুক্ত থাকে যা নির্দিষ্ট করে যে আমরা কোন ধরণের প্রসিডিউরাল ম্যাক্রো তৈরি করছি। আমরা একই ক্রেটে একাধিক ধরণের প্রসিডিউরাল ম্যাক্রো রাখতে পারি।
আসুন বিভিন্ন ধরণের প্রসিডিউরাল ম্যাক্রোগুলো দেখি। আমরা একটি কাস্টম derive
ম্যাক্রো দিয়ে শুরু করব এবং তারপর ছোটখাটো ভিন্নতাগুলো ব্যাখ্যা করব যা অন্যান্য রূপগুলোকে ভিন্ন করে তোলে।
কীভাবে একটি কাস্টম derive
ম্যাক্রো লিখবেন
আসুন hello_macro
নামে একটি ক্রেট তৈরি করি যা HelloMacro
নামে একটি trait সংজ্ঞায়িত করে যার একটি associated function hello_macro
আছে। আমাদের ব্যবহারকারীদের তাদের প্রতিটি টাইপের জন্য HelloMacro
trait ইমপ্লিমেন্ট করতে বাধ্য করার পরিবর্তে, আমরা একটি প্রসিডিউরাল ম্যাক্রো সরবরাহ করব যাতে ব্যবহারকারীরা #[derive(HelloMacro)]
দিয়ে তাদের টাইপকে অ্যানোটেট করে hello_macro
ফাংশনের একটি ডিফল্ট ইমপ্লিমেন্টেশন পেতে পারে। ডিফল্ট ইমপ্লিমেন্টেশনটি Hello, Macro! My name is TypeName!
প্রিন্ট করবে যেখানে TypeName
হলো সেই টাইপের নাম যার উপর এই trait-টি সংজ্ঞায়িত করা হয়েছে। অন্য কথায়, আমরা এমন একটি ক্রেট লিখব যা অন্য প্রোগ্রামারকে আমাদের ক্রেট ব্যবহার করে লিস্টিং ২০-৩৭-এর মতো কোড লিখতে সক্ষম করবে।
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
এই কোডটি যখন আমরা শেষ করব তখন Hello, Macro! My name is Pancakes!
প্রিন্ট করবে। প্রথম ধাপ হলো একটি নতুন লাইব্রেরি ক্রেট তৈরি করা, এইভাবে:
$ cargo new hello_macro --lib
এরপরে, লিস্টিং ২০-৩৮-এ, আমরা HelloMacro
trait এবং এর associated function সংজ্ঞায়িত করব।
pub trait HelloMacro {
fn hello_macro();
}
আমাদের একটি trait এবং তার ফাংশন আছে। এই মুহূর্তে, আমাদের ক্রেট ব্যবহারকারী কাঙ্ক্ষিত কার্যকারিতা অর্জনের জন্য trait-টি ইমপ্লিমেন্ট করতে পারে, যেমনটি লিস্টিং ২০-৩৯-এ দেখানো হয়েছে।
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
তবে, তাদের প্রতিটি টাইপের জন্য ইমপ্লিমেন্টেশন ব্লক লিখতে হতো যা তারা hello_macro
-এর সাথে ব্যবহার করতে চায়; আমরা তাদের এই কাজটি করা থেকে বাঁচাতে চাই।
উপরন্তু, আমরা এখনও hello_macro
ফাংশনটিকে ডিফল্ট ইমপ্লিমেন্টেশন দিয়ে সরবরাহ করতে পারি না যা trait-টি ইমপ্লিমেন্ট করা টাইপের নাম প্রিন্ট করবে: রাস্টের রিফ্লেকশন ক্ষমতা নেই, তাই এটি রানটাইমে টাইপের নাম দেখতে পারে না। আমাদের কম্পাইল টাইমে কোড জেনারেট করার জন্য একটি ম্যাক্রো প্রয়োজন।
পরবর্তী ধাপ হলো প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করা। এই লেখার সময়, প্রসিডিউরাল ম্যাক্রোগুলোকে তাদের নিজস্ব ক্রেটে থাকতে হবে। অবশেষে, এই সীমাবদ্ধতা তুলে নেওয়া হতে পারে। ক্রেট এবং ম্যাক্রো ক্রেট কাঠামো করার প্রথাটি নিম্নরূপ: foo
নামের একটি ক্রেটের জন্য, একটি কাস্টম derive
প্রসিডিউরাল ম্যাক্রো ক্রেটকে foo_derive
বলা হয়। চলুন আমাদের hello_macro
প্রজেক্টের ভিতরে hello_macro_derive
নামে একটি নতুন ক্রেট শুরু করি:
$ cargo new hello_macro_derive --lib
আমাদের দুটি ক্রেট ঘনিষ্ঠভাবে সম্পর্কিত, তাই আমরা আমাদের hello_macro
ক্রেটের ডিরেক্টরির মধ্যে প্রসিডিউরাল ম্যাক্রো ক্রেট তৈরি করি। যদি আমরা hello_macro
-তে trait সংজ্ঞা পরিবর্তন করি, আমাদের hello_macro_derive
-এ প্রসিডিউরাল ম্যাক্রোর ইমপ্লিমেন্টেশনও পরিবর্তন করতে হবে। দুটি ক্রেটকে আলাদাভাবে প্রকাশ করতে হবে, এবং এই ক্রেটগুলো ব্যবহারকারী প্রোগ্রামারদের উভয়কেই নির্ভরতা হিসাবে যোগ করতে হবে এবং উভয়কেই স্কোপে আনতে হবে। আমরা পরিবর্তে hello_macro
ক্রেটকে hello_macro_derive
-কে একটি নির্ভরতা হিসাবে ব্যবহার করতে এবং প্রসিডিউরাল ম্যাক্রো কোডটি পুনরায় এক্সপোর্ট করতে পারতাম। তবে, আমরা যেভাবে প্রজেক্টটি কাঠামো করেছি তা প্রোগ্রামারদের hello_macro
ব্যবহার করা সম্ভব করে তোলে এমনকি যদি তারা derive
কার্যকারিতা না চায়।
আমাদের hello_macro_derive
ক্রেটটিকে একটি প্রসিডিউরাল ম্যাক্রো ক্রেট হিসাবে ঘোষণা করতে হবে। আমাদের syn
এবং quote
ক্রেট থেকেও কার্যকারিতার প্রয়োজন হবে, যেমন আপনি এক মুহূর্তের মধ্যে দেখতে পাবেন, তাই আমাদের সেগুলোকে নির্ভরতা হিসাবে যোগ করতে হবে। hello_macro_derive
-এর জন্য Cargo.toml ফাইলে নিম্নলিখিতটি যোগ করুন:
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
প্রসিডিউরাল ম্যাক্রো সংজ্ঞায়িত করা শুরু করতে, লিস্টিং ২০-৪০-এর কোডটি আপনার hello_macro_derive
ক্রেটের জন্য src/lib.rs ফাইলে রাখুন। লক্ষ্য করুন যে এই কোডটি যতক্ষণ না আমরা impl_hello_macro
ফাংশনের জন্য একটি সংজ্ঞা যোগ করি ততক্ষণ কম্পাইল হবে না।
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate.
let ast = syn::parse(input).unwrap();
// Build the trait implementation.
impl_hello_macro(&ast)
}
লক্ষ্য করুন যে আমরা কোডটিকে hello_macro_derive
ফাংশনে বিভক্ত করেছি, যা TokenStream
পার্স করার জন্য দায়ী, এবং impl_hello_macro
ফাংশনে, যা সিনট্যাক্স ট্রি রূপান্তর করার জন্য দায়ী: এটি একটি প্রসিডিউরাল ম্যাক্রো লেখা আরও সুবিধাজনক করে তোলে। বাইরের ফাংশনের কোড (hello_macro_derive
এই ক্ষেত্রে) প্রায় প্রতিটি প্রসিডিউরাল ম্যাক্রো ক্রেটের জন্য একই হবে যা আপনি দেখেন বা তৈরি করেন। ভিতরের ফাংশনের বডিতে আপনি যে কোড নির্দিষ্ট করেন (impl_hello_macro
এই ক্ষেত্রে) আপনার প্রসিডিউরাল ম্যাক্রোর উদ্দেশ্যের উপর নির্ভর করে ভিন্ন হবে।
আমরা তিনটি নতুন ক্রেট চালু করেছি: proc_macro
, syn
, এবং quote
। proc_macro
ক্রেটটি রাস্টের সাথে আসে, তাই আমাদের সেটিকে Cargo.toml-এর নির্ভরতাগুলিতে যোগ করার প্রয়োজন ছিল না। proc_macro
ক্রেটটি হলো কম্পাইলারের API যা আমাদের আমাদের কোড থেকে রাস্ট কোড পড়তে এবং ম্যানিপুলেট করতে দেয়।
syn
ক্রেটটি একটি স্ট্রিং থেকে রাস্ট কোডকে একটি ডেটা স্ট্রাকচারে পার্স করে যার উপর আমরা অপারেশন করতে পারি। quote
ক্রেটটি syn
ডেটা স্ট্রাকচারগুলোকে আবার রাস্ট কোডে পরিণত করে। এই ক্রেটগুলো আমাদের যে কোনো ধরণের রাস্ট কোড পার্স করা অনেক সহজ করে তোলে যা আমরা হ্যান্ডেল করতে চাইতে পারি: রাস্ট কোডের জন্য একটি সম্পূর্ণ পার্সার লেখা কোনো সহজ কাজ নয়।
hello_macro_derive
ফাংশনটি তখন কল করা হবে যখন আমাদের লাইব্রেরির একজন ব্যবহারকারী একটি টাইপের উপর #[derive(HelloMacro)]
নির্দিষ্ট করবে। এটি সম্ভব কারণ আমরা এখানে hello_macro_derive
ফাংশনটিকে proc_macro_derive
দিয়ে অ্যানোটেট করেছি এবং HelloMacro
নামটি নির্দিষ্ট করেছি, যা আমাদের trait নামের সাথে মেলে; এটি বেশিরভাগ প্রসিডিউরাল ম্যাক্রোর অনুসরণ করা প্রথা।
hello_macro_derive
ফাংশনটি প্রথমে input
-কে একটি TokenStream
থেকে একটি ডেটা স্ট্রাকচারে রূপান্তর করে যা আমরা তখন ব্যাখ্যা করতে এবং অপারেশন করতে পারি। এখানেই syn
কাজে আসে। syn
-এর parse
ফাংশনটি একটি TokenStream
নেয় এবং পার্স করা রাস্ট কোড প্রতিনিধিত্বকারী একটি DeriveInput
struct রিটার্ন করে। লিস্টিং ২০-৪১ struct Pancakes;
স্ট্রিং পার্স করে আমরা যে DeriveInput
struct পাই তার প্রাসঙ্গিক অংশগুলো দেখায়।
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
এই struct-এর ফিল্ডগুলো দেখায় যে আমরা যে রাস্ট কোডটি পার্স করেছি তা ident
(identifier
, অর্থাৎ নাম) Pancakes
সহ একটি ইউনিট struct। এই struct-এ সব ধরণের রাস্ট কোড বর্ণনা করার জন্য আরও ফিল্ড রয়েছে; আরও তথ্যের জন্য syn
ডকুমেন্টেশনে DeriveInput
দেখুন।
শীঘ্রই আমরা impl_hello_macro
ফাংশনটি সংজ্ঞায়িত করব, যেখানে আমরা যে নতুন রাস্ট কোডটি অন্তর্ভুক্ত করতে চাই তা তৈরি করব। কিন্তু তার আগে, লক্ষ্য করুন যে আমাদের derive
ম্যাক্রোর আউটপুটও একটি TokenStream
। ফেরত দেওয়া TokenStream
-টি আমাদের ক্রেট ব্যবহারকারীদের লেখা কোডে যোগ করা হয়, তাই যখন তারা তাদের ক্রেট কম্পাইল করে, তারা পরিবর্তিত TokenStream
-এ আমাদের সরবরাহ করা অতিরিক্ত কার্যকারিতা পাবে।
আপনি হয়তো লক্ষ্য করেছেন যে আমরা unwrap
কল করছি যাতে syn::parse
ফাংশনে কল ব্যর্থ হলে hello_macro_derive
ফাংশনটি প্যানিক করে। আমাদের প্রসিডিউরাল ম্যাক্রোর ত্রুটির উপর প্যানিক করা প্রয়োজন কারণ proc_macro_derive
ফাংশনগুলোকে প্রসিডিউরাল ম্যাক্রো API-এর সাথে সামঞ্জস্যপূর্ণ হওয়ার জন্য Result
-এর পরিবর্তে TokenStream
রিটার্ন করতে হবে। আমরা unwrap
ব্যবহার করে এই উদাহরণটি সহজ করেছি; প্রোডাকশন কোডে, আপনার panic!
বা expect
ব্যবহার করে কী ভুল হয়েছে সে সম্পর্কে আরও নির্দিষ্ট ত্রুটির বার্তা সরবরাহ করা উচিত।
এখন যেহেতু আমাদের কাছে অ্যানোটেটেড রাস্ট কোডটিকে একটি TokenStream
থেকে একটি DeriveInput
ইনস্ট্যান্সে পরিণত করার কোড রয়েছে, আসুন অ্যানোটেটেড টাইপের উপর HelloMacro
trait ইমপ্লিমেন্ট করে এমন কোড জেনারেট করি, যেমনটি লিস্টিং ২০-৪২-এ দেখানো হয়েছে।
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
generated.into()
}
আমরা ast.ident
ব্যবহার করে অ্যানোটেটেড টাইপের নাম (আইডেন্টিফায়ার) ধারণকারী একটি Ident
struct ইনস্ট্যান্স পাই। লিস্টিং ২০-৪১-এর structটি দেখায় যে যখন আমরা লিস্টিং ২০-৩৭-এর কোডের উপর impl_hello_macro
ফাংশনটি চালাই, তখন আমরা যে ident
পাব তার ident
ফিল্ডে "Pancakes"
মান থাকবে। সুতরাং লিস্টিং ২০-৪২-এর name
ভেরিয়েবলে একটি Ident
struct ইনস্ট্যান্স থাকবে যা প্রিন্ট করলে "Pancakes"
স্ট্রিংটি হবে, যা লিস্টিং ২০-৩৭-এর struct-এর নাম।
quote!
ম্যাক্রো আমাদের সেই রাস্ট কোড সংজ্ঞায়িত করতে দেয় যা আমরা রিটার্ন করতে চাই। কম্পাইলার quote!
ম্যাক্রোর এক্সিকিউশনের সরাসরি ফলাফলের চেয়ে ভিন্ন কিছু আশা করে, তাই আমাদের এটিকে একটি TokenStream
-এ রূপান্তর করতে হবে। আমরা এটি into
মেথড কল করে করি, যা এই মধ্যবর্তী উপস্থাপনাটি গ্রহণ করে এবং প্রয়োজনীয় TokenStream
টাইপের একটি মান রিটার্ন করে।
quote!
ম্যাক্রো কিছু খুব চমৎকার টেমপ্লেটিং মেকানিক্সও সরবরাহ করে: আমরা #name
প্রবেশ করাতে পারি, এবং quote!
এটিকে name
ভেরিয়েবলের মান দিয়ে প্রতিস্থাপন করবে। আপনি এমনকি নিয়মিত ম্যাক্রোগুলো যেভাবে কাজ করে তার মতো কিছু পুনরাবৃত্তিও করতে পারেন। একটি পুঙ্খানুপুঙ্খ পরিচিতির জন্য quote
ক্রেটের ডক্স দেখুন।
আমরা চাই আমাদের প্রসিডিউরাল ম্যাক্রো ব্যবহারকারীর অ্যানোটেট করা টাইপের জন্য আমাদের HelloMacro
trait-এর একটি ইমপ্লিমেন্টেশন জেনারেট করুক, যা আমরা #name
ব্যবহার করে পেতে পারি। trait ইমপ্লিমেন্টেশনের একটি ফাংশন hello_macro
আছে, যার বডিতে আমরা যে কার্যকারিতা সরবরাহ করতে চাই তা রয়েছে: Hello, Macro! My name is
প্রিন্ট করা এবং তারপর অ্যানোটেটেড টাইপের নাম।
এখানে ব্যবহৃত stringify!
ম্যাক্রোটি রাস্টের মধ্যে বিল্ট-ইন। এটি একটি রাস্ট এক্সপ্রেশন নেয়, যেমন 1 + 2
, এবং কম্পাইল টাইমে এক্সপ্রেশনটিকে একটি স্ট্রিং লিটারাল, যেমন "1 + 2"
-তে পরিণত করে। এটি format!
বা println!
ম্যাক্রোগুলো থেকে ভিন্ন, যা এক্সপ্রেশনটি মূল্যায়ন করে এবং তারপর ফলাফলটিকে একটি String
-এ পরিণত করে। এমন একটি সম্ভাবনা রয়েছে যে #name
ইনপুটটি আক্ষরিকভাবে প্রিন্ট করার জন্য একটি এক্সপ্রেশন হতে পারে, তাই আমরা stringify!
ব্যবহার করি। stringify!
ব্যবহার করা কম্পাইল টাইমে #name
-কে একটি স্ট্রিং লিটারালে রূপান্তর করে একটি অ্যালোকেশনও বাঁচায়।
এই মুহূর্তে, cargo build
hello_macro
এবং hello_macro_derive
উভয় ক্ষেত্রেই সফলভাবে সম্পন্ন হওয়া উচিত। চলুন এই ক্রেটগুলোকে লিস্টিং ২০-৩৭-এর কোডের সাথে সংযুক্ত করে প্রসিডিউরাল ম্যাক্রোটি কার্যকর দেখি! আপনার projects ডিরেক্টরিতে cargo new pancakes
ব্যবহার করে একটি নতুন বাইনারি প্রজেক্ট তৈরি করুন। আমাদের pancakes
ক্রেটের Cargo.toml-এ hello_macro
এবং hello_macro_derive
-কে নির্ভরতা হিসেবে যোগ করতে হবে। যদি আপনি আপনার hello_macro
এবং hello_macro_derive
-এর সংস্করণ crates.io-তে প্রকাশ করেন, তবে সেগুলি নিয়মিত নির্ভরতা হবে; যদি না হয়, আপনি সেগুলোকে path
নির্ভরতা হিসেবে নির্দিষ্ট করতে পারেন নিম্নরূপ:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
src/main.rs-এ লিস্টিং ২০-৩৭-এর কোডটি রাখুন, এবং cargo run
চালান: এটি Hello, Macro! My name is Pancakes!
প্রিন্ট করা উচিত। pancakes
ক্রেটকে ইমপ্লিমেন্ট করার প্রয়োজন ছাড়াই প্রসিডিউরাল ম্যাক্রো থেকে HelloMacro
trait-এর ইমপ্লিমেন্টেশন অন্তর্ভুক্ত করা হয়েছিল; #[derive(HelloMacro)]
trait ইমপ্লিমেন্টেশনটি যোগ করেছে।
এরপরে, চলুন দেখি অন্যান্য ধরণের প্রসিডিউরাল ম্যাক্রোগুলো কাস্টম derive
ম্যাক্রো থেকে কীভাবে ভিন্ন।
অ্যাট্রিবিউট-লাইক ম্যাক্রো (Attribute-Like Macros)
অ্যাট্রিবিউট-লাইক ম্যাক্রোগুলো কাস্টম derive
ম্যাক্রোর মতোই, কিন্তু derive
অ্যাট্রিবিউটের জন্য কোড জেনারেট করার পরিবর্তে, তারা আপনাকে নতুন অ্যাট্রিবিউট তৈরি করার সুযোগ দেয়। এগুলি আরও নমনীয়: derive
শুধুমাত্র struct এবং enum-এর জন্য কাজ করে; অ্যাট্রিবিউটগুলো অন্যান্য আইটেম, যেমন ফাংশন-এর উপরও প্রয়োগ করা যেতে পারে। এখানে একটি অ্যাট্রিবিউট-লাইক ম্যাক্রো ব্যবহারের একটি উদাহরণ। ধরুন আপনার route
নামে একটি অ্যাট্রিবিউট আছে যা একটি ওয়েব অ্যাপ্লিকেশন ফ্রেমওয়ার্ক ব্যবহার করার সময় ফাংশনগুলোকে অ্যানোটেট করে:
#[route(GET, "/")]
fn index() {
এই #[route]
অ্যাট্রিবিউটটি ফ্রেমওয়ার্ক দ্বারা একটি প্রসিডিউরাল ম্যাক্রো হিসাবে সংজ্ঞায়িত করা হবে। ম্যাক্রো সংজ্ঞা ফাংশনের সিগনেচারটি এইরকম দেখাবে:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
এখানে, আমাদের TokenStream
টাইপের দুটি প্যারামিটার রয়েছে। প্রথমটি অ্যাট্রিবিউটের বিষয়বস্তুর জন্য: GET, "/"
অংশ। দ্বিতীয়টি হলো সেই আইটেমের বডি যার সাথে অ্যাট্রিবিউটটি সংযুক্ত: এই ক্ষেত্রে, fn index() {}
এবং ফাংশনের বডির বাকি অংশ।
এছাড়া, অ্যাট্রিবিউট-লাইক ম্যাক্রোগুলো কাস্টম derive
ম্যাক্রোর মতোই কাজ করে: আপনি proc-macro
ক্রেট টাইপ সহ একটি ক্রেট তৈরি করেন এবং আপনি যে কোড জেনারেট করতে চান তার জন্য একটি ফাংশন ইমপ্লিমেন্ট করেন!
ফাংশন-লাইক ম্যাক্রো (Function-Like Macros)
ফাংশন-লাইক ম্যাক্রোগুলো এমন ম্যাক্রো সংজ্ঞায়িত করে যা ফাংশন কলের মতো দেখায়। macro_rules!
ম্যাক্রোর মতোই, এগুলি ফাংশনের চেয়ে বেশি নমনীয়; উদাহরণস্বরূপ, তারা একটি অজানা সংখ্যক আর্গুমেন্ট নিতে পারে। তবে, macro_rules!
ম্যাক্রোগুলো শুধুমাত্র ম্যাচ-লাইক সিনট্যাক্স ব্যবহার করে সংজ্ঞায়িত করা যেতে পারে যা আমরা আগে “Declarative Macros with macro_rules!
for General Metaprogramming”-এ আলোচনা করেছি। ফাংশন-লাইক ম্যাক্রোগুলো একটি TokenStream
প্যারামিটার নেয়, এবং তাদের সংজ্ঞা সেই TokenStream
-কে রাস্ট কোড ব্যবহার করে ম্যানিপুলেট করে যেমনটি অন্য দুটি ধরণের প্রসিডিউরাল ম্যাক্রো করে। একটি ফাংশন-লাইক ম্যাক্রোর উদাহরণ হলো একটি sql!
ম্যাক্রো যা এইভাবে কল করা হতে পারে:
let sql = sql!(SELECT * FROM posts WHERE id=1);
এই ম্যাক্রোটি এর ভেতরের SQL স্টেটমেন্টটি পার্স করবে এবং এটি সিনট্যাক্টিক্যালি সঠিক কিনা তা পরীক্ষা করবে, যা একটি macro_rules!
ম্যাক্রো যা করতে পারে তার চেয়ে অনেক বেশি জটিল প্রক্রিয়াকরণ। sql!
ম্যাক্রোটি এইভাবে সংজ্ঞায়িত করা হবে:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
এই সংজ্ঞাটি কাস্টম derive
ম্যাক্রোর সিগনেচারের মতোই: আমরা বন্ধনীর ভেতরের টোকেনগুলো গ্রহণ করি এবং আমরা যে কোড জেনারেট করতে চেয়েছিলাম তা রিটার্ন করি।
সারাংশ (Summary)
এখন আপনার টুলবক্সে কিছু রাস্ট ফিচার রয়েছে যা আপনি সম্ভবত প্রায়শই ব্যবহার করবেন না, তবে আপনি জানবেন যে খুব নির্দিষ্ট পরিস্থিতিতে সেগুলি উপলব্ধ রয়েছে। আমরা বেশ কয়েকটি জটিল বিষয় পরিচয় করিয়ে দিয়েছি যাতে আপনি যখন ত্রুটির বার্তার পরামর্শে বা অন্য লোকের কোডে সেগুলির সম্মুখীন হন, তখন আপনি এই ধারণা এবং সিনট্যাক্সগুলি চিনতে পারবেন। এই অধ্যায়টি আপনাকে সমাধানের দিকে পরিচালিত করার জন্য একটি রেফারেন্স হিসাবে ব্যবহার করুন।
এরপরে, আমরা বই জুড়ে যা কিছু আলোচনা করেছি তা অনুশীলনে প্রয়োগ করব এবং আরও একটি প্রজেক্ট করব!
চূড়ান্ত প্রজেক্ট: একটি মাল্টিথ্রেডেড ওয়েব সার্ভার তৈরি
এটি একটি দীর্ঘ যাত্রা ছিল, কিন্তু আমরা বইয়ের একদম শেষে চলে এসেছি। এই অধ্যায়ে আমরা একসঙ্গে আরও একটি প্রজেক্ট তৈরি করব। এর মাধ্যমে আমরা শেষ কয়েকটি অধ্যায়ে শেখা কিছু ধারণা বাস্তবে প্রয়োগ করে দেখব এবং আগের কিছু পাঠ ঝালিয়ে নেব।
আমাদের চূড়ান্ত প্রজেক্ট হিসেবে, আমরা এমন একটি ওয়েব সার্ভার তৈরি করব যা ওয়েব ব্রাউজারে “hello” দেখাবে, অনেকটা চিত্র ২১-১ এর মতো।
ওয়েব সার্ভারটি তৈরির জন্য আমাদের পরিকল্পনা নিচে দেওয়া হলো:
- TCP এবং HTTP সম্পর্কে কিছুটা জানা।
- একটি socket-এ TCP connection-এর জন্য লিসেন করা।
- অল্প সংখ্যক HTTP request পার্স করা।
- একটি সঠিক HTTP response তৈরি করা।
- একটি thread pool ব্যবহার করে আমাদের server-এর throughput উন্নত করা।
চিত্র ২১-১: আমাদের চূড়ান্ত যৌথ প্রজেক্ট
শুরু করার আগে, দুটি বিষয় উল্লেখ করা জরুরি। প্রথমত, আমরা এখানে যে পদ্ধতিটি ব্যবহার করব, সেটি Rust দিয়ে ওয়েব সার্ভার তৈরি করার সেরা উপায় নয়। কমিউনিটির সদস্যরা crates.io তে এমন অনেক production-ready crate প্রকাশ করেছেন, যেগুলোতে আমাদের তৈরির চেয়েও অনেক বেশি পূর্ণাঙ্গ ওয়েব সার্ভার এবং থ্রেড পুল ইমপ্লিমেন্টেশন রয়েছে। তবে, এই অধ্যায়ে আমাদের উদ্দেশ্য হলো আপনাকে শেখানো, সহজ রাস্তা বেছে নেওয়া নয়। যেহেতু Rust একটি সিস্টেমস প্রোগ্রামিং ল্যাঙ্গুয়েজ, তাই আমরা কোন লেভেলের অ্যাবস্ট্র্যাকশনে কাজ করতে চাই তা বেছে নিতে পারি এবং অন্য অনেক ল্যাঙ্গুয়েজের তুলনায় নিম্ন স্তরে যেতে পারি, যা অন্য ল্যাঙ্গুয়েজে সম্ভব বা বাস্তবসম্মত নয়।
দ্বিতীয়ত, আমরা এখানে async
এবং await
ব্যবহার করব না। একটি async runtime
তৈরির মতো জটিলতা যুক্ত না করেও, একটি thread pool
তৈরি করাই যথেষ্ট বড় একটি চ্যালেঞ্জ। তবে, এই অধ্যায়ে আমরা যে সমস্যাগুলো দেখব, তার কয়েকটির সমাধানে async
এবং await
কীভাবে ব্যবহার করা যেতে পারে, সে সম্পর্কে আমরা আলোকপাত করব। শেষ পর্যন্ত, যেমনটা আমরা ১৭তম অধ্যায়ে দেখেছিলাম, অনেক async runtime
তাদের কাজ পরিচালনার জন্য thread pool
ব্যবহার করে।
এজন্য আমরা বেসিক HTTP server এবং thread pool ম্যানুয়ালি লিখব, যাতে আপনি ভবিষ্যতে ব্যবহার করতে পারেন এমন crate গুলোর পেছনের সাধারণ ধারণা এবং কৌশলগুলো শিখতে পারেন।
একটি সিঙ্গেল-থ্রেডেড ওয়েব সার্ভার তৈরি
আমরা একটি সিঙ্গেল-থ্রেডেড ওয়েব সার্ভার চালু করার মাধ্যমে কাজ শুরু করব। তবে তার আগে, ওয়েব সার্ভার তৈরির সাথে জড়িত প্রোটোকলগুলো সম্পর্কে সংক্ষেপে জেনে নেওয়া যাক। এই প্রোটোকলগুলোর বিস্তারিত বিবরণ এই বইয়ের আওতার বাইরে, কিন্তু একটি সংক্ষিপ্ত ধারণা আপনাকে প্রয়োজনীয় তথ্য দেবে।
ওয়েব সার্ভারের সাথে প্রধানত দুটি প্রোটোকল জড়িত: Hypertext Transfer Protocol (HTTP) এবং Transmission Control Protocol (TCP)। উভয় প্রোটোকলই request-response প্রোটোকল, যার অর্থ হলো একটি client রিকোয়েস্ট পাঠায় এবং একটি server সেই রিকোয়েস্ট শোনে এবং client-কে একটি রেসপন্স প্রদান করে। এই রিকোয়েস্ট এবং রেসপন্সগুলোর বিষয়বস্তু প্রোটোকল দ্বারা সংজ্ঞায়িত করা হয়।
TCP হলো একটি নিম্ন-স্তরের প্রোটোকল যা বর্ণনা করে কীভাবে তথ্য এক সার্ভার থেকে অন্য সার্ভারে যায়, কিন্তু সেই তথ্যটি কী, তা নির্দিষ্ট করে না। HTTP, TCP-এর উপরে তৈরি করা হয়েছে এবং এটি রিকোয়েস্ট ও রেসপন্সের বিষয়বস্তু নির্ধারণ করে। প্রযুক্তিগতভাবে HTTP অন্য প্রোটোকলের সাথেও ব্যবহার করা সম্ভব, তবে বেশিরভাগ ক্ষেত্রেই HTTP তার ডেটা TCP-এর মাধ্যমে পাঠায়। আমরা TCP এবং HTTP রিকোয়েস্ট ও রেসপন্সের raw বাইট নিয়ে কাজ করব।
TCP কানেকশনে লিসেন করা
আমাদের ওয়েব সার্ভারকে একটি TCP connection শোনার প্রয়োজন, তাই আমরা প্রথমে এই অংশটি নিয়ে কাজ করব। স্ট্যান্ডার্ড লাইব্রেরির std::net
মডিউলটি আমাদের এই কাজটি করতে সাহায্য করে। চলুন, স্বাভাবিক পদ্ধতিতে একটি নতুন প্রজেক্ট তৈরি করি:
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
এখন src/main.rs ফাইলে লিস্টিং ২১-১ এর কোডটি লিখুন। এই কোডটি স্থানীয় 127.0.0.1:7878
অ্যাড্রেসে আসা TCP stream-এর জন্য লিসেন করবে। যখন এটি একটি ইনকামিং স্ট্রিম পাবে, তখন Connection established!
প্রিন্ট করবে।
use std::net::TcpListener; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println!("Connection established!"); } }
TcpListener
ব্যবহার করে আমরা 127.0.0.1:7878
অ্যাড্রেসে TCP connection-এর জন্য লিসেন করতে পারি। এই অ্যাড্রেসে, কোলনের আগের অংশটি হলো আপনার কম্পিউটারকে প্রতিনিধিত্বকারী একটি IP address (এটি প্রতিটি কম্পিউটারে একই এবং নির্দিষ্টভাবে লেখকের কম্পিউটারকে বোঝায় না), এবং 7878
হলো পোর্ট। আমরা এই পোর্টটি দুটি কারণে বেছে নিয়েছি: সাধারণত এই পোর্টে HTTP গ্রহণ করা হয় না, তাই আমাদের সার্ভারটি আপনার মেশিনে চলমান অন্য কোনো ওয়েব সার্ভারের সাথে冲突 করার সম্ভাবনা কম, এবং 7878 একটি টেলিফোনে rust টাইপ করলে যা হয়, তাই।
এক্ষেত্রে bind
ফাংশনটি new
ফাংশনের মতোই কাজ করে, কারণ এটি একটি নতুন TcpListener
ইনস্ট্যান্স রিটার্ন করে। ফাংশনটিকে bind
বলা হয় কারণ নেটওয়ার্কিং-এর পরিভাষায়, শোনার জন্য একটি পোর্টের সাথে সংযোগ করাকে "বাইন্ডিং টু এ পোর্ট" বলা হয়।
bind
ফাংশনটি একটি Result<T, E>
রিটার্ন করে, যা নির্দেশ করে যে বাইন্ডিং ব্যর্থ হতে পারে। উদাহরণস্বরূপ, যদি আমরা আমাদের প্রোগ্রামের দুটি ইনস্ট্যান্স চালাই এবং দুটি প্রোগ্রাম একই পোর্টে লিসেন করে। যেহেতু আমরা শুধুমাত্র শেখার উদ্দেশ্যে একটি বেসিক সার্ভার লিখছি, তাই আমরা এই ধরনের এরর হ্যান্ডলিং নিয়ে চিন্তা করব না; এর পরিবর্তে, যদি কোনো এরর ঘটে, তাহলে প্রোগ্রাম বন্ধ করতে আমরা unwrap
ব্যবহার করব।
TcpListener
-এর incoming
মেথড একটি iterator রিটার্ন করে যা আমাদের একাধিক stream দেয় (আরও নির্দিষ্টভাবে বললে, TcpStream
টাইপের স্ট্রিম)। একটি সিঙ্গেল stream ক্লায়েন্ট এবং সার্ভারের মধ্যে একটি খোলা সংযোগের প্রতিনিধিত্ব করে। একটি connection হলো সম্পূর্ণ রিকোয়েস্ট এবং রেসপন্স প্রক্রিয়ার নাম, যেখানে একটি ক্লায়েন্ট সার্ভারের সাথে সংযোগ করে, সার্ভার একটি রেসপন্স তৈরি করে এবং সংযোগটি বন্ধ করে দেয়। সুতরাং, ক্লায়েন্ট কী পাঠিয়েছে তা দেখার জন্য আমরা TcpStream
থেকে ডেটা রিড করব এবং ক্লায়েন্টকে ডেটা ফেরত পাঠানোর জন্য আমাদের রেসপন্সটি স্ট্রিমে লিখব। মোটের উপর, এই for
লুপটি প্রতিটি কানেকশন একে একে প্রসেস করবে এবং আমাদের হ্যান্ডেল করার জন্য একাধিক স্ট্রিম তৈরি করবে।
আপাতত, স্ট্রিম হ্যান্ডেল করার জন্য আমরা unwrap
কল করছি, যাতে স্ট্রিমের কোনো এরর থাকলে আমাদের প্রোগ্রাম বন্ধ হয়ে যায়; যদি কোনো এরর না থাকে, প্রোগ্রাম একটি বার্তা প্রিন্ট করে। পরবর্তী লিস্টিংয়ে আমরা সফলতার ক্ষেত্রে আরও কার্যকারিতা যোগ করব। incoming
মেথড থেকে এরর পাওয়ার কারণ হলো, আমরা আসলে কানেকশনগুলোর উপর ইটারেট করছি না, বরং কানেকশন প্রচেষ্টার উপর ইটারেট করছি। কানেকশন বিভিন্ন কারণে সফল নাও হতে পারে, যার মধ্যে অনেকগুলোই অপারেটিং সিস্টেম-নির্দিষ্ট। উদাহরণস্বরূপ, অনেক অপারেটিং সিস্টেমে একই সাথে খোলা কানেকশনের সংখ্যার একটি সীমা থাকে; সেই সংখ্যার বাইরে নতুন কানেকশন প্রচেষ্টা করলে কিছু খোলা কানেকশন বন্ধ না হওয়া পর্যন্ত এরর দেখা দেবে।
চলুন এই কোডটি রান করি! টার্মিনালে cargo run
কমান্ডটি চালান এবং তারপর একটি ওয়েব ব্রাউজারে 127.0.0.1:7878 লোড করুন। ব্রাউজারে "Connection reset"-এর মতো একটি এরর মেসেজ দেখানো উচিত, কারণ সার্ভারটি বর্তমানে কোনো ডেটা ফেরত পাঠাচ্ছে না। কিন্তু আপনি যখন আপনার টার্মিনাল দেখবেন, তখন ব্রাউজার সার্ভারের সাথে সংযোগ করার সময় প্রিন্ট হওয়া বেশ কয়েকটি বার্তা দেখতে পাবেন!
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
কখনও কখনও আপনি একটি ব্রাউজার রিকোয়েস্টের জন্য একাধিক প্রিন্ট করা বার্তা দেখতে পাবেন; এর কারণ হতে পারে ব্রাউজারটি পেজের জন্য একটি রিকোয়েস্ট করছে এবং সেই সাথে অন্যান্য রিসোর্সের জন্যও রিকোয়েস্ট করছে, যেমন ব্রাউজার ট্যাবে প্রদর্শিত favicon.ico আইকন।
এমনও হতে পারে যে সার্ভারটি কোনো ডেটা দিয়ে রেসপন্স না করায় ব্রাউজার একাধিকবার সার্ভারের সাথে সংযোগ করার চেষ্টা করছে। যখন লুপের শেষে stream
স্কোপের বাইরে চলে যায় এবং ড্রপ হয়ে যায়, তখন drop
ইমপ্লিমেন্টেশনের অংশ হিসেবে সংযোগটি বন্ধ হয়ে যায়। ব্রাউজারগুলো কখনও কখনও বন্ধ হয়ে যাওয়া সংযোগ পুনরায় চেষ্টা করে মোকাবেলা করে, কারণ সমস্যাটি অস্থায়ী হতে পারে।
ব্রাউজারগুলো কোনো রিকোয়েস্ট না পাঠিয়েও সার্ভারে একাধিক কানেকশন খুলতে পারে, যাতে পরে যদি তারা রিকোয়েস্ট পাঠায়, তবে সেই রিকোয়েস্টগুলো দ্রুত সম্পন্ন হতে পারে। যখন এমনটা হয়, আমাদের সার্ভার প্রতিটি কানেকশন দেখতে পায়, সেই কানেকশনে কোনো রিকোয়েস্ট থাকুক বা না থাকুক। উদাহরণস্বরূপ, ক্রোম-ভিত্তিক ব্রাউজারগুলোর অনেক সংস্করণেই এমনটা করে থাকে; আপনি প্রাইভেট ব্রাউজিং মোড ব্যবহার করে বা অন্য কোনো ব্রাউজার ব্যবহার করে এই অপটিমাইজেশনটি নিষ্ক্রিয় করতে পারেন।
গুরুত্বপূর্ণ বিষয় হলো আমরা সফলভাবে একটি TCP connection-এর একটি হ্যান্ডেল পেয়েছি!
মনে রাখবেন, কোডের একটি নির্দিষ্ট সংস্করণ চালানো শেষ হলে ctrl-C চেপে প্রোগ্রামটি বন্ধ করতে হবে। এরপর কোডে প্রতিটি পরিবর্তনের পর cargo run
কমান্ড দিয়ে প্রোগ্রামটি পুনরায় চালু করুন, যাতে আপনি নতুন কোডটি চালাচ্ছেন তা নিশ্চিত হয়।
রিকোয়েস্ট পড়া
চলুন, ব্রাউজার থেকে রিকোয়েস্ট পড়ার কার্যকারিতা ইমপ্লিমেন্ট করি! প্রথমে একটি কানেকশন পাওয়া এবং তারপর সেই কানেকশন নিয়ে কোনো কাজ করার উদ্বেগগুলো আলাদা করতে, আমরা কানেকশন প্রসেস করার জন্য একটি নতুন ফাংশন শুরু করব। এই নতুন handle_connection
ফাংশনে, আমরা TCP স্ট্রিম থেকে ডেটা পড়ব এবং এটি প্রিন্ট করব যাতে আমরা ব্রাউজার থেকে পাঠানো ডেটা দেখতে পারি। কোডটি পরিবর্তন করে লিস্টিং ২১-২ এর মতো করুন।
use std::{ io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); println!("Request: {http_request:#?}"); }
আমরা std::io::prelude
এবং std::io::BufReader
স্কোপের মধ্যে নিয়ে এসেছি যাতে আমরা স্ট্রিম থেকে ডেটা পড়া এবং লেখার জন্য প্রয়োজনীয় trait এবং type-গুলিতে অ্যাক্সেস পেতে পারি। main
ফাংশনের for
লুপে, আমরা কানেকশন তৈরির বার্তা প্রিন্ট করার পরিবর্তে, এখন নতুন handle_connection
ফাংশনটি কল করি এবং এতে stream
পাস করি।
handle_connection
ফাংশনে, আমরা একটি নতুন BufReader
ইনস্ট্যান্স তৈরি করি যা stream
-এর একটি রেফারেন্সকে wrap করে। BufReader
আমাদের জন্য std::io::Read
trait মেথড কলগুলো পরিচালনা করে বাফারিং যুক্ত করে।
আমরা http_request
নামে একটি ভেরিয়েবল তৈরি করেছি ব্রাউজার থেকে আমাদের সার্ভারে পাঠানো রিকোয়েস্টের লাইনগুলো সংগ্রহ করার জন্য। আমরা Vec<_>
টাইপ অ্যানোটেশন যোগ করে নির্দেশ করছি যে আমরা এই লাইনগুলো একটি ভেক্টরে সংগ্রহ করতে চাই।
BufReader
, std::io::BufRead
ট্রেইটটি ইমপ্লিমেন্ট করে, যা lines
মেথড প্রদান করে। lines
মেথডটি ডেটা স্ট্রিমকে যখনই একটি নিউলাইন বাইট দেখে, তখনই বিভক্ত করে Result<String, std::io::Error>
এর একটি ইটারেটর রিটার্ন করে। প্রতিটি String
পাওয়ার জন্য, আমরা প্রতিটি Result
কে map
এবং unwrap
করি। যদি ডেটা বৈধ UTF-8 না হয় বা স্ট্রিম থেকে পড়তে কোনো সমস্যা হয়, তাহলে Result
একটি এরর হতে পারে। আবারও বলছি, একটি প্রোডাকশন-লেভেলের প্রোগ্রামে এই এররগুলো আরও ভালোভাবে পরিচালনা করা উচিত, কিন্তু আমরা সরলতার জন্য এরর হলে প্রোগ্রামটি বন্ধ করে দিচ্ছি।
ব্রাউজার একটি HTTP রিকোয়েস্টের শেষ বোঝাতে পরপর দুটি নিউলাইন ক্যারেক্টার পাঠায়, তাই স্ট্রিম থেকে একটি রিকোয়েস্ট পেতে, আমরা লাইন নিতে থাকি যতক্ষণ না একটি খালি স্ট্রিংয়ের লাইন পাই। একবার ভেক্টরে লাইনগুলো সংগ্রহ করার পরে, আমরা সেগুলোকে প্রিটি ডিবাগ ফরম্যাটিং ব্যবহার করে প্রিন্ট করছি যাতে ওয়েব ব্রাউজার আমাদের সার্ভারকে কী নির্দেশ পাঠাচ্ছে তা দেখতে পারি।
চলুন এই কোডটি চেষ্টা করি! প্রোগ্রামটি শুরু করুন এবং একটি ওয়েব ব্রাউজারে আবার একটি রিকোয়েস্ট করুন। মনে রাখবেন যে আমরা এখনও ব্রাউজারে একটি এরর পেজ পাবো, কিন্তু টার্মিনালে আমাদের প্রোগ্রামের আউটপুট এখন এইরকম দেখাবে:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
আপনার ব্রাউজারের উপর নির্ভর করে, আপনি কিছুটা ভিন্ন আউটপুট পেতে পারেন। এখন যেহেতু আমরা রিকোয়েস্ট ডেটা প্রিন্ট করছি, আমরা রিকোয়েস্টের প্রথম লাইনে GET
এর পরে পাথটি দেখে বুঝতে পারি কেন আমরা একটি ব্রাউজার রিকোয়েস্ট থেকে একাধিক কানেকশন পাই। যদি বারবার আসা কানেকশনগুলো সবই / রিকোয়েস্ট করে, আমরা জানি ব্রাউজারটি বারবার / আনার চেষ্টা করছে কারণ এটি আমাদের প্রোগ্রাম থেকে কোনো রেসপন্স পাচ্ছে না।
চলুন ব্রাউজার আমাদের প্রোগ্রাম থেকে কী চাইছে তা বুঝতে এই রিকোয়েস্ট ডেটাটি ভেঙে দেখি।
একটি HTTP রিকোয়েস্টের দিকে আরেকটু গভীর দৃষ্টি
HTTP একটি টেক্সট-ভিত্তিক প্রোটোকল, এবং একটি রিকোয়েস্ট এই ফরম্যাটে থাকে:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
প্রথম লাইনটি হলো request line যা ক্লায়েন্ট কী রিকোয়েস্ট করছে সে সম্পর্কে তথ্য ধারণ করে। রিকোয়েস্ট লাইনের প্রথম অংশটি ব্যবহৃত method নির্দেশ করে, যেমন GET
বা POST
, যা বর্ণনা করে ক্লায়েন্ট কীভাবে এই রিকোয়েস্টটি করছে। আমাদের ক্লায়েন্ট একটি GET
রিকোয়েস্ট ব্যবহার করেছে, যার মানে এটি তথ্য চাইছে।
রিকোয়েস্ট লাইনের পরবর্তী অংশ হলো / , যা ক্লায়েন্ট কোন uniform resource identifier (URI) রিকোয়েস্ট করছে তা নির্দেশ করে: একটি URI প্রায়, কিন্তু ঠিক পুরোপুরি, একটি uniform resource locator (URL) এর মতো নয়। এই অধ্যায়ে আমাদের উদ্দেশ্যে URI এবং URL-এর মধ্যে পার্থক্য গুরুত্বপূর্ণ নয়, তবে HTTP স্পেসিফিকেশন URI শব্দটি ব্যবহার করে, তাই আমরা এখানে মানসিকভাবে URI-এর জন্য URL প্রতিস্থাপন করতে পারি।
শেষ অংশটি হলো ক্লায়েন্টের ব্যবহৃত HTTP ভার্সন, এবং তারপর রিকোয়েস্ট লাইনটি একটি CRLF সিকোয়েন্সে শেষ হয়। (CRLF মানে হলো carriage return এবং line feed, যা টাইপরাইটারের দিনের পরিভাষা!) CRLF সিকোয়েন্সটিকে \r\n
হিসেবেও লেখা যেতে পারে, যেখানে \r
একটি carriage return এবং \n
একটি line feed। CRLF sequence রিকোয়েস্ট লাইনটিকে বাকি রিকোয়েস্ট ডেটা থেকে আলাদা করে। লক্ষ্য করুন যে যখন CRLF প্রিন্ট হয়, তখন আমরা \r\n
না দেখে একটি নতুন লাইন শুরু হতে দেখি।
আমাদের প্রোগ্রাম চালিয়ে প্রাপ্ত রিকোয়েস্ট লাইন ডেটা দেখলে, আমরা দেখি যে GET
হলো মেথড, / হলো রিকোয়েস্ট URI, এবং HTTP/1.1
হলো ভার্সন।
রিকোয়েস্ট লাইনের পরে, Host:
থেকে শুরু করে বাকি লাইনগুলো হলো হেডার। GET
রিকোয়েস্টে কোনো বডি থাকে না।
অন্য একটি ব্রাউজার থেকে রিকোয়েস্ট করার চেষ্টা করুন বা একটি ভিন্ন অ্যাড্রেস, যেমন 127.0.0.1:7878/test চেয়ে দেখুন, রিকোয়েস্ট ডেটা কীভাবে পরিবর্তিত হয়।
এখন যেহেতু আমরা জানি ব্রাউজার কী চাইছে, চলুন কিছু ডেটা ফেরত পাঠাই!
একটি রেসপন্স লেখা
আমরা এখন ক্লায়েন্টের রিকোয়েস্টের জবাবে ডেটা পাঠানোর কার্যকারিতা ইমপ্লিমেন্ট করব। রেসপন্সগুলোর ফরম্যাট নিচে দেওয়া হলো:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
প্রথম লাইনটি একটি status line যা রেসপন্সে ব্যবহৃত HTTP ভার্সন, একটি সংখ্যাসূচক স্ট্যাটাস কোড যা রিকোয়েস্টের ফলাফল সংক্ষিপ্তভাবে জানায়, এবং একটি কারণ-বাক্যাংশ যা স্ট্যাটাস কোডের একটি টেক্সট বর্ণনা প্রদান করে। CRLF সিকোয়েন্সের পরে যেকোনো হেডার, আরেকটি CRLF সিকোয়েন্স এবং রেসপন্সের বডি থাকে।
এখানে একটি উদাহরণ রেসপন্স রয়েছে যা HTTP ভার্সন 1.1 ব্যবহার করে, যার স্ট্যাটাস কোড 200, একটি OK কারণ-বাক্যাংশ, কোনো হেডার নেই এবং কোনো বডি নেই:
HTTP/1.1 200 OK\r\n\r\n
স্ট্যাটাস কোড 200 হলো স্ট্যান্ডার্ড সফল রেসপন্স। টেক্সটটি একটি ক্ষুদ্র সফল HTTP রেসপন্স। চলুন, একটি সফল রিকোয়েস্টের জবাবে এটিকে স্ট্রিমে লিখি! handle_connection
ফাংশন থেকে println!
যা রিকোয়েস্ট ডেটা প্রিন্ট করছিল তা সরিয়ে দিন এবং লিস্টিং ২১-৩ এর কোড দিয়ে প্রতিস্থাপন করুন।
use std::{ io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); }
প্রথম নতুন লাইনটি response
ভেরিয়েবলকে সংজ্ঞায়িত করে যা সফল বার্তার ডেটা ধারণ করে। তারপর আমরা আমাদের response
-এর উপর as_bytes
কল করে স্ট্রিং ডেটাকে বাইটে রূপান্তর করি। stream
-এর write_all
মেথডটি একটি &[u8]
নেয় এবং সেই বাইটগুলো সরাসরি কানেকশনে পাঠিয়ে দেয়। যেহেতু write_all
অপারেশন ব্যর্থ হতে পারে, তাই আমরা আগের মতোই যেকোনো এরর ফলাফলের উপর unwrap
ব্যবহার করি। আবারও, একটি বাস্তব অ্যাপ্লিকেশনে আপনার এখানে এরর হ্যান্ডলিং যোগ করা উচিত।
এই পরিবর্তনগুলোর সাথে, চলুন আমাদের কোড চালাই এবং একটি রিকোয়েস্ট করি। আমরা আর টার্মিনালে কোনো ডেটা প্রিন্ট করছি না, তাই আমরা কার্গোর আউটপুট ছাড়া আর কিছুই দেখতে পাব না। যখন আপনি একটি ওয়েব ব্রাউজারে 127.0.0.1:7878 লোড করবেন, তখন আপনি একটি এররের পরিবর্তে একটি খালি পেজ দেখতে পাবেন। আপনি এইমাত্র হাতে-কলমে একটি HTTP রিকোয়েস্ট গ্রহণ এবং একটি রেসপন্স পাঠানো কোড করেছেন!
আসল HTML ফেরত পাঠানো
চলুন একটি খালি পেজের চেয়ে বেশি কিছু ফেরত পাঠানোর কার্যকারিতা ইমপ্লিমেন্ট করি। আপনার প্রজেক্ট ডিরেক্টরির রুটে একটি নতুন ফাইল hello.html তৈরি করুন, src ডিরেক্টরিতে নয়। আপনি যেকোনো HTML ইনপুট করতে পারেন; লিস্টিং ২১-৪ একটি সম্ভাব্য উদাহরণ দেখায়।
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
এটি একটি শিরোনাম এবং কিছু টেক্সটসহ একটি ন্যূনতম HTML5 ডকুমেন্ট। একটি রিকোয়েস্ট পেলে সার্ভার থেকে এটি ফেরত পাঠানোর জন্য, আমরা লিস্টিং ২১-৫ এ দেখানো handle_connection
পরিবর্তন করব যাতে এটি HTML ফাইলটি পড়ে, এটিকে একটি বডি হিসাবে রেসপন্সে যোগ করে এবং পাঠায়।
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; // --snip-- fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); }
আমরা use
স্টেটমেন্টে fs
যোগ করেছি যাতে স্ট্যান্ডার্ড লাইব্রেরির ফাইল সিস্টেম মডিউলটি স্কোপে আসে। একটি ফাইলের বিষয়বস্তু একটি স্ট্রিংয়ে পড়ার কোডটি পরিচিত মনে হওয়া উচিত; আমরা আমাদের I/O প্রজেক্টে লিস্টিং ১২-৪ এ একটি ফাইলের বিষয়বস্তু পড়ার সময় এটি ব্যবহার করেছি।
এরপরে, আমরা ফাইলের বিষয়বস্তু সফল রেসপন্সের বডি হিসেবে যোগ করতে format!
ব্যবহার করি। একটি বৈধ HTTP রেসপন্স নিশ্চিত করার জন্য, আমরা Content-Length
হেডার যোগ করি যা আমাদের রেসপন্স বডির আকারের সমান সেট করা হয়, এক্ষেত্রে hello.html
-এর আকারের সমান।
cargo run
দিয়ে এই কোডটি চালান এবং আপনার ব্রাউজারে 127.0.0.1:7878 লোড করুন; আপনার HTML রেন্ডার হওয়া উচিত!
বর্তমানে, আমরা http_request
-এর রিকোয়েস্ট ডেটা উপেক্ষা করছি এবং শর্তহীনভাবে HTML ফাইলের বিষয়বস্তু ফেরত পাঠাচ্ছি। এর মানে হলো যদি আপনি আপনার ব্রাউজারে 127.0.0.1:7878/something-else রিকোয়েস্ট করার চেষ্টা করেন, তাহলেও আপনি এই একই HTML রেসপন্স ফিরে পাবেন। এই মুহূর্তে, আমাদের সার্ভারটি খুব সীমিত এবং বেশিরভাগ ওয়েব সার্ভার যা করে তা করে না। আমরা রিকোয়েস্টের উপর নির্ভর করে আমাদের রেসপন্স কাস্টমাইজ করতে চাই এবং শুধুমাত্র / এর জন্য একটি সুগঠিত রিকোয়েস্টের জন্য HTML ফাইলটি ফেরত পাঠাতে চাই।
রিকোয়েস্ট যাচাই এবং বেছে বেছে রেসপন্স করা
এখন, আমাদের ওয়েব সার্ভার ক্লায়েন্টের রিকোয়েস্ট যাই হোক না কেন, ফাইলের HTML ফেরত দেবে। চলুন, ব্রাউজার / রিকোয়েস্ট করছে কিনা তা পরীক্ষা করার জন্য কার্যকারিতা যোগ করি এবং যদি ব্রাউজার অন্য কিছু রিকোয়েস্ট করে তবে একটি এরর ফেরত দিই। এর জন্য আমাদের handle_connection
পরিবর্তন করতে হবে, যেমনটি লিস্টিং ২১-৬ এ দেখানো হয়েছে। এই নতুন কোডটি প্রাপ্ত রিকোয়েস্টের বিষয়বস্তু / রিকোয়েস্টের সাথে তুলনা করে এবং রিকোয়েস্টগুলো ভিন্নভাবে ট্রিট করার জন্য if
এবং else
ব্লক যোগ করে।
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } // --snip-- fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); if request_line == "GET / HTTP/1.1" { let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); } else { // some other request } }
আমরা শুধুমাত্র HTTP রিকোয়েস্টের প্রথম লাইনটি দেখব, তাই পুরো রিকোয়েস্টটি একটি ভেক্টরে পড়ার পরিবর্তে, আমরা ইটারেটর থেকে প্রথম আইটেমটি পেতে next
কল করছি। প্রথম unwrap
টি Option
-এর যত্ন নেয় এবং ইটারেটরে কোনো আইটেম না থাকলে প্রোগ্রামটি বন্ধ করে দেয়। দ্বিতীয় unwrap
টি Result
হ্যান্ডেল করে এবং লিস্টিং ২১-২-এ যোগ করা map
-এর unwrap
-এর মতোই কাজ করে।
এরপরে, আমরা request_line
পরীক্ষা করি যে এটি / পাথের একটি GET রিকোয়েস্টের রিকোয়েস্ট লাইনের সমান কিনা। যদি তাই হয়, if
ব্লকটি আমাদের HTML ফাইলের বিষয়বস্তু ফেরত দেয়।
যদি request_line
টি / পাথের GET রিকোয়েস্টের সমান না হয়, তার মানে আমরা অন্য কোনো রিকোয়েস্ট পেয়েছি। আমরা কিছুক্ষণের মধ্যে else
ব্লকে কোড যোগ করব অন্যান্য সব রিকোয়েস্টের জবাব দেওয়ার জন্য।
এখন এই কোডটি চালান এবং 127.0.0.1:7878 রিকোয়েস্ট করুন; আপনার hello.html-এর HTML পাওয়া উচিত। আপনি যদি অন্য কোনো রিকোয়েস্ট করেন, যেমন 127.0.0.1:7878/something-else, আপনি একটি কানেকশন এরর পাবেন যেমনটি আপনি লিস্টিং ২১-১ এবং লিস্টিং ২১-২ চালানোর সময় দেখেছিলেন।
এখন চলুন লিস্টিং ২১-৭ এর কোডটি else
ব্লকে যোগ করি যা স্ট্যাটাস কোড 404 সহ একটি রেসপন্স ফেরত দেবে, যা নির্দেশ করে যে রিকোয়েস্টের জন্য কন্টেন্ট পাওয়া যায়নি। আমরা এন্ড-ইউজারকে রেসপন্সটি দেখানোর জন্য ব্রাউজারে রেন্ডার করার জন্য একটি পেজের জন্য কিছু HTML-ও ফেরত দেব।
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); if request_line == "GET / HTTP/1.1" { let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); // --snip-- } else { let status_line = "HTTP/1.1 404 NOT FOUND"; let contents = fs::read_to_string("404.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); } }
এখানে, আমাদের রেসপন্সের একটি স্ট্যাটাস লাইন রয়েছে যার স্ট্যাটাস কোড 404 এবং কারণ-বাক্যাংশ NOT FOUND
। রেসপন্সের বডি হবে 404.html ফাইলের HTML। এরর পেজের জন্য আপনাকে hello.html-এর পাশে একটি 404.html ফাইল তৈরি করতে হবে; আবারও আপনার ইচ্ছামত যেকোনো HTML ব্যবহার করতে পারেন, অথবা লিস্টিং ২১-৮-এর উদাহরণ HTML ব্যবহার করতে পারেন।
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
এই পরিবর্তনগুলোর সাথে, আপনার সার্ভার আবার চালান। 127.0.0.1:7878 রিকোয়েস্ট করলে hello.html-এর বিষয়বস্তু ফেরত আসা উচিত, এবং অন্য যেকোনো রিকোয়েস্ট, যেমন 127.0.0.1:7878/foo, 404.html থেকে এরর HTML ফেরত দেওয়া উচিত।
একটু রিফ্যাক্টরিং
এই মুহূর্তে, if
এবং else
ব্লকগুলিতে অনেক পুনরাবৃত্তি রয়েছে: তারা উভয়ই ফাইল পড়ছে এবং ফাইলগুলির বিষয়বস্তু স্ট্রিমে লিখছে। একমাত্র পার্থক্য হলো স্ট্যাটাস লাইন এবং ফাইলের নাম। চলুন, কোডটিকে আরও সংক্ষিপ্ত করি এই পার্থক্যগুলোকে আলাদা if
এবং else
লাইনে নিয়ে এসে, যা স্ট্যাটাস লাইন এবং ফাইলের নামের মানগুলিকে ভেরিয়েবলে অ্যাসাইন করবে; তারপর আমরা ফাইল পড়া এবং রেসপন্স লেখার জন্য সেই ভেরিয়েবলগুলিকে শর্তহীনভাবে ব্যবহার করতে পারি। লিস্টিং ২১-৯ বড় if
এবং else
ব্লকগুলি প্রতিস্থাপন করার পরের কোডটি দেখায়।
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- let buf_reader = BufReader::new(&stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); let (status_line, filename) = if request_line == "GET / HTTP/1.1" { ("HTTP/1.1 200 OK", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = fs::read_to_string(filename).unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); }
এখন if
এবং else
ব্লকগুলো শুধুমাত্র স্ট্যাটাস লাইন এবং ফাইলের নামের জন্য উপযুক্ত মান একটি টাপল-এ রিটার্ন করে; এরপর আমরা let
স্টেটমেন্টে একটি প্যাটার্ন ব্যবহার করে status_line
এবং filename
-এ এই দুটি মান অ্যাসাইন করতে destructuring ব্যবহার করি, যা অধ্যায় ১৯-এ আলোচনা করা হয়েছে।
পূর্বে ডুপ্লিকেট করা কোডটি এখন if
এবং else
ব্লকের বাইরে এবং status_line
ও filename
ভেরিয়েবল ব্যবহার করে। এটি দুটি ক্ষেত্রের মধ্যে পার্থক্য দেখতে সহজ করে তোলে, এবং এর মানে হলো যদি আমরা ফাইল পড়া এবং রেসপন্স লেখার কাজ পরিবর্তন করতে চাই তবে আমাদের শুধুমাত্র একটি জায়গায় কোড আপডেট করতে হবে। লিস্টিং ২১-৯ এর কোডের আচরণ লিস্টিং ২১-৭ এর মতোই হবে।
অসাধারণ! আমাদের কাছে এখন প্রায় ৪০ লাইনের রাস্ট কোডে একটি সাধারণ ওয়েব সার্ভার রয়েছে যা একটি রিকোয়েস্টে একটি কন্টেন্ট পেজ দিয়ে সাড়া দেয় এবং অন্য সব রিকোয়েস্টে একটি 404 রেসপন্স দিয়ে সাড়া দেয়।
বর্তমানে, আমাদের সার্ভার একটি একক থ্রেডে চলে, যার মানে এটি একবারে শুধুমাত্র একটি রিকোয়েস্ট পরিবেশন করতে পারে। চলুন কিছু ধীরগতির রিকোয়েস্ট সিমুলেট করে দেখি কীভাবে এটি একটি সমস্যা হতে পারে। তারপর আমরা এটি ঠিক করব যাতে আমাদের সার্ভার একবারে একাধিক রিকোয়েস্ট পরিচালনা করতে পারে।
আমাদের সিঙ্গেল-থ্রেডেড সার্ভারকে মাল্টিথ্রেডেড সার্ভারে রূপান্তরিত করা
এই মুহূর্তে, সার্ভারটি প্রতিটি রিকোয়েস্ট একে একে প্রসেস করবে, যার মানে হলো প্রথমটির প্রসেসিং শেষ না হওয়া পর্যন্ত এটি দ্বিতীয় কানেকশন প্রসেস করবে না। সার্ভারে যদি ক্রমাগত রিকোয়েস্ট আসতে থাকে, তাহলে এই সিরিয়াল এক্সিকিউশন পদ্ধতিটি তত কম কার্যকর হতে থাকবে। যদি সার্ভার এমন একটি রিকোয়েস্ট পায় যা প্রসেস করতে অনেক সময় লাগে, তবে পরবর্তী রিকোয়েস্টগুলোকে সেই দীর্ঘ রিকোয়েস্টটি শেষ না হওয়া পর্যন্ত অপেক্ষা করতে হবে, এমনকি যদি নতুন রিকোয়েস্টগুলো দ্রুত প্রসেস করা সম্ভবও হয়। আমাদের এই সমস্যার সমাধান করতে হবে, তবে প্রথমে আমরা সমস্যাটি বাস্তবে দেখব।
একটি ধীরগতির রিকোয়েস্ট সিমুলেট করা
আমরা দেখব কীভাবে একটি ধীরগতির রিকোয়েস্ট আমাদের বর্তমান সার্ভার ইমপ্লিমেন্টেশনে আসা অন্যান্য রিকোয়েস্টকে প্রভাবিত করতে পারে। লিস্টিং ২১-১০ এ /sleep পাথের জন্য একটি রিকোয়েস্ট হ্যান্ডেল করার কোড দেখানো হয়েছে, যেখানে একটি কৃত্রিম ধীরগতির রেসপন্স তৈরি করা হবে যা সার্ভারকে রেসপন্স পাঠানোর আগে পাঁচ সেকেন্ডের জন্য sleep করিয়ে দেবে।
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, thread, time::Duration, }; // --snip-- fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { // --snip-- let buf_reader = BufReader::new(&stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); let (status_line, filename) = match &request_line[..] { "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), "GET /sleep HTTP/1.1" => { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK", "hello.html") } _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), }; // --snip-- let contents = fs::read_to_string(filename).unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); }
যেহেতু এখন আমাদের তিনটি কেস আছে, তাই আমরা if
থেকে match
-এ পরিবর্তিত হয়েছি। স্ট্রিং লিটারেলের সাথে প্যাটার্ন ম্যাচ করার জন্য আমাদের request_line
-এর একটি স্লাইসের উপর স্পষ্টভাবে match
করতে হবে; match
ইক্যুয়ালিটি মেথডের মতো স্বয়ংক্রিয়ভাবে referencing এবং dereferencing করে না।
প্রথম arm-টি লিস্টিং ২১-৯ এর if
ব্লকের মতোই। দ্বিতীয় arm-টি /sleep পাথের একটি রিকোয়েস্টের সাথে ম্যাচ করে। যখন সেই রিকোয়েস্টটি আসে, সার্ভার সফল HTML পেজটি রেন্ডার করার আগে পাঁচ সেকেন্ডের জন্য sleep করবে। তৃতীয় arm-টি লিস্টিং ২১-৯ এর else
ব্লকের মতোই।
আপনি দেখতে পাচ্ছেন আমাদের সার্ভার কতটা প্রাথমিক পর্যায়ের: আসল লাইব্রেরিগুলো এর চেয়ে অনেক কম ভার্বোস উপায়ে একাধিক রিকোয়েস্ট শনাক্ত করতে পারত!
cargo run
ব্যবহার করে সার্ভারটি শুরু করুন। তারপর দুটি ব্রাউজার উইন্ডো খুলুন: একটি http://127.0.0.1:7878 এর জন্য এবং অন্যটি http://127.0.0.1:7878/sleep এর জন্য। যদি আপনি আগের মতো কয়েকবার / URI টিতে যান, দেখবেন এটি দ্রুত সাড়া দিচ্ছে। কিন্তু যদি আপনি /sleep এ যান এবং তারপরে / লোড করেন, আপনি দেখবেন যে sleep
এর পুরো পাঁচ সেকেন্ড শেষ না হওয়া পর্যন্ত / লোড হওয়ার জন্য অপেক্ষা করছে।
একটি ধীরগতির রিকোয়েস্টের কারণে অন্যান্য রিকোয়েস্টের জট এড়ানোর জন্য আমরা অনেক কৌশল ব্যবহার করতে পারি, যার মধ্যে অধ্যায় ১৭-তে ব্যবহৃত async
একটি; আমরা এখানে যেটি ইমপ্লিমেন্ট করব সেটি হলো একটি থ্রেড পুল।
একটি থ্রেড পুল দিয়ে থ্রুপুট উন্নত করা
একটি thread pool হলো স্পন করা কিছু থ্রেডের একটি গ্রুপ, যা কোনো কাজ পরিচালনা করার জন্য প্রস্তুত থাকে এবং অপেক্ষা করে। যখন প্রোগ্রাম একটি নতুন টাস্ক পায়, তখন এটি পুলের একটি থ্রেডকে সেই টাস্কটি দেয় এবং সেই থ্রেড টাস্কটি প্রসেস করে। পুলের বাকি থ্রেডগুলো অন্য কোনো টাস্ক পরিচালনা করার জন্য প্রস্তুত থাকে যা প্রথম থ্রেডটির প্রসেসিং চলাকালীন আসে। যখন প্রথম থ্রেডটি তার টাস্ক প্রসেস করা শেষ করে, তখন এটি নিষ্ক্রিয় থ্রেডের পুলে ফিরে আসে, একটি নতুন টাস্ক পরিচালনা করার জন্য প্রস্তুত হয়ে। একটি থ্রেড পুল আপনাকে কানেকশনগুলো কনকারেন্টলি প্রসেস করতে দেয়, যা আপনার সার্ভারের থ্রুপুট বাড়িয়ে তোলে।
আমরা DoS আক্রমণ থেকে নিজেদের রক্ষা করার জন্য পুলের থ্রেডের সংখ্যা একটি ছোট সংখ্যায় সীমাবদ্ধ রাখব; যদি আমাদের প্রোগ্রাম প্রতিটি রিকোয়েস্ট আসার সাথে সাথে একটি নতুন থ্রেড তৈরি করত, তাহলে কেউ আমাদের সার্ভারে ১০ মিলিয়ন রিকোয়েস্ট পাঠিয়ে আমাদের সার্ভারের সমস্ত রিসোর্স ব্যবহার করে এবং রিকোয়েস্ট প্রসেসিং থামিয়ে দিয়ে বিশৃঙ্খলা সৃষ্টি করতে পারত।
সীমাহীন থ্রেড স্পন করার পরিবর্তে, আমাদের পুলে একটি নির্দিষ্ট সংখ্যক থ্রেড অপেক্ষায় থাকবে। আসা রিকোয়েস্টগুলো প্রসেসিংয়ের জন্য পুলে পাঠানো হয়। পুলটি ইনকামিং রিকোয়েস্টগুলোর একটি কিউ (queue) বজায় রাখবে। পুলের প্রতিটি থ্রেড এই কিউ থেকে একটি রিকোয়েস্ট তুলে নেবে, রিকোয়েস্টটি হ্যান্ডেল করবে, এবং তারপর আরেকটি রিকোয়েস্টের জন্য কিউকে জিজ্ঞাসা করবে। এই ডিজাইনের মাধ্যমে, আমরা একযোগে N
টি পর্যন্ত রিকোয়েস্ট প্রসেস করতে পারি, যেখানে N
হলো থ্রেডের সংখ্যা। যদি প্রতিটি থ্রেড একটি দীর্ঘ সময় ধরে চলা রিকোয়েস্টের জবাব দেয়, তবে পরবর্তী রিকোয়েস্টগুলো কিউতে আটকে যেতে পারে, কিন্তু আমরা সেই পর্যায়ে পৌঁছানোর আগে দীর্ঘ সময় ধরে চলা রিকোয়েস্টের সংখ্যা বাড়িয়ে দিয়েছি।
এই কৌশলটি একটি ওয়েব সার্ভারের থ্রুপুট উন্নত করার অনেক উপায়ের মধ্যে একটি মাত্র। অন্যান্য বিকল্প যা আপনি অন্বেষণ করতে পারেন তা হলো ফর্ক/জয়েন মডেল (fork/join model), সিঙ্গেল-থ্রেডেড অ্যাসিঙ্ক আই/ও মডেল (single-threaded async I/O model) এবং মাল্টি-থ্রেডেড অ্যাসিঙ্ক আই/ও মডেল (multithreaded async I/O model)। আপনি যদি এই বিষয়ে আগ্রহী হন, তবে আপনি অন্যান্য সমাধান সম্পর্কে আরও পড়তে পারেন এবং সেগুলি ইমপ্লিমেন্ট করার চেষ্টা করতে পারেন; Rust-এর মতো একটি লো-লেভেল ল্যাঙ্গুয়েজ দিয়ে, এই সমস্ত বিকল্পই সম্ভব।
আমরা একটি থ্রেড পুল ইমপ্লিমেন্ট করা শুরু করার আগে, চলুন আলোচনা করি যে পুলটি ব্যবহার করার প্রক্রিয়া কেমন হওয়া উচিত। যখন আপনি কোড ডিজাইন করার চেষ্টা করছেন, তখন ক্লায়েন্ট ইন্টারফেসটি প্রথমে লিখে নিলে তা আপনার ডিজাইনকে সঠিক পথে পরিচালিত করতে সাহায্য করে। কোডের API এমনভাবে লিখুন যেভাবে আপনি এটি কল করতে চান; তারপর সেই কাঠামোর মধ্যে কার্যকারিতা ইমপ্লিমেন্ট করুন, কার্যকারিতা ইমপ্লিমেন্ট করে তারপর পাবলিক API ডিজাইন করার পরিবর্তে।
অধ্যায় ১২-এর প্রজেক্টে আমরা যেমন টেস্ট-ড্রিভেন ডেভেলপমেন্ট ব্যবহার করেছি, তেমনি এখানে আমরা কম্পাইলার-ড্রিভেন ডেভেলপমেন্ট (compiler-driven development) ব্যবহার করব। আমরা যে ফাংশনগুলো কল করতে চাই, সেই কোডটি লিখব এবং তারপর কোডটি কাজ করানোর জন্য আমাদের পরবর্তী কী পরিবর্তন করা উচিত তা নির্ধারণ করতে কম্পাইলারের এররগুলো দেখব। তবে, তার আগে, আমরা যে কৌশলটি ব্যবহার করব না সেটি একটি সূচনা বিন্দু হিসাবে অন্বেষণ করব।
প্রতিটি রিকোয়েস্টের জন্য একটি থ্রেড স্পন করা
প্রথমে, চলুন দেখি আমাদের কোড কেমন হতে পারত যদি এটি প্রতিটি কানেকশনের জন্য একটি নতুন থ্রেড তৈরি করত। যেমন আগে উল্লেখ করা হয়েছে, এটি আমাদের চূড়ান্ত পরিকল্পনা নয় কারণ এতে সম্ভাব্য সীমাহীন সংখ্যক থ্রেড স্পন করার সমস্যা রয়েছে, তবে এটি একটি কার্যকর মাল্টিথ্রেডেড সার্ভার তৈরির জন্য একটি সূচনা বিন্দু। তারপর আমরা একটি উন্নতি হিসাবে থ্রেড পুল যোগ করব, এবং দুটি সমাধানের তুলনা করা সহজ হবে।
লিস্টিং ২১-১১ main
ফাংশনে করা পরিবর্তনগুলো দেখায়, যেখানে for
লুপের মধ্যে প্রতিটি স্ট্রিম হ্যান্ডেল করার জন্য একটি নতুন থ্রেড স্পন করা হয়েছে।
use std::{ fs, io::{BufReader, prelude::*}, net::{TcpListener, TcpStream}, thread, time::Duration, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(|| { handle_connection(stream); }); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); let (status_line, filename) = match &request_line[..] { "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), "GET /sleep HTTP/1.1" => { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK", "hello.html") } _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), }; let contents = fs::read_to_string(filename).unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); }
যেমন আপনি অধ্যায় ১৬-এ শিখেছেন, thread::spawn
একটি নতুন থ্রেড তৈরি করবে এবং তারপর নতুন থ্রেডে ক্লোজারের কোডটি চালাবে। যদি আপনি এই কোডটি চালান এবং আপনার ব্রাউজারে /sleep লোড করেন, তারপর আরও দুটি ব্রাউজার ট্যাবে / লোড করেন, আপনি সত্যিই দেখবেন যে / এর রিকোয়েস্টগুলোকে /sleep শেষ হওয়ার জন্য অপেক্ষা করতে হবে না। তবে, যেমন আমরা উল্লেখ করেছি, এটি অবশেষে সিস্টেমকে অভিভূত করবে কারণ আপনি কোনো সীমা ছাড়াই নতুন থ্রেড তৈরি করছেন।
আপনার হয়তো অধ্যায় ১৭ থেকে মনে থাকতে পারে যে ঠিক এই ধরনের পরিস্থিতিতেই async
এবং await
সত্যিই অসাধারণ কাজ করে! আমরা যখন থ্রেড পুল তৈরি করব তখন এটি মনে রাখবেন এবং ভাববেন যে async
এর সাথে জিনিসগুলো কীভাবে ভিন্ন বা একই রকম দেখতে হতো।
একটি সীমিত সংখ্যক থ্রেড তৈরি করা
আমরা চাই আমাদের থ্রেড পুল একটি একইরকম এবং পরিচিত উপায়ে কাজ করুক, যাতে থ্রেড থেকে থ্রেড পুলে স্যুইচ করার জন্য আমাদের API ব্যবহার করা কোডে বড় পরিবর্তন আনার প্রয়োজন না হয়। লিস্টিং ২১-১২ একটি ThreadPool
struct-এর কাল্পনিক ইন্টারফেস দেখায় যা আমরা thread::spawn
-এর পরিবর্তে ব্যবহার করতে চাই।
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
আমরা ThreadPool::new
ব্যবহার করে একটি কনফিগারযোগ্য সংখ্যক থ্রেডসহ একটি নতুন থ্রেড পুল তৈরি করি, এক্ষেত্রে চারটি। তারপর, for
লুপে, pool.execute
-এর thread::spawn
-এর মতোই একটি ইন্টারফেস রয়েছে, কারণ এটি একটি ক্লোজার নেয় যা পুল প্রতিটি স্ট্রিমের জন্য চালাবে। আমাদের pool.execute
ইমপ্লিমেন্ট করতে হবে যাতে এটি ক্লোজারটি নেয় এবং এটি চালানোর জন্য পুলের একটি থ্রেডকে দেয়। এই কোডটি এখনও কম্পাইল হবে না, তবে আমরা চেষ্টা করব যাতে কম্পাইলার আমাদের এটি ঠিক করার পথে গাইড করতে পারে।
কম্পাইলার-ড্রিভেন ডেভেলপমেন্ট ব্যবহার করে ThreadPool
তৈরি করা
src/main.rs-এ লিস্টিং ২১-১২ এর পরিবর্তনগুলো করুন, এবং তারপর আমাদের ডেভেলপমেন্টকে চালিত করতে cargo check
থেকে আসা কম্পাইলার এররগুলো ব্যবহার করি। এখানে প্রথম এররটি আমরা পাই:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error
চমৎকার! এই এররটি আমাদের বলছে যে আমাদের একটি ThreadPool
টাইপ বা মডিউল প্রয়োজন, তাই আমরা এখন একটি তৈরি করব। আমাদের ThreadPool
ইমপ্লিমেন্টেশন আমাদের ওয়েব সার্ভারের কাজের ধরনের থেকে স্বাধীন হবে। তাই চলুন, ThreadPool
ইমপ্লিমেন্টেশন রাখার জন্য hello
ক্রেটটিকে একটি বাইনারি ক্রেট থেকে একটি লাইব্রেরি ক্রেটে পরিবর্তন করি। লাইব্রেরি ক্রেটে পরিবর্তন করার পরে, আমরা আলাদা থ্রেড পুল লাইব্রেরিটি যেকোনো কাজের জন্য ব্যবহার করতে পারি, শুধু ওয়েব রিকোয়েস্ট সার্ভ করার জন্য নয়।
একটি src/lib.rs ফাইল তৈরি করুন যাতে নিম্নলিখিত কোডটি থাকে, যা এই মুহূর্তে আমাদের ThreadPool
struct-এর সবচেয়ে সহজ সংজ্ঞা:
pub struct ThreadPool;
তারপর src/main.rs ফাইলের শীর্ষে নিম্নলিখিত কোডটি যোগ করে ThreadPool
-কে লাইব্রেরি ক্রেট থেকে স্কোপে আনুন:
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
এই কোডটি এখনও কাজ করবে না, কিন্তু চলুন পরবর্তী এররটি পেতে এটি আবার চেক করি যা আমাদের সমাধান করতে হবে:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
এই এররটি নির্দেশ করে যে আমাদের ThreadPool
-এর জন্য new
নামে একটি অ্যাসোসিয়েটেড ফাংশন তৈরি করতে হবে। আমরা আরও জানি যে new
-এর একটি প্যারামিটার থাকতে হবে যা 4
আর্গুমেন্ট হিসাবে গ্রহণ করতে পারে এবং একটি ThreadPool
ইনস্ট্যান্স রিটার্ন করা উচিত। চলুন, এই বৈশিষ্ট্যগুলোসহ সবচেয়ে সহজ new
ফাংশনটি ইমপ্লিমেন্ট করি:
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
আমরা size
প্যারামিটারের টাইপ হিসাবে usize
বেছে নিয়েছি কারণ আমরা জানি যে একটি ঋণাত্মক সংখ্যক থ্রেড অর্থহীন। আমরা আরও জানি যে আমরা এই 4
-কে থ্রেডের একটি কালেকশনের উপাদানের সংখ্যা হিসাবে ব্যবহার করব, যার জন্য usize
টাইপটি ব্যবহৃত হয়, যেমনটি অধ্যায় ৩-এর "Integer Types" এ আলোচনা করা হয়েছে।
চলুন কোডটি আবার চেক করি:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
--> src/main.rs:17:14
|
17 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error```
এখন এররটি ঘটছে কারণ `ThreadPool`-এ আমাদের কোনো `execute` মেথড নেই। ["Creating a Finite Number of Threads"](#creating-a-finite-number-of-threads) থেকে মনে করুন যে আমরা সিদ্ধান্ত নিয়েছিলাম আমাদের থ্রেড পুলের `thread::spawn`-এর মতো একটি ইন্টারফেস থাকা উচিত। এছাড়াও, আমরা `execute` ফাংশনটি এমনভাবে ইমপ্লিমেন্ট করব যাতে এটি প্রদত্ত ক্লোজারটি নিয়ে পুলের একটি নিষ্ক্রিয় থ্রেডকে চালানোর জন্য দেয়।
আমরা `ThreadPool`-এ `execute` মেথডটিকে একটি প্যারামিটার হিসাবে একটি ক্লোজার নেওয়ার জন্য সংজ্ঞায়িত করব। অধ্যায় ১৩-এর ["Moving Captured Values Out of the Closure and the `Fn` Traits"][fn-traits] থেকে মনে করুন যে আমরা তিনটি ভিন্ন ট্রেইট দিয়ে ক্লোজারকে প্যারামিটার হিসাবে নিতে পারি: `Fn`, `FnMut`, এবং `FnOnce`। আমাদের এখানে কোন ধরনের ক্লোজার ব্যবহার করতে হবে তা সিদ্ধান্ত নিতে হবে। আমরা জানি যে আমরা শেষ পর্যন্ত স্ট্যান্ডার্ড লাইব্রেরি `thread::spawn` ইমপ্লিমেন্টেশনের মতো কিছু করব, তাই আমরা `thread::spawn`-এর স্বাক্ষরের প্যারামিটারে কী বাউন্ড রয়েছে তা দেখতে পারি। ডকুমেন্টেশন আমাদের নিম্নলিখিতটি দেখায়:
```rust,ignore
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
F
টাইপ প্যারামিটারটি এখানে আমাদের উদ্বেগের বিষয়; T
টাইপ প্যারামিটারটি রিটার্ন ভ্যালুর সাথে সম্পর্কিত, এবং আমরা এটি নিয়ে চিন্তিত নই। আমরা দেখতে পাচ্ছি যে spawn
F
-এর উপর ট্রেইট বাউন্ড হিসাবে FnOnce
ব্যবহার করে। এটি সম্ভবত আমরাও চাই, কারণ আমরা অবশেষে execute
-এ প্রাপ্ত আর্গুমেন্টটি spawn
-এ পাস করব। আমরা আরও নিশ্চিত হতে পারি যে FnOnce
-ই আমাদের কাঙ্ক্ষিত ট্রেইট, কারণ একটি রিকোয়েস্ট চালানোর জন্য থ্রেডটি সেই রিকোয়েস্টের ক্লোজারটি শুধুমাত্র একবারই এক্সিকিউট করবে, যা FnOnce
-এর Once
-এর সাথে মিলে যায়।
F
টাইপ প্যারামিটারের Send
ট্রেইট বাউন্ড এবং 'static
লাইফটাইম বাউন্ডও রয়েছে, যা আমাদের পরিস্থিতিতে কার্যকর: আমাদের এক থ্রেড থেকে অন্য থ্রেডে ক্লোজার স্থানান্তর করতে Send
প্রয়োজন এবং 'static
প্রয়োজন কারণ আমরা জানি না থ্রেডটি এক্সিকিউট হতে কত সময় নেবে। চলুন ThreadPool
-এ একটি execute
মেথড তৈরি করি যা এই বাউন্ডগুলোসহ F
টাইপের একটি জেনেরিক প্যারামিটার নেবে:
pub struct ThreadPool;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
আমরা এখনও FnOnce
এর পরে ()
ব্যবহার করছি কারণ এই FnOnce
একটি ক্লোজারকে প্রতিনিধিত্ব করে যা কোনো প্যারামিটার নেয় না এবং ইউনিট টাইপ ()
রিটার্ন করে। ফাংশন সংজ্ঞার মতোই, রিটার্ন টাইপটি স্বাক্ষর থেকে বাদ দেওয়া যেতে পারে, কিন্তু আমাদের কোনো প্যারামিটার না থাকলেও, আমাদের এখনও বন্ধনী প্রয়োজন।
আবারও, এটি execute
মেথডের সবচেয়ে সহজ ইমপ্লিমেন্টেশন: এটি কিছুই করে না, কিন্তু আমরা শুধু আমাদের কোড কম্পাইল করার চেষ্টা করছি। চলুন এটি আবার চেক করি:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
এটি কম্পাইল হয়! কিন্তু লক্ষ্য করুন যে আপনি যদি cargo run
চেষ্টা করেন এবং ব্রাউজারে একটি রিকোয়েস্ট করেন, তবে আপনি ব্রাউজারে অধ্যায়ের শুরুতে দেখা এররগুলো দেখতে পাবেন। আমাদের লাইব্রেরি আসলে execute
-এ পাস করা ক্লোজারটি এখনও কল করছে না!
দ্রষ্টব্য: Haskell এবং Rust-এর মতো কঠোর কম্পাইলারসহ ল্যাঙ্গুয়েজ সম্পর্কে আপনি একটি কথা শুনতে পারেন: "যদি কোড কম্পাইল হয়, তবে এটি কাজ করে।" কিন্তু এই কথাটি সর্বজনীনভাবে সত্য নয়। আমাদের প্রজেক্ট কম্পাইল হচ্ছে, কিন্তু এটি बिल्कुल কিছুই করছে না! যদি আমরা একটি বাস্তব, সম্পূর্ণ প্রজেক্ট তৈরি করতাম, তবে এটি ইউনিট টেস্ট লেখা শুরু করার একটি ভাল সময় হতো যাতে কোড কম্পাইল হয় এবং আমাদের কাঙ্ক্ষিত আচরণ করে।
বিবেচনা করুন: যদি আমরা একটি ক্লোজারের পরিবর্তে একটি ফিউচার (future) এক্সিকিউট করতে যেতাম তাহলে এখানে কী ভিন্ন হতো?
new
-তে থ্রেডের সংখ্যা যাচাই করা
আমরা new
এবং execute
-এর প্যারামিটার দিয়ে কিছুই করছি না। চলুন, আমাদের কাঙ্ক্ষিত আচরণসহ এই ফাংশনগুলোর বডি ইমপ্লিমেন্ট করি। শুরু করার জন্য, চলুন new
সম্পর্কে ভাবি। আগে আমরা size
প্যারামিটারের জন্য একটি আনসাইন্ড টাইপ বেছে নিয়েছিলাম কারণ ঋণাত্মক সংখ্যক থ্রেডসহ একটি পুল অর্থহীন। তবে, শূন্য থ্রেডসহ একটি পুলও অর্থহীন, তবুও শূন্য একটি পুরোপুরি বৈধ usize
। আমরা কোড যোগ করব যা ThreadPool
ইনস্ট্যান্স রিটার্ন করার আগে size
শূন্যের চেয়ে বড় কিনা তা পরীক্ষা করবে এবং যদি শূন্য পায় তবে assert!
ম্যাক্রো ব্যবহার করে প্রোগ্রামটি প্যানিক করাবে, যেমনটি লিস্টিং ২১-১৩ এ দেখানো হয়েছে।
pub struct ThreadPool;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
ThreadPool
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
আমরা ডক কমেন্ট দিয়ে আমাদের ThreadPool
-এর জন্য কিছু ডকুমেন্টেশনও যোগ করেছি। লক্ষ্য করুন যে আমরা অধ্যায় ১৪-তে আলোচনা করা অনুযায়ী ভাল ডকুমেন্টেশন পদ্ধতি অনুসরণ করেছি এবং এমন একটি বিভাগ যোগ করেছি যা সেই পরিস্থিতিগুলো উল্লেখ করে যেখানে আমাদের ফাংশন প্যানিক করতে পারে। cargo doc --open
চালান এবং ThreadPool
struct-এ ক্লিক করে দেখুন new
-এর জন্য জেনারেট করা ডক্স কেমন দেখায়!
এখানে assert!
ম্যাক্রো যোগ করার পরিবর্তে, আমরা new
-কে build
-এ পরিবর্তন করতে পারতাম এবং I/O প্রজেক্টে লিস্টিং ১২-৯-এর Config::build
-এর মতো একটি Result
রিটার্ন করতে পারতাম। কিন্তু আমরা এই ক্ষেত্রে সিদ্ধান্ত নিয়েছি যে কোনো থ্রেড ছাড়াই একটি থ্রেড পুল তৈরি করার চেষ্টা একটি ناقابل পুনরুদ্ধারযোগ্য এরর হওয়া উচিত। আপনি যদি উচ্চাকাঙ্ক্ষী হন, তবে new
ফাংশনের সাথে তুলনা করার জন্য নিম্নলিখিত স্বাক্ষরসহ build
নামে একটি ফাংশন লেখার চেষ্টা করুন:
pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
থ্রেড সংরক্ষণ করার জন্য স্থান তৈরি করা
এখন যেহেতু আমরা জানি যে পুলে সংরক্ষণ করার জন্য আমাদের কাছে একটি বৈধ সংখ্যক থ্রেড আছে, আমরা সেই থ্রেডগুলো তৈরি করতে পারি এবং ThreadPool
struct-এ সংরক্ষণ করতে পারি structটি রিটার্ন করার আগে। কিন্তু আমরা কীভাবে একটি থ্রেড "সংরক্ষণ" করব? চলুন thread::spawn
এর স্বাক্ষরটি আরেকবার দেখি:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
spawn
ফাংশনটি একটি JoinHandle<T>
রিটার্ন করে, যেখানে T
হলো সেই টাইপ যা ক্লোজারটি রিটার্ন করে। চলুন JoinHandle
ব্যবহার করে দেখি কী হয়। আমাদের ক্ষেত্রে, আমরা যে ক্লোজারগুলো থ্রেড পুলে পাস করছি সেগুলি কানেকশন হ্যান্ডেল করবে এবং কিছু রিটার্ন করবে না, তাই T
হবে ইউনিট টাইপ ()
।
লিস্টিং ২১-১৪ এর কোডটি কম্পাইল হবে কিন্তু এখনও কোনো থ্রেড তৈরি করবে না। আমরা ThreadPool
-এর সংজ্ঞা পরিবর্তন করে thread::JoinHandle<()>
ইনস্ট্যান্সের একটি ভেক্টর ধারণ করার ব্যবস্থা করেছি, size
ক্যাপাসিটিসহ ভেক্টরটি ইনিশিয়ালাইজ করেছি, একটি for
লুপ সেট আপ করেছি যা থ্রেড তৈরির জন্য কিছু কোড চালাবে, এবং তাদের ধারণকারী একটি ThreadPool
ইনস্ট্যান্স রিটার্ন করেছি।
use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut threads = Vec::with_capacity(size);
for _ in 0..size {
// create some threads and store them in the vector
}
ThreadPool { threads }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
আমরা লাইব্রেরি ক্রেটে std::thread
-কে স্কোপে নিয়ে এসেছি কারণ আমরা ThreadPool
-এর ভেক্টরের আইটেমগুলোর টাইপ হিসাবে thread::JoinHandle
ব্যবহার করছি।
একবার একটি বৈধ সাইজ পেলে, আমাদের ThreadPool
একটি নতুন ভেক্টর তৈরি করে যা size
সংখ্যক আইটেম ধারণ করতে পারে। with_capacity
ফাংশনটি Vec::new
-এর মতোই কাজ করে কিন্তু একটি গুরুত্বপূর্ণ পার্থক্যসহ: এটি ভেক্টরে স্থান পূর্ব-বরাদ্দ করে। যেহেতু আমরা জানি যে আমাদের ভেক্টরে size
সংখ্যক এলিমেন্ট সংরক্ষণ করতে হবে, তাই এই বরাদ্দটি আগে থেকে করা Vec::new
ব্যবহার করার চেয়ে কিছুটা বেশি কার্যকর, যা এলিমেন্ট প্রবেশ করানোর সাথে সাথে নিজেকে রিসাইজ করে।
যখন আপনি cargo check
আবার চালাবেন, এটি সফল হওয়া উচিত।
ThreadPool
থেকে একটি থ্রেডে কোড পাঠানো
আমরা লিস্টিং ২১-১৪ এর for
লুপে থ্রেড তৈরির বিষয়ে একটি কমেন্ট রেখেছিলাম। এখানে, আমরা দেখব কীভাবে আমরা আসলে থ্রেড তৈরি করি। স্ট্যান্ডার্ড লাইব্রেরি থ্রেড তৈরির একটি উপায় হিসাবে thread::spawn
প্রদান করে, এবং thread::spawn
আশা করে যে থ্রেডটি তৈরি হওয়ার সাথে সাথেই চালানোর জন্য কিছু কোড পাবে। তবে, আমাদের ক্ষেত্রে, আমরা থ্রেডগুলো তৈরি করতে এবং তাদের এমন কোডের জন্য অপেক্ষা করতে চাই যা আমরা পরে পাঠাব। স্ট্যান্ডার্ড লাইব্রেরির থ্রেড ইমপ্লিমেন্টেশনে এটি করার কোনো উপায় অন্তর্ভুক্ত নেই; আমাদের এটি ম্যানুয়ালি ইমপ্লিমেন্ট করতে হবে।
আমরা ThreadPool
এবং থ্রেডগুলোর মধ্যে একটি নতুন ডেটা স্ট্রাকচার চালু করে এই আচরণটি ইমপ্লিমেন্ট করব যা এই নতুন আচরণটি পরিচালনা করবে। আমরা এই ডেটা স্ট্রাকচারটিকে Worker বলব, যা পুলিং ইমপ্লিমেন্টেশনে একটি সাধারণ পরিভাষা। Worker
চালানোর জন্য প্রয়োজনীয় কোড তুলে নেয় এবং সেই কোডটি তার থ্রেডে চালায়।
একটি রেস্তোরাঁর রান্নাঘরে কাজ করা লোকদের কথা ভাবুন: কর্মীরা গ্রাহকদের কাছ থেকে অর্ডার আসা পর্যন্ত অপেক্ষা করে, এবং তারপর তারা সেই অর্ডারগুলো নিয়ে সেগুলি পূরণ করার জন্য দায়ী।
থ্রেড পুলে JoinHandle<()>
ইনস্ট্যান্সের একটি ভেক্টর সংরক্ষণ করার পরিবর্তে, আমরা Worker
struct-এর ইনস্ট্যান্স সংরক্ষণ করব। প্রতিটি Worker
একটি একক JoinHandle<()>
ইনস্ট্যান্স সংরক্ষণ করবে। তারপর আমরা Worker
-এর উপর একটি মেথড ইমপ্লিমেন্ট করব যা চালানোর জন্য একটি ক্লোজার নেবে এবং এটি এক্সিকিউশনের জন্য ইতিমধ্যে চলমান থ্রেডে পাঠাবে। আমরা প্রতিটি Worker
-কে একটি id
দেব যাতে আমরা লগিং বা ডিবাগিং করার সময় পুলের বিভিন্ন Worker
ইনস্ট্যান্সের মধ্যে পার্থক্য করতে পারি।
এখানে নতুন প্রক্রিয়াটি রয়েছে যা আমরা একটি ThreadPool
তৈরি করার সময় ঘটবে। Worker
-কে এভাবে সেট আপ করার পরে আমরা ক্লোজারটি থ্রেডে পাঠানোর কোডটি ইমপ্লিমেন্ট করব:
- একটি
Worker
struct সংজ্ঞায়িত করুন যা একটিid
এবং একটিJoinHandle<()>
ধারণ করে। ThreadPool
-কেWorker
ইনস্ট্যান্সের একটি ভেক্টর ধারণ করার জন্য পরিবর্তন করুন।- একটি
Worker::new
ফাংশন সংজ্ঞায়িত করুন যা একটিid
নম্বর নেয় এবং একটিWorker
ইনস্ট্যান্স রিটার্ন করে যাid
এবং একটি খালি ক্লোজার দিয়ে স্পন করা একটি থ্রেড ধারণ করে। ThreadPool::new
-এ,for
লুপ কাউন্টার ব্যবহার করে একটিid
তৈরি করুন, সেইid
দিয়ে একটি নতুনWorker
তৈরি করুন, এবংWorker
-টিকে ভেক্টরে সংরক্ষণ করুন।
আপনি যদি একটি চ্যালেঞ্জের জন্য প্রস্তুত হন, তবে লিস্টিং ২১-১৫ এর কোডটি দেখার আগে নিজে এই পরিবর্তনগুলি ইমপ্লিমেন্ট করার চেষ্টা করুন।
প্রস্তুত? এখানে লিস্টিং ২১-১৫ রয়েছে যা পূর্ববর্তী পরিবর্তনগুলি করার একটি উপায় দেখায়।
use std::thread;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
আমরা ThreadPool
-এর ফিল্ডের নাম threads
থেকে workers
-এ পরিবর্তন করেছি কারণ এটি এখন JoinHandle<()>
ইনস্ট্যান্সের পরিবর্তে Worker
ইনস্ট্যান্স ধারণ করছে। আমরা for
লুপের কাউন্টারটিকে Worker::new
-এর আর্গুমেন্ট হিসাবে ব্যবহার করি, এবং আমরা প্রতিটি নতুন Worker
-কে workers
নামের ভেক্টরে সংরক্ষণ করি।
বাহ্যিক কোড (যেমন src/main.rs-এ আমাদের সার্ভার) ThreadPool
-এর মধ্যে একটি Worker
struct ব্যবহারের বাস্তবায়ন বিবরণ জানার প্রয়োজন নেই, তাই আমরা Worker
struct এবং তার new
ফাংশনটিকে প্রাইভেট করে দিই। Worker::new
ফাংশনটি আমাদের দেওয়া id
ব্যবহার করে এবং একটি JoinHandle<()>
ইনস্ট্যান্স সংরক্ষণ করে যা একটি খালি ক্লোজার ব্যবহার করে একটি নতুন থ্রেড স্পন করে তৈরি করা হয়।
দ্রষ্টব্য: যদি অপারেটিং সিস্টেম পর্যাপ্ত সিস্টেম রিসোর্স না থাকার কারণে একটি থ্রেড তৈরি করতে না পারে,
thread::spawn
প্যানিক করবে। এটি আমাদের পুরো সার্ভারটিকে প্যানিক করাবে, যদিও কিছু থ্রেড তৈরি সফল হতে পারে। সরলতার জন্য, এই আচরণটি ঠিক আছে, কিন্তু একটি প্রোডাকশন থ্রেড পুল ইমপ্লিমেন্টেশনে, আপনি সম্ভবতstd::thread::Builder
এবং তারspawn
মেথড ব্যবহার করতে চাইবেন যাResult
রিটার্ন করে।
এই কোডটি কম্পাইল হবে এবং ThreadPool::new
-এর আর্গুমেন্ট হিসাবে নির্দিষ্ট করা Worker
ইনস্ট্যান্সের সংখ্যা সংরক্ষণ করবে। কিন্তু আমরা এখনও execute
-এ পাওয়া ক্লোজারটি প্রসেস করছি না। চলুন দেখি পরবর্তীতে এটি কীভাবে করা যায়।
চ্যানেলগুলির মাধ্যমে থ্রেডগুলিতে রিকোয়েস্ট পাঠানো
পরবর্তী সমস্যাটি হলো thread::spawn
-কে দেওয়া ক্লোজারগুলো কিছুই করে না। বর্তমানে, আমরা execute
মেথডে যে ক্লোজারটি এক্সিকিউট করতে চাই তা পাই। কিন্তু ThreadPool
তৈরির সময় প্রতিটি Worker
তৈরি করার সময় আমাদের thread::spawn
-কে চালানোর জন্য একটি ক্লোজার দিতে হবে।
আমরা চাই যে আমরা এইমাত্র যে Worker
struct গুলো তৈরি করেছি সেগুলি ThreadPool
-এ রাখা একটি কিউ থেকে চালানোর জন্য কোড আনুক এবং সেই কোডটি চালানোর জন্য তার থ্রেডে পাঠাক।
অধ্যায় ১৬-তে শেখা চ্যানেলগুলো—দুটি থ্রেডের মধ্যে যোগাযোগের একটি সহজ উপায়—এই ব্যবহারের জন্য উপযুক্ত হবে। আমরা একটি চ্যানেলকে জবের কিউ হিসাবে কাজ করার জন্য ব্যবহার করব, এবং execute
ThreadPool
থেকে Worker
ইনস্ট্যান্সগুলিতে একটি জব পাঠাবে, যা জবটি তার থ্রেডে পাঠাবে। এখানে পরিকল্পনাটি হলো:
ThreadPool
একটি চ্যানেল তৈরি করবে এবং সেন্ডারটি ধরে রাখবে।- প্রতিটি
Worker
রিসিভারটি ধরে রাখবে। - আমরা একটি নতুন
Job
struct তৈরি করব যা চ্যানেলের মাধ্যমে পাঠাতে চাওয়া ক্লোজারগুলো ধারণ করবে। execute
মেথডটি যে জবটি এক্সিকিউট করতে চায় তা সেন্ডারের মাধ্যমে পাঠাবে।- তার থ্রেডে,
Worker
তার রিসিভারের উপর লুপ করবে এবং প্রাপ্ত যেকোনো জবের ক্লোজার এক্সিকিউট করবে।
চলুন ThreadPool::new
-এ একটি চ্যানেল তৈরি করে এবং ThreadPool
ইনস্ট্যান্সে সেন্ডারটি ধরে রাখার মাধ্যমে শুরু করি, যেমনটি লিস্টিং ২১-১৬ এ দেখানো হয়েছে। Job
struct টি আপাতত কিছু ধারণ করে না তবে এটি সেই আইটেমের টাইপ হবে যা আমরা চ্যানেলের মাধ্যমে পাঠাচ্ছি।
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool::new
-এ, আমরা আমাদের নতুন চ্যানেল তৈরি করি এবং পুলটিকে সেন্ডার ধরে রাখতে দিই। এটি সফলভাবে কম্পাইল হবে।
চলুন থ্রেড পুল চ্যানেল তৈরি করার সময় প্রতিটি Worker
-এর মধ্যে চ্যানেলের একটি রিসিভার পাস করার চেষ্টা করি। আমরা জানি যে আমরা Worker
ইনস্ট্যান্স দ্বারা স্পন করা থ্রেডে রিসিভারটি ব্যবহার করতে চাই, তাই আমরা ক্লোজারে receiver
প্যারামিটারটি রেফারেন্স করব। লিস্টিং ২১-১৭ এর কোডটি এখনও ঠিক কম্পাইল হবে না।
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, receiver));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
আমরা কিছু ছোট এবং সহজ পরিবর্তন করেছি: আমরা Worker::new
-এ রিসিভার পাস করেছি, এবং তারপর আমরা এটি ক্লোজারের ভিতরে ব্যবহার করেছি।
যখন আমরা এই কোডটি চেক করার চেষ্টা করি, আমরা এই এররটি পাই:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
help: consider moving the expression out of the loop so it is only moved once
|
25 ~ let mut value = Worker::new(id, receiver);
26 ~ for id in 0..size {
27 ~ workers.push(value);
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error```
কোডটি `receiver`-কে একাধিক `Worker` ইনস্ট্যান্সে পাস করার চেষ্টা করছে। এটি কাজ করবে না, যেমন আপনি অধ্যায় ১৬ থেকে মনে করতে পারেন: Rust দ্বারা প্রদত্ত চ্যানেল ইমপ্লিমেন্টেশনটি হলো মাল্টিপল _প্রডিউসার_, সিঙ্গেল _কনজিউমার_। এর মানে আমরা এই কোডটি ঠিক করার জন্য চ্যানেলের কনজিউমিং এন্ডটি ক্লোন করতে পারি না। আমরা একটি বার্তা একাধিক কনজিউমারের কাছে একাধিকবার পাঠাতেও চাই না; আমরা একাধিক `Worker` ইনস্ট্যান্সসহ বার্তাগুলির একটি তালিকা চাই যাতে প্রতিটি বার্তা একবার প্রসেস হয়।
এছাড়াও, চ্যানেল কিউ থেকে একটি জব নেওয়ার জন্য `receiver`-কে মিউটেট করতে হয়, তাই থ্রেডগুলির `receiver`-কে শেয়ার এবং পরিবর্তন করার জন্য একটি নিরাপদ উপায় প্রয়োজন; অন্যথায়, আমরা রেস কন্ডিশন পেতে পারি (যেমনটি অধ্যায় ১৬-তে আলোচনা করা হয়েছে)।
অধ্যায় ১৬-তে আলোচনা করা থ্রেড-সেফ স্মার্ট পয়েন্টারগুলির কথা মনে করুন: একাধিক থ্রেডে মালিকানা শেয়ার করতে এবং থ্রেডগুলিকে মান পরিবর্তন করার অনুমতি দিতে, আমাদের `Arc<Mutex<T>>` ব্যবহার করতে হবে। `Arc` টাইপ একাধিক `Worker`-কে রিসিভারের মালিকানা দেবে এবং `Mutex` নিশ্চিত করবে যে একবারে শুধুমাত্র একটি `Worker`-ই রিসিভার থেকে জব পাবে। লিস্টিং ২১-১৮ আমাদের প্রয়োজনীয় পরিবর্তনগুলি দেখায়।
<Listing number="21-18" file-name="src/lib.rs" caption="`Arc` এবং `Mutex` ব্যবহার করে `Worker` ইনস্ট্যান্সগুলির মধ্যে রিসিভার শেয়ার করা">
```rust,noplayground
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
// --snip--
# pub struct ThreadPool {
# workers: Vec<Worker>,
# sender: mpsc::Sender<Job>,
# }
#
# struct Job;
#
impl ThreadPool {
// --snip--
# /// Create a new ThreadPool.
# ///
# /// The size is the number of threads in the pool.
# ///
# /// # Panics
# ///
# /// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
// --snip--
#
# pub fn execute<F>(&self, f: F)
# where
# F: FnOnce() + Send + 'static,
# {
# }
}
// --snip--
# struct Worker {
# id: usize,
# thread: thread::JoinHandle<()>,
# }
#
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
# let thread = thread::spawn(|| {
# receiver;
# });
#
# Worker { id, thread }
}
}
ThreadPool::new
-এ, আমরা রিসিভারটিকে একটি Arc
এবং একটি Mutex
-এ রাখি। প্রতিটি নতুন Worker
-এর জন্য, আমরা Arc
ক্লোন করি যাতে রেফারেন্স কাউন্ট বাড়ে এবং Worker
ইনস্ট্যান্সগুলি রিসিভারের মালিকানা শেয়ার করতে পারে।
এই পরিবর্তনগুলির সাথে, কোডটি কম্পাইল হয়! আমরা লক্ষ্যের কাছাকাছি চলে এসেছি!
execute
মেথডটি ইমপ্লিমেন্ট করা
চলুন অবশেষে ThreadPool
-এর execute
মেথডটি ইমপ্লিমেন্ট করি। আমরা Job
-কে একটি struct থেকে একটি trait object-এর জন্য একটি টাইপ এলিয়াসে পরিবর্তন করব যা execute
-এর প্রাপ্ত ক্লোজারের টাইপ ধারণ করে। অধ্যায় ২০-এর "Creating Type Synonyms with Type Aliases"-এ আলোচনা করা হয়েছে, টাইপ এলিয়াস আমাদের দীর্ঘ টাইপগুলোকে ব্যবহারের সুবিধার জন্য ছোট করতে দেয়। লিস্টিং ২১-১৯ দেখুন।
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
execute
-এ প্রাপ্ত ক্লোজার ব্যবহার করে একটি নতুন Job
ইনস্ট্যান্স তৈরি করার পরে, আমরা সেই জবটি চ্যানেলের সেন্ডিং এন্ড দিয়ে পাঠাই। আমরা send
-এর উপর unwrap
কল করছি যদি সেন্ডিং ব্যর্থ হয় সেই কেসের জন্য। এটি ঘটতে পারে যদি, উদাহরণস্বরূপ, আমরা আমাদের সমস্ত থ্রেডকে এক্সিকিউট করা থেকে বিরত রাখি, যার মানে রিসিভিং এন্ড নতুন বার্তা গ্রহণ করা বন্ধ করে দিয়েছে। এই মুহূর্তে, আমরা আমাদের থ্রেডগুলিকে এক্সিকিউট করা থেকে বিরত রাখতে পারি না: আমাদের থ্রেডগুলি পুল বিদ্যমান থাকা পর্যন্ত এক্সিকিউট হতে থাকে। আমরা unwrap
ব্যবহার করার কারণ হলো আমরা জানি যে ব্যর্থতার কেসটি ঘটবে না, কিন্তু কম্পাইলার তা জানে না।
কিন্তু আমরা এখনও পুরোপুরি শেষ করিনি! Worker
-এ, thread::spawn
-কে পাস করা আমাদের ক্লোজারটি এখনও শুধুমাত্র চ্যানেলের রিসিভিং এন্ডকে রেফারেন্স করে। এর পরিবর্তে, আমাদের ক্লোজারটিকে চিরতরে লুপ করতে হবে, চ্যানেলের রিসিভিং এন্ড থেকে একটি জবের জন্য জিজ্ঞাসা করতে হবে এবং যখন এটি একটি জব পায় তখন সেটি চালাতে হবে। চলুন লিস্টিং ২১-২০-তে দেখানো পরিবর্তনটি Worker::new
-তে করি।
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
এখানে, আমরা প্রথমে মিউটেক্স অর্জন করতে receiver
-এর উপর lock
কল করি, এবং তারপর আমরা যেকোনো এররের উপর প্যানিক করার জন্য unwrap
কল করি। একটি লক অর্জন ব্যর্থ হতে পারে যদি মিউটেক্সটি একটি poisoned অবস্থায় থাকে, যা ঘটতে পারে যদি অন্য কোনো থ্রেড লকটি ধরে রাখার সময় প্যানিক করে এবং লকটি রিলিজ না করে। এই পরিস্থিতিতে, এই থ্রেডটিকে প্যানিক করানোর জন্য unwrap
কল করা সঠিক পদক্ষেপ। আপনি এই unwrap
-কে আপনার জন্য অর্থপূর্ণ একটি এরর বার্তা সহ একটি expect
-এ পরিবর্তন করতে পারেন।
যদি আমরা মিউটেক্সের উপর লক পাই, আমরা চ্যানেল থেকে একটি Job
গ্রহণ করতে recv
কল করি। একটি চূড়ান্ত unwrap
এখানেও যেকোনো এরর পার করে দেয়, যা ঘটতে পারে যদি সেন্ডার ধারণকারী থ্রেডটি বন্ধ হয়ে যায়, যেমন send
মেথডটি রিসিভার বন্ধ হয়ে গেলে Err
রিটার্ন করে।
recv
-এর কলটি ব্লক করে, তাই যদি এখনও কোনো জব না থাকে, বর্তমান থ্রেডটি একটি জব উপলব্ধ না হওয়া পর্যন্ত অপেক্ষা করবে। Mutex<T>
নিশ্চিত করে যে একবারে শুধুমাত্র একটি Worker
থ্রেডই একটি জব রিকোয়েস্ট করার চেষ্টা করছে।
আমাদের থ্রেড পুল এখন একটি কার্যকরী অবস্থায় আছে! cargo run
দিন এবং কিছু রিকোয়েস্ট করুন:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
7 | workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: fields `id` and `thread` are never read
--> src/lib.rs:48:5
|
47 | struct Worker {
| ------ fields in this struct
48 | id: usize,
| ^^
49 | thread: thread::JoinHandle<()>,
| ^^^^^^
warning: `hello` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
সফল! আমাদের এখন একটি থ্রেড পুল আছে যা কানেকশনগুলো অ্যাসিঙ্ক্রোনাসভাবে এক্সিকিউট করে। এখানে কখনও চারের বেশি থ্রেড তৈরি হয় না, তাই সার্ভার যদি অনেক রিকোয়েস্ট পায় তবে আমাদের সিস্টেম ওভারলোড হবে না। যদি আমরা /sleep-এ একটি রিকোয়েস্ট করি, সার্ভারটি অন্য একটি থ্রেড দিয়ে অন্য রিকোয়েস্টগুলো সার্ভ করতে সক্ষম হবে।
দ্রষ্টব্য: আপনি যদি একাধিক ব্রাউজার উইন্ডোতে একই সাথে /sleep খোলেন, তবে সেগুলি পাঁচ সেকেন্ডের ব্যবধানে একে একে লোড হতে পারে। কিছু ওয়েব ব্রাউজার ক্যাশিংয়ের কারণে একই রিকোয়েস্টের একাধিক ইনস্ট্যান্স ক্রমানুসারে এক্সিকিউট করে। এই সীমাবদ্ধতাটি আমাদের ওয়েব সার্ভারের কারণে নয়।
এটি একটি ভাল সময় থামার এবং চিন্তা করার যে লিস্টিং ২১-১৮, ২১-১৯, এবং ২১-২০-এর কোড কীভাবে ভিন্ন হতো যদি আমরা কাজ করার জন্য একটি ক্লোজারের পরিবর্তে ফিউচার (futures) ব্যবহার করতাম। কোন টাইপগুলি পরিবর্তন হতো? মেথড সিগনেচারগুলো কীভাবে ভিন্ন হতো, যদি überhaupt হয়? কোডের কোন অংশগুলো একই থাকতো?
অধ্যায় ১৭ এবং অধ্যায় ১৯-এ while let
লুপ সম্পর্কে শেখার পর, আপনি হয়তো ভাবছেন কেন আমরা Worker
থ্রেডের কোডটি লিস্টিং ২১-২১-এ দেখানো উপায়ে লিখিনি।
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
while let Ok(job) = receiver.lock().unwrap().recv() {
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
এই কোডটি কম্পাইল এবং রান হয় কিন্তু কাঙ্ক্ষিত থ্রেডিং আচরণ দেয় না: একটি ধীরগতির রিকোয়েস্ট এখনও অন্যান্য রিকোয়েস্টকে প্রসেস করার জন্য অপেক্ষা করাবে। কারণটি কিছুটা সূক্ষ্ম: Mutex
struct-এর কোনো পাবলিক unlock
মেথড নেই কারণ লকের মালিকানা lock
মেথডের রিটার্ন করা LockResult<MutexGuard<T>>
-এর মধ্যে থাকা MutexGuard<T>
-এর লাইফটাইমের উপর ভিত্তি করে। কম্পাইল টাইমে, বোরো চেকার তখন এই নিয়মটি প্রয়োগ করতে পারে যে একটি Mutex
দ্বারা সুরক্ষিত রিসোর্স অ্যাক্সেস করা যাবে না যদি না আমরা লকটি ধরে রাখি। তবে, এই ইমপ্লিমেন্টেশনটি MutexGuard<T>
-এর লাইফটাইম সম্পর্কে সতর্ক না থাকলে উদ্দেশ্যর চেয়ে বেশি সময় ধরে লক ধরে রাখতে পারে।
লিস্টিং ২১-২০-এর কোড যা let job = receiver.lock().unwrap().recv().unwrap();
ব্যবহার করে, তা কাজ করে কারণ let
-এর সাথে, সমান চিহ্নের ডানদিকে এক্সপ্রেশনে ব্যবহৃত যেকোনো টেম্পোরারি ভ্যালু let
স্টেটমেন্ট শেষ হওয়ার সাথে সাথে ড্রপ হয়ে যায়। তবে, while let
(এবং if let
ও match
) সংশ্লিষ্ট ব্লকের শেষ না হওয়া পর্যন্ত টেম্পোরারি ভ্যালু ড্রপ করে না। লিস্টিং ২১-২১-এ, job()
কলের পুরো সময় ধরে লকটি ধরা থাকে, যার মানে অন্য Worker
ইনস্ট্যান্সগুলো জব রিসিভ করতে পারে না।
সুন্দরভাবে শাটডাউন এবং পরিচ্ছন্নতা
লিস্টিং ২১-২০ এর কোডটি আমাদের উদ্দেশ্য অনুযায়ী, একটি থ্রেড পুল ব্যবহার করে অ্যাসিঙ্ক্রোনাসভাবে রিকোয়েস্টের জবাব দিচ্ছে। আমরা workers
, id
, এবং thread
ফিল্ডগুলো সরাসরি ব্যবহার করছি না বলে কিছু ওয়ার্নিং পাচ্ছি, যা আমাদের মনে করিয়ে দিচ্ছে যে আমরা কোনো কিছুই পরিচ্ছন্ন (cleanup) করছি না। যখন আমরা প্রধান থ্রেডটি বন্ধ করার জন্য কম মার্জিত ctrl-C` পদ্ধতি ব্যবহার করি, তখন অন্য সমস্ত থ্রেডও সঙ্গে সঙ্গে বন্ধ হয়ে যায়, এমনকি যদি তারা কোনো রিকোয়েস্ট সার্ভ করার মাঝখানেও থাকে।
এরপর, আমরা Drop
ট্রেইট ইমপ্লিমেন্ট করব যাতে পুলের প্রতিটি থ্রেডের উপর join
কল করা যায় এবং তারা বন্ধ হওয়ার আগে তাদের হাতে থাকা রিকোয়েস্টগুলোর কাজ শেষ করতে পারে। তারপর আমরা থ্রেডগুলোকে জানানোর জন্য একটি উপায় ইমপ্লিমেন্ট করব যে তাদের নতুন রিকোয়েস্ট গ্রহণ করা বন্ধ করে শাটডাউন করা উচিত। এই কোডটি বাস্তবে দেখার জন্য, আমরা আমাদের সার্ভারটিকে এমনভাবে পরিবর্তন করব যাতে এটি তার থ্রেড পুল সুন্দরভাবে শাটডাউন করার আগে মাত্র দুটি রিকোয়েস্ট গ্রহণ করে।
এগোনোর সময় একটি বিষয় লক্ষ্য করার মতো: এই পরিবর্তনগুলোর কোনোটিই সেই কোডের অংশকে প্রভাবিত করবে না যা ক্লোজার এক্সিকিউট করার কাজ করে, তাই আমরা যদি একটি async runtime
-এর জন্য থ্রেড পুল ব্যবহার করতাম, তাহলেও সবকিছু একই থাকত।
ThreadPool
-এর উপর Drop
ট্রেইট ইমপ্লিমেন্ট করা
চলুন আমাদের থ্রেড পুলের উপর Drop
ইমপ্লিমেন্ট করার মাধ্যমে শুরু করি। যখন পুলটি ড্রপ করা হবে, আমাদের সমস্ত থ্রেড join
করা উচিত যাতে তারা তাদের কাজ শেষ করতে পারে। লিস্টিং ২১-২২-এ Drop
ইমপ্লিমেন্টেশনের একটি প্রথম প্রচেষ্টা দেখানো হয়েছে; এই কোডটি এখনও ঠিকভাবে কাজ করবে না।
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
প্রথমে আমরা থ্রেড পুলের প্রতিটি workers
-এর মধ্যে দিয়ে লুপ করি। আমরা এর জন্য &mut
ব্যবহার করি কারণ self
একটি মিউটেবল রেফারেন্স এবং আমাদের worker
-কেও মিউটেট করতে হবে। প্রতিটি worker
-এর জন্য, আমরা একটি বার্তা প্রিন্ট করি যে এই নির্দিষ্ট Worker
ইনস্ট্যান্সটি শাট ডাউন হচ্ছে, এবং তারপর আমরা সেই Worker
ইনস্ট্যান্সের থ্রেডে join
কল করি। যদি join
কল ব্যর্থ হয়, আমরা Rust-কে প্যানিক করতে এবং একটি অসুন্দর শাটডাউনে যেতে unwrap
ব্যবহার করি।
এই কোডটি কম্পাইল করার সময় আমরা যে এররটি পাই তা এখানে দেওয়া হলো:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
--> src/lib.rs:52:13
|
52 | worker.thread.join().unwrap();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
|
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
--> /rustc/4eb161250e340c8f48f66e2b929ef4a5bed7c181/library/std/src/thread/mod.rs:1876:17
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error
এররটি আমাদের বলছে যে আমরা join
কল করতে পারছি না কারণ আমাদের প্রতিটি worker
-এর শুধুমাত্র একটি মিউটেবল borrow আছে এবং join
তার আর্গুমেন্টের মালিকানা নিয়ে নেয়। এই সমস্যাটি সমাধান করার জন্য, আমাদের thread
টিকে Worker
ইনস্ট্যান্স থেকে মুভ করতে হবে যা thread
এর মালিক, যাতে join
থ্রেডটিকে কনজিউম করতে পারে। এটি করার একটি উপায় হলো লিস্টিং ১৮-১৫ তে আমরা যে পদ্ধতিটি নিয়েছিলাম সেটি গ্রহণ করা। যদি Worker
একটি Option<thread::JoinHandle<()>>
ধারণ করত, আমরা Option
-এর উপর take
মেথড কল করতে পারতাম যাতে ভ্যালুটি Some
ভ্যারিয়েন্ট থেকে মুভ করে তার জায়গায় একটি None
ভ্যারিয়েন্ট রেখে দেওয়া যায়। অন্য কথায়, একটি চলমান Worker
-এর thread
-এ একটি Some
ভ্যারিয়েন্ট থাকত এবং যখন আমরা একটি Worker
-কে পরিচ্ছন্ন করতে চাইতাম, তখন আমরা Some
-কে None
দিয়ে প্রতিস্থাপন করতাম যাতে Worker
-এর চালানোর জন্য কোনো থ্রেড না থাকে।
যাইহোক, এই পরিস্থিতিটি শুধুমাত্র Worker
ড্রপ করার সময়ই আসত। এর বিনিময়ে, আমাদের worker.thread
অ্যাক্সেস করার সময় সব জায়গায় একটি Option<thread::JoinHandle<()>>
নিয়ে কাজ করতে হতো। ইডিওম্যাটিক Rust Option
অনেক ব্যবহার করে, কিন্তু যখন আপনি নিজেকে এমন কিছুকে Option
-এ র্যাপ করতে দেখেন যা আপনি জানেন সবসময় উপস্থিত থাকবে, শুধুমাত্র এই ধরনের একটি workaround হিসেবে, তখন আপনার কোডকে আরও পরিষ্কার এবং কম এরর-প্রোন করতে বিকল্প পদ্ধতির সন্ধান করা একটি ভাল ধারণা।
এই ক্ষেত্রে, একটি ভালো বিকল্প বিদ্যমান: Vec::drain
মেথড। এটি একটি রেঞ্জ প্যারামিটার গ্রহণ করে যা ভেক্টর থেকে কোন আইটেমগুলো সরাতে হবে তা নির্দিষ্ট করে এবং সেই আইটেমগুলোর একটি ইটারেটর রিটার্ন করে। ..
রেঞ্জ সিনট্যাক্স পাস করলে ভেক্টর থেকে প্রতিটি ভ্যালু সরিয়ে দেওয়া হবে।
তাই আমাদের ThreadPool
drop
ইমপ্লিমেন্টেশনটি এভাবে আপডেট করতে হবে:
#![allow(unused)] fn main() { use std::{ sync::{Arc, Mutex, mpsc}, thread, }; pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } type Job = Box<dyn FnOnce() + Send + 'static>; impl ThreadPool { /// Create a new ThreadPool. /// /// The size is the number of threads in the pool. /// /// # Panics /// /// The `new` function will panic if the size is zero. pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { for worker in self.workers.drain(..) { println!("Shutting down worker {}", worker.id); worker.thread.join().unwrap(); } } } struct Worker { id: usize, thread: thread::JoinHandle<()>, } impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || { loop { let job = receiver.lock().unwrap().recv().unwrap(); println!("Worker {id} got a job; executing."); job(); } }); Worker { id, thread } } } }
এটি কম্পাইলার এরর সমাধান করে এবং আমাদের কোডে অন্য কোনো পরিবর্তনের প্রয়োজন হয় না। লক্ষ্য করুন যে, যেহেতু প্যানিক করার সময় ড্রপ কল করা যেতে পারে, তাই unwrap
-ও প্যানিক করতে পারে এবং একটি ডাবল প্যানিক ঘটাতে পারে, যা প্রোগ্রামটি অবিলম্বে ক্র্যাশ করে এবং চলমান যেকোনো পরিচ্ছন্নতার কাজ শেষ করে দেয়। এটি একটি উদাহরণ প্রোগ্রামের জন্য ঠিক আছে, কিন্তু প্রোডাকশন কোডের জন্য সুপারিশ করা হয় না।
থ্রেডগুলোকে জব শোনা বন্ধ করার জন্য সংকেত দেওয়া
আমরা যে সমস্ত পরিবর্তন করেছি, তাতে আমাদের কোড কোনো ওয়ার্নিং ছাড়াই কম্পাইল হচ্ছে। যাইহোক, খারাপ খবর হলো এই কোডটি এখনও আমাদের কাঙ্ক্ষিত উপায়ে কাজ করছে না। মূল বিষয় হলো Worker
ইনস্ট্যান্সের থ্রেড দ্বারা চালিত ক্লোজারগুলোর লজিক: এই মুহূর্তে, আমরা join
কল করছি, কিন্তু এটি থ্রেডগুলোকে শাট ডাউন করবে না, কারণ তারা জব খোঁজার জন্য চিরতরে loop
করতে থাকে। যদি আমরা আমাদের drop
-এর বর্তমান ইমপ্লিমেন্টেশন দিয়ে আমাদের ThreadPool
ড্রপ করার চেষ্টা করি, তাহলে প্রধান থ্রেডটি চিরতরে ব্লক হয়ে যাবে, প্রথম থ্রেডটি শেষ হওয়ার জন্য অপেক্ষা করতে থাকবে।
এই সমস্যাটি সমাধান করার জন্য, আমাদের ThreadPool
drop
ইমপ্লিমেন্টেশনে একটি পরিবর্তন এবং তারপর Worker
লুপে একটি পরিবর্তন প্রয়োজন হবে।
প্রথমে আমরা ThreadPool
drop
ইমপ্লিমেন্টেশন পরিবর্তন করে থ্রেডগুলো শেষ হওয়ার জন্য অপেক্ষা করার আগে স্পষ্টভাবে sender
ড্রপ করব। লিস্টিং ২১-২৩ ThreadPool
-এ sender
-কে স্পষ্টভাবে ড্রপ করার পরিবর্তনগুলো দেখায়। থ্রেডের মতো নয়, এখানে আমাদের sender
-কে ThreadPool
থেকে Option::take
দিয়ে মুভ করার জন্য একটি Option
ব্যবহার করতে হবে।
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
// --snip--
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
sender
ড্রপ করা চ্যানেলটি বন্ধ করে দেয়, যা নির্দেশ করে যে আর কোনো মেসেজ পাঠানো হবে না। যখন এটি ঘটে, তখন Worker
ইনস্ট্যান্সগুলো ইনফিনিট লুপে যে recv
কলগুলো করে, সেগুলি সব একটি এরর রিটার্ন করবে। লিস্টিং ২১-২৪-এ, আমরা Worker
লুপটি পরিবর্তন করি যাতে সেই ক্ষেত্রে লুপ থেকে সুন্দরভাবে প্রস্থান করা যায়, যার মানে হলো ThreadPool
drop
ইমপ্লিমেন্টেশন যখন তাদের উপর join
কল করবে তখন থ্রেডগুলো শেষ হয়ে যাবে।
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker { id, thread }
}
}
এই কোডটি বাস্তবে দেখার জন্য, চলুন main
পরিবর্তন করি যাতে সার্ভারটি সুন্দরভাবে শাট ডাউন করার আগে মাত্র দুটি রিকোয়েস্ট গ্রহণ করে, যেমনটি লিস্টিং ২১-২৫ এ দেখানো হয়েছে।
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
আপনি চাইবেন না যে একটি বাস্তব-বিশ্বের ওয়েব সার্ভার মাত্র দুটি রিকোয়েস্ট সার্ভ করার পর শাট ডাউন হয়ে যাক। এই কোডটি শুধু দেখাচ্ছে যে সুন্দরভাবে শাটডাউন এবং পরিচ্ছন্নতার প্রক্রিয়াটি সঠিকভাবে কাজ করছে।
take
মেথডটি Iterator
ট্রেইটে সংজ্ঞায়িত এবং এটি ইটারেশনকে সর্বোচ্চ প্রথম দুটি আইটেমে সীমাবদ্ধ করে। ThreadPool
main
-এর শেষে স্কোপের বাইরে চলে যাবে, এবং drop
ইমপ্লিমেন্টেশনটি চলবে।
cargo run
দিয়ে সার্ভারটি শুরু করুন, এবং তিনটি রিকোয়েস্ট করুন। তৃতীয় রিকোয়েস্টটি এরর দেওয়া উচিত, এবং আপনার টার্মিনালে আপনি এই ধরনের আউটপুট দেখতে পাবেন:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
আপনি হয়তো Worker
আইডি এবং প্রিন্ট করা মেসেজগুলোর একটি ভিন্ন ক্রম দেখতে পারেন। আমরা মেসেজগুলো থেকে দেখতে পাচ্ছি এই কোডটি কীভাবে কাজ করে: Worker
ইনস্ট্যান্স 0 এবং 3 প্রথম দুটি রিকোয়েস্ট পেয়েছে। দ্বিতীয় কানেকশনের পরে সার্ভার কানেকশন গ্রহণ করা বন্ধ করে দিয়েছে, এবং ThreadPool
-এর উপর Drop
ইমপ্লিমেন্টেশনটি Worker
3 তার জব শুরু করার আগেই চালানো শুরু হয়। sender
ড্রপ করা সমস্ত Worker
ইনস্ট্যান্সকে ডিসকানেক্ট করে এবং তাদের শাট ডাউন হতে বলে। Worker
ইনস্ট্যান্সগুলো প্রত্যেকেই ডিসকানেক্ট হওয়ার সময় একটি মেসেজ প্রিন্ট করে, এবং তারপর থ্রেড পুল প্রতিটি Worker
থ্রেড শেষ হওয়ার জন্য অপেক্ষা করতে join
কল করে।
এই নির্দিষ্ট এক্সিকিউশনের একটি আকর্ষণীয় দিক লক্ষ্য করুন: ThreadPool
sender
-কে ড্রপ করেছে, এবং কোনো Worker
এরর পাওয়ার আগেই, আমরা Worker
0 কে join
করার চেষ্টা করেছি। Worker
0 এখনও recv
থেকে কোনো এরর পায়নি, তাই প্রধান থ্রেডটি Worker
0 শেষ হওয়ার জন্য অপেক্ষা করতে ব্লক হয়ে গেছে। এই সময়ের মধ্যে, Worker
3 একটি জব পেয়েছে এবং তারপর সমস্ত থ্রেড একটি এরর পেয়েছে। যখন Worker
0 শেষ হয়েছে, প্রধান থ্রেডটি বাকি Worker
ইনস্ট্যান্সগুলো শেষ হওয়ার জন্য অপেক্ষা করেছে। সেই সময়ে, তারা সবাই তাদের লুপ থেকে বেরিয়ে গেছে এবং থেমে গেছে।
অভিনন্দন! আমরা এখন আমাদের প্রজেক্ট সম্পন্ন করেছি; আমাদের একটি বেসিক ওয়েব সার্ভার আছে যা অ্যাসিঙ্ক্রোনাসভাবে সাড়া দেওয়ার জন্য একটি থ্রেড পুল ব্যবহার করে। আমরা সার্ভারের একটি সুন্দর শাটডাউন সম্পাদন করতে সক্ষম, যা পুলের সমস্ত থ্রেড পরিচ্ছন্ন করে।
রেফারেন্সের জন্য এখানে সম্পূর্ণ কোড দেওয়া হলো:
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
আমরা এখানে আরও অনেক কিছু করতে পারি! আপনি যদি এই প্রজেক্টটি আরও উন্নত করতে চান, তবে এখানে কিছু ধারণা দেওয়া হলো:
ThreadPool
এবং এর পাবলিক মেথডগুলিতে আরও ডকুমেন্টেশন যোগ করুন।- লাইব্রেরির কার্যকারিতার জন্য টেস্ট যোগ করুন।
unwrap
কলগুলোকে আরও শক্তিশালী এরর হ্যান্ডলিং দিয়ে পরিবর্তন করুন।- ওয়েব রিকোয়েস্ট সার্ভ করা ছাড়া অন্য কোনো কাজ সম্পাদন করার জন্য
ThreadPool
ব্যবহার করুন। - crates.io তে একটি থ্রেড পুল ক্রেট খুঁজুন এবং সেই ক্রেট ব্যবহার করে একটি অনুরূপ ওয়েব সার্ভার ইমপ্লিমেন্ট করুন। তারপর এর API এবং শক্তিশালীতার সাথে আমাদের ইমপ্লিমেন্ট করা থ্রেড পুলের তুলনা করুন।
সারাংশ
চমৎকার! আপনি বইয়ের শেষ পর্যন্ত পৌঁছে গেছেন! Rust-এর এই সফরে আমাদের সাথে যোগ দেওয়ার জন্য আমরা আপনাকে ধন্যবাদ জানাতে চাই। আপনি এখন আপনার নিজের Rust প্রজেক্ট ইমপ্লিমেন্ট করতে এবং অন্য লোকের প্রজেক্টে সাহায্য করতে প্রস্তুত। মনে রাখবেন যে অন্যান্য Rustaceans-দের একটি স্বাগত জানানো কমিউনিটি রয়েছে যারা আপনার Rust যাত্রায় যেকোনো চ্যালেঞ্জের সম্মুখীন হলে আপনাকে সাহায্য করতে ভালোবাসবে।
পরিশিষ্ট
নিম্নলিখিত বিভাগগুলিতে এমন রেফারেন্স উপকরণ রয়েছে যা আপনার Rust যাত্রাপথে কাজে লাগতে পারে।****
পরিশিষ্ট ক: কীওয়ার্ড
নিচের তালিকায় এমন সব কীওয়ার্ড রয়েছে যা Rust ল্যাঙ্গুয়েজ দ্বারা বর্তমান বা ভবিষ্যতের ব্যবহারের জন্য সংরক্ষিত। তাই, এগুলো আইডেন্টিফায়ার (identifier) হিসেবে ব্যবহার করা যাবে না (ব্যতিক্রম হিসেবে 'র আইডেন্টিফায়ার' ব্যবহার করা যেতে পারে, যা আমরা 'র আইডেন্টিফায়ার' বিভাগে আলোচনা করব)। আইডেন্টিফায়ার হলো ফাংশন, ভেরিয়েবল, প্যারামিটার, স্ট্রাকট ফিল্ড, মডিউল, ক্রেট, কনস্ট্যান্ট, ম্যাক্রো, স্ট্যাটিক ভ্যালু, অ্যাট্রিবিউট, টাইপ, ট্রেইট বা লাইফটাইমের নাম।
বর্তমানে ব্যবহৃত কীওয়ার্ড
নিচে বর্তমানে ব্যবহৃত কীওয়ার্ডগুলোর একটি তালিকা এবং তাদের কার্যকারিতার বর্ণনা দেওয়া হলো।
as
- প্রিমিটিভ কাস্টিং করতে, কোনো আইটেম ধারণকারী নির্দিষ্ট ট্রেইটকে দ্ব্যর্থহীন করতে, অথবাuse
স্টেটমেন্টে আইটেমগুলোর নতুন নাম দিতে ব্যবহৃত হয়।async
- বর্তমান থ্রেডকে ব্লক না করে একটিFuture
রিটার্ন করে।await
- একটিFuture
-এর ফলাফল প্রস্তুত না হওয়া পর্যন্ত এক্সিকিউশন স্থগিত রাখে।break
- একটি লুপ থেকে অবিলম্বে বেরিয়ে যায়।const
- কনস্ট্যান্ট আইটেম বা কনস্ট্যান্ট র পয়েন্টার ডিফাইন করে।continue
- লুপের পরবর্তী ইটারেশনে চলে যায়।crate
- একটি মডিউল পাথে, ক্রেটের রুটকে বোঝায়।dyn
- একটি ট্রেইট অবজেক্টে ডাইনামিক ডিসপ্যাচ করে।else
-if
এবংif let
কন্ট্রোল ফ্লো কনস্ট্রাক্টের ফলব্যাক।enum
- একটি enumeration ডিফাইন করে।extern
- একটি এক্সটার্নাল ফাংশন বা ভেরিয়েবল লিঙ্ক করে।false
- বুলিয়ানfalse
লিটারাল।fn
- একটি ফাংশন বা ফাংশন পয়েন্টার টাইপ ডিফাইন করে।for
- একটি ইটারেটরের আইটেমগুলোর উপর লুপ চালানো, একটি ট্রেইট ইমপ্লিমেন্ট করা, বা একটি হায়ার-র্যাঙ্কড লাইফটাইম নির্দিষ্ট করার জন্য ব্যবহৃত হয়।if
- একটি শর্তাধীন এক্সপ্রেশনের ফলাফলের উপর ভিত্তি করে ব্রাঞ্চ তৈরি করে।impl
- ইনহেরেন্ট বা ট্রেইট কার্যকারিতা ইমপ্লিমেন্ট করে।in
-for
লুপ সিনট্যাক্সের অংশ।let
- একটি ভেরিয়েবল বাইন্ড করে।loop
- শর্তহীনভাবে লুপ চালায়।match
- একটি ভ্যালুকে প্যাটার্নের সাথে ম্যাচ করে।mod
- একটি মডিউল ডিফাইন করে।move
- একটি ক্লোজারকে তার সমস্ত ক্যাপচারের মালিকানা নিতে বাধ্য করে।mut
- রেফারেন্স, র পয়েন্টার, বা প্যাটার্ন বাইন্ডিং-এ মিউটেবিলিটি বোঝায়।pub
- স্ট্রাকট ফিল্ড,impl
ব্লক, বা মডিউলে পাবলিক ভিজিবিলিটি বোঝায়।ref
- রেফারেন্স দ্বারা বাইন্ড করে।return
- ফাংশন থেকে রিটার্ন করে।Self
- যে টাইপটি আমরা ডিফাইন বা ইমপ্লিমেন্ট করছি তার জন্য একটি টাইপ এলিয়াস।self
- মেথডের সাবজেক্ট বা বর্তমান মডিউল।static
- গ্লোবাল ভেরিয়েবল বা পুরো প্রোগ্রাম এক্সিকিউশন পর্যন্ত স্থায়ী লাইফটাইম।struct
- একটি স্ট্রাকচার ডিফাইন করে।super
- বর্তমান মডিউলের প্যারেন্ট মডিউল।trait
- একটি ট্রেইট ডিফাইন করে।true
- বুলিয়ানtrue
লিটারাল।type
- একটি টাইপ এলিয়াস বা অ্যাসোসিয়েটেড টাইপ ডিফাইন করে।union
- একটি union ডিফাইন করে; এটি শুধুমাত্র একটি union ডিক্লারেশনে ব্যবহৃত হলেই কীওয়ার্ড হিসেবে গণ্য হয়।unsafe
- আনসেফ কোড, ফাংশন, ট্রেইট, বা ইমপ্লিমেন্টেশন বোঝায়।use
- স্কোপে সিম্বল নিয়ে আসে; জেনেরিক এবং লাইফটাইম বাউন্ডের জন্য সুনির্দিষ্ট ক্যাপচার নির্দিষ্ট করে।where
- এমন ক্লজ বোঝায় যা একটি টাইপকে সীমাবদ্ধ করে।while
- একটি এক্সপ্রেশনের ফলাফলের উপর ভিত্তি করে শর্তসাপেক্ষে লুপ চালায়।
ভবিষ্যৎ ব্যবহারের জন্য সংরক্ষিত কীওয়ার্ড
নিচের কীওয়ার্ডগুলোর এখনো কোনো কার্যকারিতা নেই, কিন্তু Rust ভবিষ্যতে ব্যবহারের সম্ভাবনার জন্য এগুলোকে সংরক্ষিত রেখেছে।
abstract
become
box
do
final
gen
macro
override
priv
try
typeof
unsized
virtual
yield
র আইডেন্টিফায়ার
র আইডেন্টিফায়ার হলো এমন একটি সিনট্যাক্স যা আপনাকে সেইসব জায়গায় কীওয়ার্ড ব্যবহার করতে দেয় যেখানে সাধারণত তা অনুমোদিত নয়। আপনি একটি কীওয়ার্ডের আগে r#
উপসর্গ যোগ করে একটি র আইডেন্টিফায়ার ব্যবহার করতে পারেন।
উদাহরণস্বরূপ, match
একটি কীওয়ার্ড। আপনি যদি match
নামটি ব্যবহার করে নিচের ফাংশনটি কম্পাইল করার চেষ্টা করেন:
ফাইলের নাম: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
আপনি এই এররটি পাবেন:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
এররটি দেখাচ্ছে যে আপনি match
কীওয়ার্ডটি ফাংশন আইডেন্টিফায়ার হিসেবে ব্যবহার করতে পারবেন না। match
কে ফাংশনের নাম হিসেবে ব্যবহার করতে হলে আপনাকে র আইডেন্টিফায়ার সিনট্যাক্স ব্যবহার করতে হবে, যেমন:
ফাইলের নাম: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool { haystack.contains(needle) } fn main() { assert!(r#match("foo", "foobar")); }
এই কোডটি কোনো এরর ছাড়াই কম্পাইল হবে। লক্ষ্য করুন, ফাংশনের সংজ্ঞায় এবং main
-এ যেখানে ফাংশনটি কল করা হয়েছে, উভয় স্থানেই ফাংশনের নামের আগে r#
উপসর্গটি রয়েছে।
র আইডেন্টিফায়ার আপনাকে আপনার পছন্দের যেকোনো শব্দকে আইডেন্টিফায়ার হিসেবে ব্যবহার করার অনুমতি দেয়, এমনকি যদি সেই শব্দটি একটি সংরক্ষিত কীওয়ার্ডও হয়। এটি আমাদের আইডেন্টিফায়ারের নাম বেছে নেওয়ার ক্ষেত্রে আরও স্বাধীনতা দেয়, এবং এমন প্রোগ্রামের সাথে ইন্টিগ্রেট করতে সাহায্য করে যা এমন ভাষায় লেখা যেখানে এই শব্দগুলো কীওয়ার্ড নয়। এছাড়াও, র আইডেন্টিফায়ার আপনাকে আপনার ক্রেটের চেয়ে ভিন্ন Rust এডিশনে লেখা লাইব্রেরি ব্যবহার করার সুযোগ দেয়। উদাহরণস্বরূপ, try
2015 এডিশনে কোনো কীওয়ার্ড নয়, কিন্তু 2018, 2021 এবং 2024 এডিশনে এটি একটি কীওয়ার্ড। আপনি যদি 2015 এডিশন ব্যবহার করে লেখা একটি লাইব্রেরির উপর নির্ভর করেন এবং সেখানে একটি try
ফাংশন থাকে, তবে পরবর্তী এডিশনগুলোতে আপনার কোড থেকে সেই ফাংশনটি কল করার জন্য আপনাকে র আইডেন্টিফায়ার সিনট্যাক্স, এক্ষেত্রে r#try
, ব্যবহার করতে হবে। এডিশন সম্পর্কে আরও তথ্যের জন্য পরিশিষ্ট ঙ দেখুন।
পরিশিষ্ট খ: অপারেটর এবং প্রতীক
এই পরিশিষ্টে Rust-এর সিনট্যাক্সের একটি শব্দকোষ রয়েছে, যার মধ্যে অপারেটর এবং অন্যান্য প্রতীকগুলো অন্তর্ভুক্ত যা নিজে থেকেই অথবা পাথ, জেনেরিক, ট্রেইট বাউন্ড, ম্যাক্রো, অ্যাট্রিবিউট, কমেন্ট, টাপল এবং ব্র্যাকেটের প্রসঙ্গে উপস্থিত হয়।
অপারেটর
সারণী খ-১ এ Rust-এর অপারেটর, প্রসঙ্গে অপারেটরটি কীভাবে প্রদর্শিত হবে তার একটি উদাহরণ, একটি সংক্ষিপ্ত ব্যাখ্যা এবং সেই অপারেটরটি ওভারলোডযোগ্য কিনা তা রয়েছে। যদি কোনো অপারেটর ওভারলোডযোগ্য হয়, তবে সেই অপারেটরটিকে ওভারলোড করার জন্য ব্যবহৃত প্রাসঙ্গিক ট্রেইটটি তালিকাভুক্ত করা হয়েছে।
সারণী খ-১: অপারেটর
অপারেটর | উদাহরণ | ব্যাখ্যা | ওভারলোডযোগ্য? |
---|---|---|---|
! | ident!(...) , ident!{...} , ident![...] | ম্যাক্রো এক্সপ্যানশন | |
! | !expr | বিটওয়াইজ বা লজিক্যাল কমপ্লিমেন্ট | Not |
!= | expr != expr | অসমতা তুলনা | PartialEq |
% | expr % expr | গাণিতিক ভাগশেষ | Rem |
%= | var %= expr | গাণিতিক ভাগশেষ এবং অ্যাসাইনমেন্ট | RemAssign |
& | &expr , &mut expr | ধার (Borrow) | |
& | &type , &mut type , &'a type , &'a mut type | ধার করা পয়েন্টার টাইপ | |
& | expr & expr | বিটওয়াইজ AND | BitAnd |
&= | var &= expr | বিটওয়াইজ AND এবং অ্যাসাইনমেন্ট | BitAndAssign |
&& | expr && expr | শর্ট-সার্কিটিং লজিক্যাল AND | |
* | expr * expr | গাণিতিক গুণ | Mul |
*= | var *= expr | গাণিতিক গুণ এবং অ্যাসাইনমেন্ট | MulAssign |
* | *expr | ডিরেফারেন্স | Deref |
* | *const type , *mut type | র পয়েন্টার | |
+ | trait + trait , 'a + trait | যৌগিক টাইপ কনস্ট্রেইন্ট | |
+ | expr + expr | গাণিতিক যোগ | Add |
+= | var += expr | গাণিতিক যোগ এবং অ্যাসাইনমেন্ট | AddAssign |
, | expr, expr | আর্গুমেন্ট এবং এলিমেন্ট বিভাজক | |
- | - expr | গাণিতিক নেগেশন | Neg |
- | expr - expr | গাণিতিক বিয়োগ | Sub |
-= | var -= expr | গাণিতিক বিয়োগ এবং অ্যাসাইনমেন্ট | SubAssign |
-> | fn(...) -> type , ` | ... | -> type` |
. | expr.ident | ফিল্ড অ্যাক্সেস | |
. | expr.ident(expr, ...) | মেথড কল | |
. | expr.0 , expr.1 , ইত্যাদি | টাপল ইনডেক্সিং | |
.. | .. , expr.. , ..expr , expr..expr | ডান-এক্সক্লুসিভ রেঞ্জ লিটারাল | PartialOrd |
..= | ..=expr , expr..=expr | ডান-ইনক্লুসিভ রেঞ্জ লিটারাল | PartialOrd |
.. | ..expr | স্ট্রাকট লিটারাল আপডেট সিনট্যাক্স | |
.. | variant(x, ..) , struct_type { x, .. } | “এবং বাকিটা” প্যাটার্ন বাইন্ডিং | |
... | expr...expr | (অপ্রচলিত, এর পরিবর্তে ..= ব্যবহার করুন) একটি প্যাটার্নে: ইনক্লুসিভ রেঞ্জ প্যাটার্ন | |
/ | expr / expr | গাণিতিক ভাগ | Div |
/= | var /= expr | গাণিতিক ভাগ এবং অ্যাসাইনমেন্ট | DivAssign |
: | pat: type , ident: type | কনস্ট্রেইন্ট | |
: | ident: expr | স্ট্রাকট ফিল্ড ইনিশিয়ালাইজার | |
: | 'a: loop {...} | লুপ লেবেল | |
; | expr; | স্টেটমেন্ট এবং আইটেম টার্মিনেটর | |
; | [...; len] | ফিক্সড-সাইজ অ্যারে সিনট্যাক্সের অংশ | |
<< | expr << expr | লেফট-শিফট | Shl |
<<= | var <<= expr | লেফট-শিফট এবং অ্যাসাইনমেন্ট | ShlAssign |
< | expr < expr | ছোট তুলনা | PartialOrd |
<= | expr <= expr | ছোট বা সমান তুলনা | PartialOrd |
= | var = expr , ident = type | অ্যাসাইনমেন্ট/সমতা | |
== | expr == expr | সমতা তুলনা | PartialEq |
=> | pat => expr | ম্যাচ আর্ম সিনট্যাক্সের অংশ | |
> | expr > expr | বড় তুলনা | PartialOrd |
>= | expr >= expr | বড় বা সমান তুলনা | PartialOrd |
>> | expr >> expr | রাইট-শিফট | Shr |
>>= | var >>= expr | রাইট-শিফট এবং অ্যাসাইনমেন্ট | ShrAssign |
@ | ident @ pat | প্যাটার্ন বাইন্ডিং | |
^ | expr ^ expr | বিটওয়াইজ এক্সক্লুসিভ OR | BitXor |
^= | var ^= expr | বিটওয়াইজ এক্সক্লুসিভ OR এবং অ্যাসাইনমেন্ট | BitXorAssign |
` | ` | `pat | pat` |
` | ` | `expr | expr` |
` | =` | `var | = expr` |
` | ` | `expr | |
? | expr? | এরর প্রোপাগেশন |
নন-অপারেটর প্রতীক
নিচের তালিকায় এমন সব প্রতীক রয়েছে যা অপারেটর হিসেবে কাজ করে না; অর্থাৎ, তারা কোনো ফাংশন বা মেথড কলের মতো আচরণ করে না।
সারণী খ-২ এ এমন প্রতীক দেখানো হয়েছে যা নিজে থেকেই উপস্থিত হয় এবং বিভিন্ন স্থানে বৈধ।
সারণী খ-২: স্বতন্ত্র সিনট্যাক্স
প্রতীক | ব্যাখ্যা |
---|---|
'ident | নামযুক্ত লাইফটাইম বা লুপ লেবেল |
u8 , i32 , f64 , usize , ইত্যাদি দ্বারা অবিলম্বে অনুসরণ করা সংখ্যা | নির্দিষ্ট টাইপের নিউমেরিক লিটারাল |
"..." | স্ট্রিং লিটারাল |
r"..." , r#"..."# , r##"..."## , ইত্যাদি | র স্ট্রিং লিটারাল, এস্কেপ ক্যারেক্টার প্রসেস করা হয় না |
b"..." | বাইট স্ট্রিং লিটারাল; স্ট্রিং এর পরিবর্তে বাইটের একটি অ্যারে তৈরি করে |
br"..." , br#"..."# , br##"..."## , ইত্যাদি | র বাইট স্ট্রিং লিটারাল, র এবং বাইট স্ট্রিং লিটারালের সংমিশ্রণ |
'...' | ক্যারেক্টার লিটারাল |
b'...' | ASCII বাইট লিটারাল |
` | ... |
! | ডাইভার্জিং ফাংশনের জন্য সর্বদা খালি বটম টাইপ |
_ | "উপেক্ষিত" প্যাটার্ন বাইন্ডিং; ইন্টিজার লিটারালকে পঠনযোগ্য করতেও ব্যবহৃত হয় |
সারণী খ-৩ এ এমন প্রতীক দেখানো হয়েছে যা মডিউল হায়ারার্কির মাধ্যমে একটি আইটেমের পাথের প্রসঙ্গে উপস্থিত হয়।
সারণী খ-৩: পাথ-সম্পর্কিত সিনট্যাক্স
প্রতীক | ব্যাখ্যা |
---|---|
ident::ident | নেমস্পেস পাথ |
::path | extern prelude-এর সাপেক্ষে পাথ, যেখানে অন্য সব ক্রেট রুট করা হয় (অর্থাৎ, ক্রেটের নামসহ একটি স্পষ্টভাবে অ্যাবসোলিউট পাথ) |
self::path | বর্তমান মডিউলের সাপেক্ষে পাথ (অর্থাৎ, একটি স্পষ্টভাবে রিলেটিভ পাথ) |
super::path | বর্তমান মডিউলের প্যারেন্টের সাপেক্ষে পাথ |
type::ident , <type as trait>::ident | অ্যাসোসিয়েটেড কনস্ট্যান্ট, ফাংশন, এবং টাইপ |
<type>::... | এমন একটি টাইপের জন্য অ্যাসোসিয়েটেড আইটেম যা সরাসরি নাম দেওয়া যায় না (যেমন, <&T>::... , <[T]>::... , ইত্যাদি) |
trait::method(...) | মেথড কলকে দ্ব্যর্থহীন করা, যে ট্রেইটটি এটি ডিফাইন করে তার নাম দিয়ে |
type::method(...) | মেথড কলকে দ্ব্যর্থহীন করা, যে টাইপের জন্য এটি ডিফাইন করা হয়েছে তার নাম দিয়ে |
<type as trait>::method(...) | ট্রেইট এবং টাইপের নাম দিয়ে মেথড কলকে দ্ব্যর্থহীন করা |
সারণী খ-৪ এ এমন প্রতীক দেখানো হয়েছে যা জেনেরিক টাইপ প্যারামিটার ব্যবহারের প্রসঙ্গে উপস্থিত হয়।
সারণী খ-৪: জেনেরিক
প্রতীক | ব্যাখ্যা |
---|---|
path<...> | একটি টাইপে জেনেরিক টাইপের জন্য প্যারামিটার নির্দিষ্ট করে (যেমন, Vec<u8> ) |
path::<...> , method::<...> | একটি এক্সপ্রেশনে জেনেরিক টাইপ, ফাংশন, বা মেথডের জন্য প্যারামিটার নির্দিষ্ট করে; প্রায়শই টার্বোফিশ হিসাবে উল্লেখ করা হয় (যেমন, "42".parse::<i32>() ) |
fn ident<...> ... | জেনেরিক ফাংশন ডিফাইন করে |
struct ident<...> ... | জেনেরিক স্ট্রাকচার ডিফাইন করে |
enum ident<...> ... | জেনেরিক ইনুমারেশন ডিফাইন করে |
impl<...> ... | জেনেরিক ইমপ্লিমেন্টেশন ডিফাইন করে |
for<...> type | হায়ার-র্যাঙ্কড লাইফটাইম বাউন্ড |
type<ident=type> | একটি জেনেরিক টাইপ যেখানে এক বা একাধিক অ্যাসোসিয়েটেড টাইপের নির্দিষ্ট অ্যাসাইনমেন্ট থাকে (যেমন, Iterator<Item=T> ) |
সারণী খ-৫ এ এমন প্রতীক দেখানো হয়েছে যা জেনেরিক টাইপ প্যারামিটারকে ট্রেইট বাউন্ড দিয়ে সীমাবদ্ধ করার প্রসঙ্গে উপস্থিত হয়।
সারণী খ-৫: ট্রেইট বাউন্ড কনস্ট্রেইন্ট
প্রতীক | ব্যাখ্যা |
---|---|
T: U | জেনেরিক প্যারামিটার T , U ইমপ্লিমেন্ট করে এমন টাইপগুলিতে সীমাবদ্ধ |
T: 'a | জেনেরিক টাইপ T অবশ্যই 'a লাইফটাইমকে আউটলাইভ করবে (অর্থাৎ টাইপটিতে ট্রানজিটিভভাবে 'a এর চেয়ে ছোট লাইফটাইম সহ কোনো রেফারেন্স থাকতে পারে না) |
T: 'static | জেনেরিক টাইপ T -তে 'static ছাড়া অন্য কোনো ধার করা রেফারেন্স নেই |
'b: 'a | জেনেরিক লাইফটাইম 'b অবশ্যই 'a লাইফটাইমকে আউটলাইভ করবে |
T: ?Sized | জেনেরিক টাইপ প্যারামিটারকে ডাইনামিক্যালি সাইজড টাইপ হতে অনুমতি দেয় |
'a + trait , trait + trait | যৌগিক টাইপ কনস্ট্রেইন্ট |
সারণী খ-৬ এ এমন প্রতীক দেখানো হয়েছে যা ম্যাক্রো কল বা ডিফাইন করার এবং একটি আইটেমের উপর অ্যাট্রিবিউট নির্দিষ্ট করার প্রসঙ্গে উপস্থিত হয়।
সারণী খ-৬: ম্যাক্রো এবং অ্যাট্রিবিউট
প্রতীক | ব্যাখ্যা |
---|---|
#[meta] | আউটার অ্যাট্রিবিউট |
#![meta] | ইনার অ্যাট্রিবিউট |
$ident | ম্যাক্রো সাবস্টিটিউশন |
$ident:kind | ম্যাক্রো মেটাভেরিয়েবল |
$(...)... | ম্যাক্রো রিপিটিশন |
ident!(...) , ident!{...} , ident![...] | ম্যাক্রো ইনভোকেশন |
সারণী খ-৭ এ এমন প্রতীক দেখানো হয়েছে যা কমেন্ট তৈরি করে।
সারণী খ-৭: কমেন্ট
প্রতীক | ব্যাখ্যা |
---|---|
// | লাইন কমেন্ট |
//! | ইনার লাইন ডক কমেন্ট |
/// | আউটার লাইন ডক কমেন্ট |
/*...*/ | ব্লক কমেন্ট |
/*!...*/ | ইনার ব্লক ডক কমেন্ট |
/**...*/ | আউটার ব্লক ডক কমেন্ট |
সারণী খ-৮ এ সেইসব প্রসঙ্গ দেখানো হয়েছে যেখানে প্রথম বন্ধনী (parentheses) ব্যবহৃত হয়।
সারণী খ-৮: প্রথম বন্ধনী
প্রতীক | ব্যাখ্যা |
---|---|
() | খালি টাপল (একে ইউনিটও বলা হয়), লিটারাল এবং টাইপ উভয়ই |
(expr) | প্যারেন্থেসাইজড এক্সপ্রেশন |
(expr,) | একক-উপাদান টাপল এক্সপ্রেশন |
(type,) | একক-উপাদান টাপল টাইপ |
(expr, ...) | টাপল এক্সপ্রেশন |
(type, ...) | টাপল টাইপ |
expr(expr, ...) | ফাংশন কল এক্সপ্রেশন; টাপল struct এবং টাপল enum ভ্যারিয়েন্ট ইনিশিয়ালাইজ করতেও ব্যবহৃত হয় |
সারণী খ-৯ এ সেইসব প্রসঙ্গ দেখানো হয়েছে যেখানে কোঁকড়া বন্ধনী (curly braces) ব্যবহৃত হয়।
সারণী খ-৯: কোঁকড়া বন্ধনী
প্রসঙ্গ | ব্যাখ্যা |
---|---|
{...} | ব্লক এক্সপ্রেশন |
Type {...} | স্ট্রাকট লিটারাল |
সারণী খ-১০ এ সেইসব প্রসঙ্গ দেখানো হয়েছে যেখানে বর্গাকার বন্ধনী (square brackets) ব্যবহৃত হয়।
সারণী খ-১০: বর্গাকার বন্ধনী
প্রসঙ্গ | ব্যাখ্যা |
---|---|
[...] | অ্যারে লিটারাল |
[expr; len] | expr এর len সংখ্যক কপি সহ অ্যারে লিটারাল |
[type; len] | type এর len সংখ্যক ইনস্ট্যান্স সহ অ্যারে টাইপ |
expr[expr] | কালেকশন ইন্ডেক্সিং। ওভারলোডযোগ্য (Index , IndexMut ) |
expr[..] , expr[a..] , expr[..b] , expr[a..b] | কালেকশন ইন্ডেক্সিং যা কালেকশন স্লাইসিং এর মতো কাজ করে, "ইনডেক্স" হিসেবে Range , RangeFrom , RangeTo , বা RangeFull ব্যবহার করে |
পরিশিষ্ট গ: ডিরাইভেবল ট্রেইট
বইয়ের বিভিন্ন জায়গায় আমরা derive
অ্যাট্রিবিউট নিয়ে আলোচনা করেছি, যা আপনি একটি struct
বা enum
ডেফিনিশনে প্রয়োগ করতে পারেন। derive
অ্যাট্রিবিউট এমন কোড জেনারেট করে যা আপনার derive
সিনট্যাক্স দিয়ে অ্যানোটেট করা টাইপের উপর একটি ট্রেইটের ডিফল্ট ইমপ্লিমেন্টেশন তৈরি করে।
এই পরিশিষ্টে, আমরা স্ট্যান্ডার্ড লাইব্রেরির সেই সমস্ত ট্রেইটের একটি রেফারেন্স প্রদান করছি যা আপনি derive
এর সাথে ব্যবহার করতে পারেন। প্রতিটি বিভাগে আলোচনা করা হয়েছে:
- এই ট্রেইট ডিরাইভ করলে কোন অপারেটর এবং মেথডগুলো ব্যবহার করা যাবে
derive
দ্বারা প্রদত্ত ট্রেইটের ইমপ্লিমেন্টেশন কী করে- ট্রেইট ইমপ্লিমেন্ট করা টাইপটি সম্পর্কে কী বোঝায়
- কোন শর্তে আপনি ট্রেইটটি ইমপ্লিমেন্ট করতে পারবেন বা পারবেন না
- যেসব অপারেশনের জন্য এই ট্রেইট প্রয়োজন তার উদাহরণ
আপনি যদি derive
অ্যাট্রিবিউট দ্বারা প্রদত্ত আচরণের থেকে ভিন্ন আচরণ চান, তবে কীভাবে সেগুলো ম্যানুয়ালি ইমপ্লিমেন্ট করতে হয় তার বিস্তারিত জানার জন্য প্রতিটি ট্রেইটের জন্য স্ট্যান্ডার্ড লাইব্রেরি ডকুমেন্টেশন দেখুন।
এখানে তালিকাভুক্ত ট্রেইটগুলোই স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সংজ্ঞায়িত একমাত্র ট্রেইট যা derive
ব্যবহার করে আপনার টাইপের উপর ইমপ্লিমেন্ট করা যেতে পারে। স্ট্যান্ডার্ড লাইব্রেরিতে সংজ্ঞায়িত অন্যান্য ট্রেইটগুলোর কোনো সংবেদনশীল ডিফল্ট আচরণ নেই, তাই আপনি কী অর্জন করার চেষ্টা করছেন তার উপর ভিত্তি করে সেগুলোকে আপনার নিজের মতো করে ইমপ্লিমেন্ট করতে হবে।
এমন একটি ট্রেইটের উদাহরণ যা ডিরাইভ করা যায় না তা হলো Display
, যা এন্ড-ইউজারদের জন্য ফরম্যাটিং পরিচালনা করে। আপনার সবসময় একজন এন্ড-ইউজারের কাছে একটি টাইপ প্রদর্শনের সবচেয়ে উপযুক্ত উপায় কী তা বিবেচনা করা উচিত। টাইপের কোন অংশগুলো একজন এন্ড-ইউজার দেখতে পাবে? কোন অংশগুলো তাদের কাছে প্রাসঙ্গিক মনে হবে? ডেটার কোন ফরম্যাটটি তাদের জন্য সবচেয়ে প্রাসঙ্গিক হবে? Rust কম্পাইলারের এই অন্তর্দৃষ্টি নেই, তাই এটি আপনার জন্য উপযুক্ত ডিফল্ট আচরণ প্রদান করতে পারে না।
এই পরিশিষ্টে দেওয়া ডিরাইভেবল ট্রেইটের তালিকাটি সম্পূর্ণ নয়: লাইব্রেরিগুলো তাদের নিজস্ব ট্রেইটের জন্য derive
ইমপ্লিমেন্ট করতে পারে, যার ফলে আপনি derive
ব্যবহার করতে পারেন এমন ট্রেইটের তালিকাটি সত্যিই অফুরন্ত। derive
ইমপ্লিমেন্ট করার জন্য একটি প্রসিডিউরাল ম্যাক্রো ব্যবহার করতে হয়, যা অধ্যায় ২০-এর “ম্যাক্রো” বিভাগে আলোচনা করা হয়েছে।
প্রোগ্রামার আউটপুটের জন্য Debug
Debug
ট্রেইট ফরম্যাট স্ট্রিং-এ ডিবাগ ফরম্যাটিং সক্রিয় করে, যা আপনি {}
প্লেসহোল্ডারের মধ্যে :?
যোগ করে নির্দেশ করেন।
Debug
ট্রেইট আপনাকে ডিবাগিংয়ের উদ্দেশ্যে একটি টাইপের ইনস্ট্যান্স প্রিন্ট করার অনুমতি দেয়, যাতে আপনি এবং আপনার টাইপ ব্যবহারকারী অন্যান্য প্রোগ্রামাররা একটি প্রোগ্রামের এক্সিকিউশনের নির্দিষ্ট সময়ে একটি ইনস্ট্যান্স পরিদর্শন করতে পারেন।
Debug
ট্রেইটটি প্রয়োজন হয়, উদাহরণস্বরূপ, assert_eq!
ম্যাক্রো ব্যবহার করার সময়। এই ম্যাক্রোটি আর্গুমেন্ট হিসেবে দেওয়া ইনস্ট্যান্সগুলোর মান প্রিন্ট করে যদি সমতা যাচাই ব্যর্থ হয়, যাতে প্রোগ্রামাররা দেখতে পারেন কেন দুটি ইনস্ট্যান্স সমান ছিল না।
সমতা তুলনার জন্য PartialEq
এবং Eq
PartialEq
ট্রেইট আপনাকে সমতা পরীক্ষা করার জন্য একটি টাইপের ইনস্ট্যান্স তুলনা করার অনুমতি দেয় এবং ==
ও !=
অপারেটরগুলোর ব্যবহার সক্রিয় করে।
PartialEq
ডিরাইভ করলে eq
মেথড ইমপ্লিমেন্ট হয়। যখন PartialEq
struct-এর উপর ডিরাইভ করা হয়, তখন দুটি ইনস্ট্যান্স তখনই সমান হয় যদি সমস্ত ফিল্ড সমান হয়, এবং যদি কোনো ফিল্ড সমান না হয় তবে ইনস্ট্যান্সগুলো অসমান হয়। যখন enum-এর উপর ডিরাইভ করা হয়, তখন প্রতিটি ভ্যারিয়েন্ট নিজের সমান হয় এবং অন্য ভ্যারিয়েন্টগুলোর সমান হয় না।
PartialEq
ট্রেইটটি প্রয়োজন হয়, উদাহরণস্বরূপ, assert_eq!
ম্যাক্রো ব্যবহার করার সময়, যা সমতার জন্য একটি টাইপের দুটি ইনস্ট্যান্স তুলনা করতে সক্ষম হতে হবে।
Eq
ট্রেইটের কোনো মেথড নেই। এর উদ্দেশ্য হলো সংকেত দেওয়া যে অ্যানোটেটেড টাইপের প্রতিটি ভ্যালুর জন্য, ভ্যালুটি নিজের সমান। Eq
ট্রেইট শুধুমাত্র সেই টাইপগুলোতে প্রয়োগ করা যেতে পারে যেগুলো PartialEq
-ও ইমপ্লিমেন্ট করে, যদিও PartialEq
ইমপ্লিমেন্ট করে এমন সব টাইপ Eq
ইমপ্লিমেন্ট করতে পারে না। এর একটি উদাহরণ হলো ফ্লোটিং পয়েন্ট নাম্বার টাইপ: ফ্লোটিং পয়েন্ট নাম্বারের ইমপ্লিমেন্টেশন অনুযায়ী, নট-এ-নাম্বার (NaN
) ভ্যালুর দুটি ইনস্ট্যান্স একে অপরের সমান নয়।
Eq
কখন প্রয়োজন তার একটি উদাহরণ হলো HashMap<K, V>
-এর কী-এর জন্য, যাতে HashMap<K, V>
বলতে পারে দুটি কী একই কিনা।
ক্রম তুলনার জন্য PartialOrd
এবং Ord
PartialOrd
ট্রেইট আপনাকে সর্টিংয়ের উদ্দেশ্যে একটি টাইপের ইনস্ট্যান্স তুলনা করার অনুমতি দেয়। একটি টাইপ যা PartialOrd
ইমপ্লিমেন্ট করে তা <
, >
, <=
, এবং >=
অপারেটরগুলোর সাথে ব্যবহার করা যেতে পারে। আপনি PartialOrd
ট্রেইট শুধুমাত্র সেই টাইপগুলোতে প্রয়োগ করতে পারেন যেগুলো PartialEq
-ও ইমপ্লিমেন্ট করে।
PartialOrd
ডিরাইভ করলে partial_cmp
মেথড ইমপ্লিমেন্ট হয়, যা একটি Option<Ordering>
রিটার্ন করে যা None
হবে যখন প্রদত্ত ভ্যালুগুলো একটি ক্রম তৈরি করে না। এমন একটি ভ্যালুর উদাহরণ যা একটি ক্রম তৈরি করে না, যদিও সেই টাইপের বেশিরভাগ ভ্যালু তুলনা করা যায়, তা হলো নট-এ-নাম্বার (NaN
) ফ্লোটিং পয়েন্ট ভ্যালু। যেকোনো ফ্লোটিং-পয়েন্ট নাম্বার এবং NaN
ফ্লোটিং-পয়েন্ট ভ্যালু দিয়ে partial_cmp
কল করলে None
রিটার্ন হবে।
struct-এর উপর ডিরাইভ করা হলে, PartialOrd
struct ডেফিনিশনে ফিল্ডগুলো যে ক্রমে উপস্থিত হয় সেই ক্রমে প্রতিটি ফিল্ডের ভ্যালু তুলনা করে দুটি ইনস্ট্যান্স তুলনা করে। enum-এর উপর ডিরাইভ করা হলে, enum ডেফিনিশনে আগে ঘোষিত enum-এর ভ্যারিয়েন্টগুলো পরে তালিকাভুক্ত ভ্যারিয়েন্টগুলোর চেয়ে ছোট বলে বিবেচিত হয়।
PartialOrd
ট্রেইটটি প্রয়োজন হয়, উদাহরণস্বরূপ, rand
ক্রেটের gen_range
মেথডের জন্য যা একটি রেঞ্জ এক্সপ্রেশন দ্বারা নির্দিষ্ট করা রেঞ্জের মধ্যে একটি র্যান্ডম ভ্যালু তৈরি করে।
Ord
ট্রেইট আপনাকে জানতে দেয় যে অ্যানোটেটেড টাইপের যেকোনো দুটি ভ্যালুর জন্য, একটি বৈধ ক্রম বিদ্যমান থাকবে। Ord
ট্রেইট cmp
মেথড ইমপ্লিমেন্ট করে, যা একটি Ordering
রিটার্ন করে, Option<Ordering>
নয়, কারণ একটি বৈধ ক্রম সর্বদা সম্ভব হবে। আপনি Ord
ট্রেইট শুধুমাত্র সেই টাইপগুলোতে প্রয়োগ করতে পারেন যেগুলো PartialOrd
এবং Eq
(এবং Eq
-এর জন্য PartialEq
প্রয়োজন) উভয়ই ইমপ্লিমেন্ট করে। struct এবং enum-এর উপর ডিরাইভ করা হলে, cmp
PartialOrd
-এর সাথে partial_cmp
-এর ডিরাইভড ইমপ্লিমেন্টেশনের মতোই আচরণ করে।
Ord
কখন প্রয়োজন তার একটি উদাহরণ হলো BTreeSet<T>
-এ ভ্যালু সংরক্ষণ করার সময়, যা একটি ডেটা স্ট্রাকচার যা ভ্যালুগুলোর সর্ট অর্ডারের উপর ভিত্তি করে ডেটা সংরক্ষণ করে।
ভ্যালু ডুপ্লিকেট করার জন্য Clone
এবং Copy
Clone
ট্রেইট আপনাকে একটি ভ্যালুর একটি ডিপ কপি স্পষ্টভাবে তৈরি করার অনুমতি দেয়, এবং এই ডুপ্লিকেশন প্রক্রিয়ায় আর্বিট্রারি কোড চালানো এবং হিপ ডেটা কপি করা জড়িত থাকতে পারে। Clone
সম্পর্কে আরও তথ্যের জন্য অধ্যায় ৪-এর “ভেরিয়েবল এবং ডেটার সাথে ক্লোনের ইন্টারঅ্যাকশন” দেখুন।
Clone
ডিরাইভ করলে clone
মেথড ইমপ্লিমেন্ট হয়, যা পুরো টাইপের জন্য ইমপ্লিমেন্ট করা হলে, টাইপের প্রতিটি অংশের উপর clone
কল করে। এর মানে হলো Clone
ডিরাইভ করার জন্য টাইপের সমস্ত ফিল্ড বা ভ্যালুগুলোকেও অবশ্যই Clone
ইমপ্লিমেন্ট করতে হবে।
Clone
কখন প্রয়োজন তার একটি উদাহরণ হলো একটি স্লাইসের উপর to_vec
মেথড কল করার সময়। স্লাইসটি তার মধ্যে থাকা টাইপ ইনস্ট্যান্সগুলোর মালিক নয়, কিন্তু to_vec
থেকে রিটার্ন করা ভেক্টরকে তার ইনস্ট্যান্সগুলোর মালিক হতে হবে, তাই to_vec
প্রতিটি আইটেমের উপর clone
কল করে। সুতরাং স্লাইসে সংরক্ষিত টাইপটিকে অবশ্যই Clone
ইমপ্লিমেন্ট করতে হবে।
Copy
ট্রেইট আপনাকে শুধুমাত্র স্ট্যাকে সংরক্ষিত বিট কপি করে একটি ভ্যালু ডুপ্লিকেট করার অনুমতি দেয়; কোনো আর্বিট্রারি কোডের প্রয়োজন নেই। Copy
সম্পর্কে আরও তথ্যের জন্য অধ্যায় ৪-এর “শুধুমাত্র-স্ট্যাক ডেটা: কপি” দেখুন।
Copy
ট্রেইট কোনো মেথড ডিফাইন করে না যাতে প্রোগ্রামাররা সেই মেথডগুলো ওভারলোড করে এই ধারণা লঙ্ঘন করতে না পারে যে কোনো আর্বিট্রারি কোড চালানো হচ্ছে না। এইভাবে, সমস্ত প্রোগ্রামাররা ধরে নিতে পারেন যে একটি ভ্যালু কপি করা খুব দ্রুত হবে।
আপনি Copy
ডিরাইভ করতে পারেন যেকোনো টাইপের উপর যার সমস্ত অংশ Copy
ইমপ্লিমেন্ট করে। একটি টাইপ যা Copy
ইমপ্লিমেন্ট করে তাকে অবশ্যই Clone
-ও ইমপ্লিমেন্ট করতে হবে, কারণ একটি টাইপ যা Copy
ইমপ্লিমেন্ট করে তার একটি ট্রিভিয়াল Clone
ইমপ্লিমেন্টেশন থাকে যা Copy
-এর মতোই কাজ করে।
Copy
ট্রেইট খুব কমই প্রয়োজন হয়; যে টাইপগুলো Copy
ইমপ্লিমেন্ট করে তাদের জন্য অপটিমাইজেশন উপলব্ধ থাকে, যার মানে আপনাকে clone
কল করতে হয় না, যা কোডকে আরও সংক্ষিপ্ত করে তোলে।
Copy
দিয়ে যা কিছু সম্ভব তা আপনি Clone
দিয়েও অর্জন করতে পারেন, কিন্তু কোডটি ধীর হতে পারে বা কিছু জায়গায় clone
ব্যবহার করতে হতে পারে।
একটি ভ্যালুকে নির্দিষ্ট আকারের ভ্যালুতে ম্যাপ করার জন্য Hash
Hash
ট্রেইট আপনাকে যেকোনো আকারের একটি টাইপের ইনস্ট্যান্স নিয়ে সেই ইনস্ট্যান্সটিকে একটি হ্যাশ ফাংশন ব্যবহার করে একটি নির্দিষ্ট আকারের ভ্যালুতে ম্যাপ করার অনুমতি দেয়। Hash
ডিরাইভ করলে hash
মেথড ইমপ্লিমেন্ট হয়। hash
মেথডের ডিরাইভড ইমপ্লিমেন্টেশন টাইপের প্রতিটি অংশের উপর hash
কল করার ফলাফলকে একত্রিত করে, যার মানে Hash
ডিরাইভ করার জন্য সমস্ত ফিল্ড বা ভ্যালুগুলোকেও অবশ্যই Hash
ইমপ্লিমেন্ট করতে হবে।
Hash
কখন প্রয়োজন তার একটি উদাহরণ হলো ডেটা দক্ষতার সাথে সংরক্ষণ করার জন্য HashMap<K, V>
-এ কী সংরক্ষণ করার সময়।
ডিফল্ট ভ্যালুর জন্য Default
Default
ট্রেইট আপনাকে একটি টাইপের জন্য একটি ডিফল্ট ভ্যালু তৈরি করার অনুমতি দেয়। Default
ডিরাইভ করলে default
ফাংশন ইমপ্লিমেন্ট হয়। default
ফাংশনের ডিরাইভড ইমপ্লিমেন্টেশন টাইপের প্রতিটি অংশের উপর default
ফাংশন কল করে, যার মানে Default
ডিরাইভ করার জন্য টাইপের সমস্ত ফিল্ড বা ভ্যালুগুলোকেও অবশ্যই Default
ইমপ্লিমেন্ট করতে হবে।
Default::default
ফাংশনটি সাধারণত স্ট্রাকট আপডেট সিনট্যাক্সের সাথে একত্রে ব্যবহৃত হয় যা অধ্যায় ৫-এর “স্ট্রাকট আপডেট সিনট্যাক্স দিয়ে অন্যান্য ইনস্ট্যান্স থেকে ইনস্ট্যান্স তৈরি করা” এ আলোচনা করা হয়েছে। আপনি একটি struct-এর কয়েকটি ফিল্ড কাস্টমাইজ করতে পারেন এবং তারপর ..Default::default()
ব্যবহার করে বাকি ফিল্ডগুলোর জন্য একটি ডিফল্ট ভ্যালু সেট এবং ব্যবহার করতে পারেন।
Default
ট্রেইটটি প্রয়োজন হয় যখন আপনি Option<T>
ইনস্ট্যান্সের উপর unwrap_or_default
মেথড ব্যবহার করেন, উদাহরণস্বরূপ। যদি Option<T>
টি None
হয়, তবে unwrap_or_default
মেথডটি Option<T>
-এ সংরক্ষিত T
টাইপের জন্য Default::default
-এর ফলাফল রিটার্ন করবে।
পরিশিষ্ট ঘ - দরকারি ডেভেলপমেন্ট টুল
এই পরিশিষ্টে, আমরা Rust প্রজেক্ট দ্বারা সরবরাহ করা কিছু দরকারি ডেভেলপমেন্ট টুল নিয়ে আলোচনা করব। আমরা স্বয়ংক্রিয় ফরম্যাটিং, ওয়ার্নিংগুলো দ্রুত সমাধান করার উপায়, একটি লিন্টার এবং IDE-এর সাথে ইন্টিগ্রেশন নিয়ে আলোচনা করব।
rustfmt
দিয়ে স্বয়ংক্রিয় ফরম্যাটিং
rustfmt
টুলটি আপনার কোডকে কমিউনিটির কোড স্টাইল অনুযায়ী রিফরম্যাট করে। অনেক সহযোগী প্রজেক্টে rustfmt
ব্যবহার করা হয় যাতে Rust লেখার সময় কোন স্টাইল ব্যবহার করা হবে তা নিয়ে তর্ক-বিতর্ক এড়ানো যায়: সবাই এই টুলটি ব্যবহার করে তাদের কোড ফরম্যাট করে।
Rust ইনস্টলেশনের সাথে rustfmt
ডিফল্টভাবে অন্তর্ভুক্ত থাকে, তাই আপনার সিস্টেমে ইতোমধ্যে rustfmt
এবং cargo-fmt
প্রোগ্রামগুলো থাকা উচিত। এই দুটি কমান্ড rustc
এবং cargo
-এর মতোই, যেখানে rustfmt
আরও সূক্ষ্ম নিয়ন্ত্রণ দেয় এবং cargo-fmt
কার্গো ব্যবহারকারী একটি প্রজেক্টের কনভেনশনগুলো বোঝে। যেকোনো কার্গো প্রজেক্ট ফরম্যাট করতে, নিম্নলিখিতটি লিখুন:
$ cargo fmt
এই কমান্ডটি চালালে বর্তমান ক্রেটের সমস্ত Rust কোড রিফরম্যাট হয়ে যায়। এটি শুধুমাত্র কোডের স্টাইল পরিবর্তন করবে, কোডের সেমান্টিক্স (অর্থ) নয়। rustfmt
সম্পর্কে আরও তথ্যের জন্য, এর ডকুমেন্টেশন দেখুন।
rustfix
দিয়ে আপনার কোড ঠিক করুন
rustfix
টুলটি Rust ইনস্টলেশনের সাথে অন্তর্ভুক্ত থাকে এবং এটি কম্পাইলারের সেইসব ওয়ার্নিং স্বয়ংক্রিয়ভাবে ঠিক করতে পারে যেগুলোর সমস্যা সমাধানের একটি স্পষ্ট উপায় আছে এবং সম্ভবত আপনি সেটাই করতে চান। আপনি সম্ভবত আগেও কম্পাইলার ওয়ার্নিং দেখেছেন। উদাহরণস্বরূপ, এই কোডটি বিবেচনা করুন:
ফাইলের নাম: src/main.rs
fn main() { let mut x = 42; println!("{x}"); }
এখানে, আমরা x
ভেরিয়েবলটিকে মিউটেবল হিসেবে ডিফাইন করছি, কিন্তু আমরা আসলে এটিকে কখনও পরিবর্তন করছি না। Rust আমাদের এই বিষয়ে সতর্ক করে:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut x = 0;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
ওয়ার্নিংটি আমাদের mut
কীওয়ার্ডটি সরিয়ে ফেলার পরামর্শ দিচ্ছে। আমরা rustfix
টুলটি ব্যবহার করে cargo fix
কমান্ডটি চালিয়ে স্বয়ংক্রিয়ভাবে সেই পরামর্শটি প্রয়োগ করতে পারি:
$ cargo fix
Checking myprogram v0.1.0 (file:///projects/myprogram)
Fixing src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
যখন আমরা আবার src/main.rs দেখব, তখন আমরা দেখতে পাব যে cargo fix
কোডটি পরিবর্তন করেছে:
ফাইলের নাম: src/main.rs
fn main() { let x = 42; println!("{x}"); }``` `x` ভেরিয়েবলটি এখন ইমিউটেবল, এবং ওয়ার্নিংটি আর দেখা যাচ্ছে না। আপনি বিভিন্ন Rust এডিশনের মধ্যে আপনার কোড স্থানান্তর করতেও `cargo fix` কমান্ডটি ব্যবহার করতে পারেন। এডিশনগুলো [পরিশিষ্ট ঙ][editions]-এ আলোচনা করা হয়েছে। ## `clippy` দিয়ে আরও লিন্ট Clippy টুলটি হলো আপনার কোড বিশ্লেষণ করার জন্য লিন্টের একটি সংগ্রহ, যাতে আপনি সাধারণ ভুলগুলো ধরতে পারেন এবং আপনার Rust কোড উন্নত করতে পারেন। Clippy স্ট্যান্ডার্ড Rust ইনস্টলেশনের সাথে অন্তর্ভুক্ত থাকে। যেকোনো কার্গো প্রজেক্টে Clippy-এর লিন্টগুলো চালাতে, নিম্নলিখিতটি লিখুন: ```console $ cargo clippy
উদাহরণস্বরূপ, ধরুন আপনি একটি প্রোগ্রাম লিখেছেন যা একটি গাণিতিক ধ্রুবক, যেমন পাই (pi)-এর একটি আনুমানিক মান ব্যবহার করে, যেমন এই প্রোগ্রামটি করে:
fn main() { let x = 3.1415; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
এই প্রজেক্টে cargo clippy
চালালে এই এররটি আসে:
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
এই এররটি আপনাকে জানায় যে Rust-এ ইতোমধ্যে আরও নির্ভুল একটি PI
ধ্রুবক ডিফাইন করা আছে, এবং আপনি যদি সেই ধ্রুবকটি ব্যবহার করেন তবে আপনার প্রোগ্রামটি আরও সঠিক হবে। এরপর আপনি PI
ধ্রুবকটি ব্যবহার করার জন্য আপনার কোড পরিবর্তন করবেন।
নিম্নলিখিত কোডটি Clippy থেকে কোনো এরর বা ওয়ার্নিং তৈরি করে না:
fn main() { let x = std::f64::consts::PI; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
Clippy সম্পর্কে আরও তথ্যের জন্য, এর ডকুমেন্টেশন দেখুন।
rust-analyzer
ব্যবহার করে IDE ইন্টিগ্রেশন
IDE ইন্টিগ্রেশনে সাহায্য করার জন্য, Rust কমিউনিটি rust-analyzer
ব্যবহার করার সুপারিশ করে। এই টুলটি হলো কম্পাইলার-কেন্দ্রিক ইউটিলিটিগুলোর একটি সেট যা Language Server Protocol (LSP) ব্যবহার করে কথা বলে, যা IDE এবং প্রোগ্রামিং ল্যাঙ্গুয়েজগুলোর একে অপরের সাথে যোগাযোগের জন্য একটি স্পেসিফিকেশন। বিভিন্ন ক্লায়েন্ট rust-analyzer
ব্যবহার করতে পারে, যেমন Visual Studio Code-এর জন্য Rust analyzer প্লাগ-ইন।
ইনস্টলেশন নির্দেশাবলীর জন্য rust-analyzer
প্রজেক্টের হোম পেজ দেখুন, তারপর আপনার নির্দিষ্ট IDE-তে ল্যাঙ্গুয়েজ সার্ভার সাপোর্ট ইনস্টল করুন। আপনার IDE স্বয়ংক্রিয়-সম্পূর্ণকরণ (autocompletion), সংজ্ঞায় ঝাঁপ (jump to definition), এবং ইনলাইন এরর (inline errors) এর মতো ক্ষমতা অর্জন করবে।
পরিশিষ্ট ঙ - এডিশন
অধ্যায় ১-এ আপনি দেখেছেন যে cargo new
আপনার Cargo.toml ফাইলে এডিশন সম্পর্কে কিছু মেটাডেটা যোগ করে। এই পরিশিষ্টে আলোচনা করা হবে এর অর্থ কী!
Rust ল্যাঙ্গুয়েজ এবং কম্পাইলারের একটি ছয়-সัปতাহের রিলিজ সাইকেল রয়েছে, যার মানে ব্যবহারকারীরা ক্রমাগত নতুন ফিচারের একটি স্রোত পান। অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজগুলো কম সময়ে বড় পরিবর্তন রিলিজ করে; Rust ঘন ঘন ছোট আপডেট রিলিজ করে। কিছু সময় পর, এই সমস্ত ছোট ছোট পরিবর্তনগুলো একত্রিত হয়ে বড় আকার ধারণ করে। কিন্তু এক রিলিজ থেকে অন্য রিলিজে, পেছনে ফিরে বলা কঠিন হতে পারে, “বাহ, Rust 1.10 এবং Rust 1.31-এর মধ্যে, Rust অনেক বদলে গেছে!”
প্রতি তিন বছর বা তার কাছাকাছি সময়ে, Rust টিম একটি নতুন Rust এডিশন তৈরি করে। প্রতিটি এডিশন যেসব ফিচার এসেছে সেগুলোকে সম্পূর্ণ আপডেট করা ডকুমেন্টেশন এবং টুলিংসহ একটি স্পষ্ট প্যাকেজে একত্রিত করে। নতুন এডিশনগুলো স্বাভাবিক ছয়-সัปতাহের রিলিজ প্রক্রিয়ার অংশ হিসেবেই পাঠানো হয়।
এডিশনগুলো বিভিন্ন মানুষের জন্য বিভিন্ন উদ্দেশ্যে কাজ করে:
- সক্রিয় Rust ব্যবহারকারীদের জন্য, একটি নতুন এডিশন ক্রমবর্ধমান পরিবর্তনগুলোকে একটি সহজবোধ্য প্যাকেজে একত্রিত করে।
- যারা ব্যবহারকারী নন, তাদের জন্য একটি নতুন এডিশন সংকেত দেয় যে কিছু বড় অগ্রগতি এসেছে, যা Rust-কে আরেকবার দেখার মতো করে তুলতে পারে।
- যারা Rust ডেভেলপ করছেন, তাদের জন্য একটি নতুন এডিশন পুরো প্রজেক্টের জন্য একটি সমাবেশের বিন্দু প্রদান করে।
এই লেখার সময়, চারটি Rust এডিশন উপলব্ধ রয়েছে: Rust 2015, Rust 2018, Rust 2021 এবং Rust 2024। এই বইটি Rust 2024 এডিশনের ইডিয়ম ব্যবহার করে লেখা হয়েছে।
Cargo.toml ফাইলের edition
কী (key) নির্দেশ করে যে কম্পাইলার আপনার কোডের জন্য কোন এডিশন ব্যবহার করবে। যদি কী-টি موجود না থাকে, তবে Rust ব্যাকওয়ার্ড কম্প্যাটিবিলিটির কারণে 2015
-কে এডিশন ভ্যালু হিসেবে ব্যবহার করে।
প্রতিটি প্রজেক্ট ডিফল্ট 2015 এডিশন ছাড়া অন্য কোনো এডিশন বেছে নিতে পারে। এডিশনগুলোতে ইনকম্প্যাটিবল পরিবর্তন থাকতে পারে, যেমন একটি নতুন কীওয়ার্ড অন্তর্ভুক্ত করা যা কোডের আইডেন্টিফায়ারের সাথে কনফ্লিক্ট করে। তবে, আপনি যদি সেই পরিবর্তনগুলো বেছে না নেন, তবে আপনার কোড কম্পাইল হতে থাকবে এমনকি আপনি যে Rust কম্পাইলার সংস্করণ ব্যবহার করছেন তা আপগ্রেড করার পরেও।
সমস্ত Rust কম্পাইলার সংস্করণ সেই কম্পাইলারের রিলিজে আগে বিদ্যমান যেকোনো এডিশন সমর্থন করে, এবং তারা যেকোনো সমর্থিত এডিশনের ক্রেটগুলোকে একসাথে লিঙ্ক করতে পারে। এডিশন পরিবর্তনগুলো শুধুমাত্র কম্পাইলার প্রাথমিকভাবে কোড পার্স করার পদ্ধতিকে প্রভাবিত করে। অতএব, যদি আপনি Rust 2015 ব্যবহার করেন এবং আপনার একটি ডিপেন্ডেন্সি Rust 2018 ব্যবহার করে, আপনার প্রজেক্ট কম্পাইল হবে এবং সেই ডিপেন্ডেন্সি ব্যবহার করতে সক্ষম হবে। বিপরীত পরিস্থিতি, যেখানে আপনার প্রজেক্ট Rust 2018 ব্যবহার করে এবং একটি ডিপেন্ডেন্সি Rust 2015 ব্যবহার করে, সেটাও কাজ করবে।
স্পষ্ট করে বলতে গেলে: বেশিরভাগ ফিচার সব এডিশনেই উপলব্ধ থাকবে। যেকোনো Rust এডিশন ব্যবহারকারী ডেভেলপাররা নতুন স্টেবল রিলিজ হওয়ার সাথে সাথে উন্নতি দেখতে থাকবেন। তবে, কিছু ক্ষেত্রে, প্রধানত যখন নতুন কীওয়ার্ড যোগ করা হয়, তখন কিছু নতুন ফিচার শুধুমাত্র পরবর্তী এডিশনগুলোতে উপলব্ধ হতে পারে। এই ধরনের ফিচারগুলোর সুবিধা নিতে হলে আপনাকে এডিশন পরিবর্তন করতে হবে।
আরও বিস্তারিত জানার জন্য, এডিশন গাইড হলো এডিশন সম্পর্কে একটি সম্পূর্ণ বই যা এডিশনগুলোর মধ্যে পার্থক্য গণনা করে এবং cargo fix
-এর মাধ্যমে কীভাবে স্বয়ংক্রিয়ভাবে আপনার কোড একটি নতুন এডিশনে আপগ্রেড করতে হয় তা ব্যাখ্যা করে।
পরিশিষ্ট চ: বইটির অনুবাদসমূহ
ইংরেজি ছাড়া অন্য ভাষার রিসোর্সের জন্য। বেশিরভাগ অনুবাদ এখনও চলমান; সাহায্য করতে বা একটি নতুন অনুবাদ সম্পর্কে আমাদের জানাতে ট্রান্সলেশন লেবেলটি দেখুন!
- Português (BR)
- Português (PT)
- 简体中文: KaiserY/trpl-zh-cn, gnu4cn/rust-lang-Zh_CN
- 正體中文
- Українська
- Español, alternate, Español por RustLangES
- Русский
- 한국어
- 日本語
- Français
- Polski
- Cebuano
- Tagalog
- Esperanto
- ελληνική
- Svenska
- Farsi, Persian (FA)
- Deutsch
- हिंदी
- ไทย
- Danske
পরিশিষ্ট G - Rust কীভাবে তৈরি হয় এবং “নাইটলি Rust”
এই পরিশিষ্টটি Rust কীভাবে তৈরি হয় এবং একজন Rust ডেভেলপার হিসেবে এটি আপনাকে কীভাবে প্রভাবিত করে সে সম্পর্কে।
স্থবিরতা ছাড়া স্থিতিশীলতা
একটি ভাষা হিসেবে, Rust আপনার কোডের স্থিতিশীলতা নিয়ে অনেক বেশি ভাবে। আমরা চাই Rust একটি শিলা-কঠিন ভিত্তি হোক যার উপর আপনি নির্মাণ করতে পারেন, এবং যদি জিনিসগুলো ক্রমাগত পরিবর্তন হতে থাকে, তবে তা অসম্ভব হবে। একই সময়ে, যদি আমরা নতুন ফিচার নিয়ে পরীক্ষা-নিরীক্ষা করতে না পারি, তবে আমরা হয়তো তাদের প্রকাশের পরে গুরুত্বপূর্ণ ত্রুটিগুলো খুঁজে পাব না, যখন আমরা আর জিনিসগুলো পরিবর্তন করতে পারব না।
এই সমস্যার আমাদের সমাধান হলো যাকে আমরা বলি "স্থবিরতা ছাড়া স্থিতিশীলতা" (stability without stagnation), এবং আমাদের পথপ্রদর্শক নীতিটি হলো: আপনার কখনই স্থিতিশীল Rust-এর নতুন সংস্করণে আপগ্রেড করতে ভয় পাওয়া উচিত নয়। প্রতিটি আপগ্রেড যন্ত্রণাহীন হওয়া উচিত, তবে আপনার জন্য নতুন ফিচার, কম বাগ এবং দ্রুত কম্পাইল সময়ও নিয়ে আসা উচিত।
ছু, ছু! রিলিজ চ্যানেল এবং ট্রেনে চড়া
Rust-এর ডেভেলপমেন্ট একটি ট্রেন সময়সূচী (train schedule) মেনে চলে। অর্থাৎ, সমস্ত ডেভেলপমেন্ট Rust রিপোজিটরির master
ব্রাঞ্চে করা হয়। রিলিজগুলো একটি সফটওয়্যার রিলিজ ট্রেন মডেল অনুসরণ করে, যা Cisco IOS এবং অন্যান্য সফটওয়্যার প্রজেক্টে ব্যবহৃত হয়েছে। Rust-এর জন্য তিনটি রিলিজ চ্যানেল রয়েছে:
- নাইটলি (Nightly)
- বেটা (Beta)
- স্টেবল (Stable)
বেশিরভাগ Rust ডেভেলপার প্রাথমিকভাবে স্টেবল চ্যানেল ব্যবহার করেন, কিন্তু যারা পরীক্ষামূলক নতুন ফিচার চেষ্টা করতে চান তারা নাইটলি বা বেটা ব্যবহার করতে পারেন।
ডেভেলপমেন্ট এবং রিলিজ প্রক্রিয়া কীভাবে কাজ করে তার একটি উদাহরণ এখানে দেওয়া হলো: চলুন ধরে নেওয়া যাক যে Rust টিম Rust 1.5 রিলিজের উপর কাজ করছে। সেই রিলিজটি ২০১৫ সালের ডিসেম্বরে হয়েছিল, কিন্তু এটি আমাদের বাস্তবসম্মত সংস্করণ নম্বর সরবরাহ করবে। Rust-এ একটি নতুন ফিচার যোগ করা হয়েছে: master
ব্রাঞ্চে একটি নতুন কমিট এসেছে। প্রতি রাতে, Rust-এর একটি নতুন নাইটলি সংস্করণ তৈরি করা হয়। প্রতিদিন একটি রিলিজের দিন, এবং এই রিলিজগুলো আমাদের রিলিজ পরিকাঠামো দ্বারা স্বয়ংক্রিয়ভাবে তৈরি করা হয়। তাই সময় পার হওয়ার সাথে সাথে, আমাদের রিলিজগুলো প্রতি রাতে এমন দেখায়:
nightly: * - - * - - *
প্রতি ছয় সপ্তাহ পর, একটি নতুন রিলিজ প্রস্তুত করার সময়! Rust রিপোজিটরির beta
ব্রাঞ্চটি নাইটলি দ্বারা ব্যবহৃত master
ব্রাঞ্চ থেকে আলাদা হয়। এখন, দুটি রিলিজ রয়েছে:
nightly: * - - * - - *
|
beta: *
বেশিরভাগ Rust ব্যবহারকারী সক্রিয়ভাবে বেটা রিলিজ ব্যবহার করেন না, কিন্তু সম্ভাব্য রিগ্রেশন আবিষ্কারে Rust-কে সাহায্য করার জন্য তাদের CI সিস্টেমে বেটার বিরুদ্ধে পরীক্ষা চালান। ইতিমধ্যে, প্রতি রাতে এখনও একটি নাইটলি রিলিজ হয়:
nightly: * - - * - - * - - * - - *
|
beta: *
ধরা যাক একটি রিগ্রেশন পাওয়া গেল। ভালো ব্যাপার যে স্টেবল রিলিজে রিগ্রেশনটি ঢুকে পড়ার আগে বেটা রিলিজ পরীক্ষা করার জন্য আমাদের কিছু সময় ছিল! ফিক্সটি master
-এ প্রয়োগ করা হয়, যাতে নাইটলি ঠিক হয়ে যায়, এবং তারপর ফিক্সটি beta
ব্রাঞ্চে ব্যাকপোর্ট করা হয়, এবং বেটার একটি নতুন রিলিজ তৈরি করা হয়:
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
প্রথম বেটা তৈরি হওয়ার ছয় সপ্তাহ পর, একটি স্টেবল রিলিজের সময়! stable
ব্রাঞ্চটি beta
ব্রাঞ্চ থেকে তৈরি করা হয়:
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
হুররে! Rust 1.5 সম্পন্ন! যাইহোক, আমরা একটি জিনিস ভুলে গেছি: যেহেতু ছয় সপ্তাহ কেটে গেছে, তাই আমাদের Rust-এর পরবর্তী সংস্করণ, 1.6-এর একটি নতুন বেটাও প্রয়োজন। তাই stable
ব্রাঞ্চটি beta
থেকে আলাদা হওয়ার পর, beta
-এর পরবর্তী সংস্করণটি আবার nightly
থেকে আলাদা হয়:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
এটিকে "ট্রেন মডেল" বলা হয় কারণ প্রতি ছয় সপ্তাহে, একটি রিলিজ "স্টেশন ছেড়ে যায়", কিন্তু একটি স্টেবল রিলিজ হিসাবে পৌঁছানোর আগে তাকে বেটা চ্যানেলের মাধ্যমে একটি যাত্রা করতে হয়।
Rust প্রতি ছয় সপ্তাহে, ঘড়ির কাঁটার মতো রিলিজ করে। আপনি যদি একটি Rust রিলিজের তারিখ জানেন, আপনি পরেরটির তারিখ জানতে পারবেন: এটি ছয় সপ্তাহ পরে। প্রতি ছয় সপ্তাহে রিলিজ নির্ধারিত থাকার একটি চমৎকার দিক হলো যে পরবর্তী ট্রেনটি শীঘ্রই আসছে। যদি কোনো ফিচার কোনো নির্দিষ্ট রিলিজ মিস করে, তবে চিন্তা করার দরকার নেই: অল্প সময়ের মধ্যেই আরেকটি আসছে! এটি রিলিজের সময়সীমার কাছাকাছি সম্ভাব্য অপরিশোধিত ফিচারগুলোকে লুকিয়ে ফেলার চাপ কমাতে সাহায্য করে।
এই প্রক্রিয়ার জন্য ধন্যবাদ, আপনি সর্বদা Rust-এর পরবর্তী বিল্ডটি পরীক্ষা করতে পারেন এবং নিজের জন্য যাচাই করতে পারেন যে এটিতে আপগ্রেড করা সহজ: যদি একটি বেটা রিলিজ প্রত্যাশা অনুযায়ী কাজ না করে, আপনি টিমকে এটি রিপোর্ট করতে পারেন এবং পরবর্তী স্টেবল রিলিজ হওয়ার আগে এটি ঠিক করাতে পারেন! একটি বেটা রিলিজে ব্রেকএজ তুলনামূলকভাবে বিরল, কিন্তু rustc
এখনও একটি সফটওয়্যার, এবং বাগ বিদ্যমান।
রক্ষণাবেক্ষণের সময়
Rust প্রজেক্ট সবচেয়ে সাম্প্রতিক স্টেবল সংস্করণটিকে সমর্থন করে। যখন একটি নতুন স্টেবল সংস্করণ রিলিজ হয়, তখন পুরানো সংস্করণটি তার জীবনকালের শেষ পর্যায়ে (end of life - EOL) পৌঁছে যায়। এর মানে হলো প্রতিটি সংস্করণ ছয় সপ্তাহের জন্য সমর্থিত থাকে।
আনস্টেবল ফিচার
এই রিলিজ মডেলের সাথে আরও একটি বিষয় জড়িত: আনস্টেবল ফিচার। Rust একটি নির্দিষ্ট রিলিজে কোন ফিচারগুলো সক্রিয় আছে তা নির্ধারণ করতে "ফিচার ফ্ল্যাগ" নামক একটি কৌশল ব্যবহার করে। যদি একটি নতুন ফিচার সক্রিয় বিকাশের অধীনে থাকে, তবে এটি master
-এ আসে, এবং ফলস্বরূপ, নাইটলি-তে, কিন্তু একটি ফিচার ফ্ল্যাগের আড়ালে। আপনি যদি একজন ব্যবহারকারী হিসেবে, কাজটি চলমান থাকা ফিচারটি চেষ্টা করতে চান, তবে আপনি তা করতে পারেন, কিন্তু আপনাকে অবশ্যই Rust-এর একটি নাইটলি রিলিজ ব্যবহার করতে হবে এবং অপ্ট-ইন করার জন্য আপনার সোর্স কোডটি উপযুক্ত ফ্ল্যাগ দিয়ে অ্যানোটেট করতে হবে।
আপনি যদি Rust-এর বেটা বা স্টেবল রিলিজ ব্যবহার করেন, তবে আপনি কোনো ফিচার ফ্ল্যাগ ব্যবহার করতে পারবেন না। এটিই সেই চাবিকাঠি যা আমাদের নতুন ফিচারগুলোকে চিরস্থায়ীভাবে স্টেবল ঘোষণা করার আগে তাদের ব্যবহারিক ব্যবহার পেতে দেয়। যারা অত্যাধুনিক ফিচার অপ্ট-ইন করতে চান তারা তা করতে পারেন, এবং যারা একটি শিলা-কঠিন অভিজ্ঞতা চান তারা স্টেবল-এর সাথে থাকতে পারেন এবং জানতে পারেন যে তাদের কোড ভাঙবে না। স্থবিরতা ছাড়া স্থিতিশীলতা।
এই বইটিতে শুধুমাত্র স্টেবল ফিচার সম্পর্কে তথ্য রয়েছে, কারণ কাজ চলমান থাকা ফিচারগুলো এখনও পরিবর্তিত হচ্ছে, এবং নিশ্চিতভাবেই এই বইটি লেখার সময় এবং যখন সেগুলি স্টেবল বিল্ডে সক্রিয় হবে তার মধ্যে সেগুলি ভিন্ন হবে। আপনি অনলাইনে নাইটলি-কেবল ফিচারগুলোর জন্য ডকুমেন্টেশন খুঁজে পেতে পারেন।
rustup
এবং রাস্ট নাইটলি-র ভূমিকা
rustup
গ্লোবাল বা প্রতি-প্রজেক্ট ভিত্তিতে Rust-এর বিভিন্ন রিলিজ চ্যানেলের মধ্যে পরিবর্তন করা সহজ করে তোলে। ডিফল্টরূপে, আপনার কাছে স্টেবল Rust ইনস্টল করা থাকবে। উদাহরণস্বরূপ, নাইটলি ইনস্টল করতে:
$ rustup toolchain install nightly
আপনি rustup
দিয়ে আপনার ইনস্টল করা সমস্ত টুলচেইন (Rust-এর রিলিজ এবং সংশ্লিষ্ট উপাদান) দেখতে পারেন। এখানে আপনার একজন লেখকের উইন্ডোজ কম্পিউটারে একটি উদাহরণ দেওয়া হলো:
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
যেমন আপনি দেখতে পাচ্ছেন, স্টেবল টুলচেইনটি ডিফল্ট। বেশিরভাগ Rust ব্যবহারকারী বেশিরভাগ সময় স্টেবল ব্যবহার করেন। আপনি হয়তো বেশিরভাগ সময় স্টেবল ব্যবহার করতে চান, কিন্তু একটি নির্দিষ্ট প্রজেক্টে নাইটলি ব্যবহার করতে চান, কারণ আপনি একটি অত্যাধুনিক ফিচার নিয়ে আগ্রহী। এটি করার জন্য, আপনি সেই প্রজেক্টের ডিরেক্টরিতে rustup override
ব্যবহার করে নাইটলি টুলচেইনটিকে সেট করতে পারেন যা rustup
ব্যবহার করবে যখন আপনি সেই ডিরেক্টরিতে থাকবেন:
$ cd ~/projects/needs-nightly
$ rustup override set nightly
এখন, প্রতিবার যখন আপনি ~/projects/needs-nightly-এর ভিতরে rustc
বা cargo
কল করবেন, তখন rustup
নিশ্চিত করবে যে আপনি আপনার ডিফল্ট স্টেবল Rust-এর পরিবর্তে নাইটলি Rust ব্যবহার করছেন। এটি কার্যকর হয় যখন আপনার অনেক Rust প্রজেক্ট থাকে!
RFC প্রক্রিয়া এবং টিম
তাহলে আপনি এই নতুন ফিচারগুলো সম্পর্কে কীভাবে জানবেন? Rust-এর ডেভেলপমেন্ট মডেল একটি রিকোয়েস্ট ফর কমেন্টস (RFC) প্রক্রিয়া অনুসরণ করে। আপনি যদি Rust-এ কোনো উন্নতি চান, তবে আপনি একটি প্রস্তাবনা লিখতে পারেন, যাকে RFC বলা হয়।
যে কেউ Rust উন্নত করার জন্য RFC লিখতে পারে, এবং প্রস্তাবনাগুলো Rust টিম দ্বারা পর্যালোচনা এবং আলোচনা করা হয়, যা অনেকগুলো বিষয়ভিত্তিক সাবটিম নিয়ে গঠিত। Rust-এর ওয়েবসাইটে টিমগুলোর একটি সম্পূর্ণ তালিকা রয়েছে, যার মধ্যে প্রজেক্টের প্রতিটি ক্ষেত্রের জন্য টিম অন্তর্ভুক্ত: ভাষা ডিজাইন, কম্পাইলার ইমপ্লিমেন্টেশন, ইনফ্রাস্ট্রাকচার, ডকুমেন্টেশন এবং আরও অনেক কিছু। উপযুক্ত টিম প্রস্তাবনা এবং মন্তব্যগুলো পড়ে, তাদের নিজস্ব কিছু মন্তব্য লেখে, এবং অবশেষে, ফিচারটি গ্রহণ বা প্রত্যাখ্যান করার জন্য একটি ঐকমত্যে পৌঁছানো হয়।
যদি ফিচারটি গৃহীত হয়, তবে Rust রিপোজিটরিতে একটি ইস্যু খোলা হয়, এবং কেউ এটি ইমপ্লিমেন্ট করতে পারে। যে ব্যক্তি এটি ইমপ্লিমেন্ট করে সে খুব সম্ভবত সেই ব্যক্তি নাও হতে পারে যে প্রথম ফিচারটি প্রস্তাব করেছিল! যখন ইমপ্লিমেন্টেশনটি প্রস্তুত হয়, তখন এটি একটি ফিচার গেটের আড়ালে master
ব্রাঞ্চে আসে, যেমনটি আমরা "আনস্টেবল ফিচার" বিভাগে আলোচনা করেছি।
কিছু সময় পর, যখন নাইটলি রিলিজ ব্যবহারকারী Rust ডেভেলপাররা নতুন ফিচারটি চেষ্টা করতে সক্ষম হন, তখন টিমের সদস্যরা ফিচারটি নিয়ে আলোচনা করেন, এটি নাইটলি-তে কীভাবে কাজ করেছে তা নিয়ে আলোচনা করেন, এবং সিদ্ধান্ত নেন যে এটি স্টেবল Rust-এ আসা উচিত কিনা। যদি এগিয়ে যাওয়ার সিদ্ধান্ত নেওয়া হয়, তবে ফিচার গেটটি সরিয়ে ফেলা হয়, এবং ফিচারটি এখন স্টেবল হিসাবে বিবেচিত হয়! এটি ট্রেনগুলোতে চড়ে Rust-এর একটি নতুন স্টেবল রিলিজে আসে।