হ্যাশ ম্যাপে অ্যাসোসিয়েটেড ভ্যালু সহ কী সংরক্ষণ করা (Storing Keys with Associated Values in Hash Maps)

আমাদের সাধারণ কালেকশনগুলোর মধ্যে সর্বশেষ হল হ্যাশ ম্যাপ (hash map)HashMap<K, V> টাইপ একটি হ্যাশিং ফাংশন (hashing function) ব্যবহার করে K টাইপের কী (key)-গুলোকে V টাইপের ভ্যালু (value)-তে ম্যাপ করে। এই হ্যাশিং ফাংশন নির্ধারণ করে যে কীভাবে এই কী এবং ভ্যালুগুলোকে মেমরিতে রাখা হবে। অনেক প্রোগ্রামিং ল্যাঙ্গুয়েজ এই ধরনের ডেটা স্ট্রাকচার সমর্থন করে, কিন্তু সেগুলোতে প্রায়শই ভিন্ন নাম ব্যবহার করা হয়, যেমন হ্যাশ (hash), ম্যাপ (map), অবজেক্ট (object), হ্যাশ টেবিল (hash table), ডিকশনারি (dictionary), বা অ্যাসোসিয়েটিভ অ্যারে (associative array)

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

আমরা এই বিভাগে হ্যাশ ম্যাপের বেসিক API নিয়ে আলোচনা করব, তবে স্ট্যান্ডার্ড লাইব্রেরি দ্বারা HashMap<K, V>-তে সংজ্ঞায়িত ফাংশনগুলোতে আরও অনেক কিছু রয়েছে। যথারীতি, আরও তথ্যের জন্য স্ট্যান্ডার্ড লাইব্রেরির ডকুমেন্টেশন দেখুন।

একটি নতুন হ্যাশ ম্যাপ তৈরি করা (Creating a New Hash Map)

খালি হ্যাশ ম্যাপ তৈরি করার একটি উপায় হল new ব্যবহার করা এবং insert দিয়ে এলিমেন্ট যোগ করা। Listing 8-20-তে, আমরা দুটি দলের স্কোর ট্র্যাক করছি যাদের নাম Blue এবং Yellow। Blue টিম 10 পয়েন্ট দিয়ে শুরু করে এবং Yellow টিম 50 পয়েন্ট দিয়ে শুরু করে।

fn main() {
    use std::collections::HashMap;

    let mut scores = HashMap::new();

    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);
}

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

ভেক্টরের মতোই, হ্যাশ ম্যাপগুলো তাদের ডেটা হিপে সংরক্ষণ করে। এই HashMap-এর String টাইপের কী এবং i32 টাইপের ভ্যালু রয়েছে। ভেক্টরের মতো, হ্যাশ ম্যাপগুলোও হোমোজিনিয়াস (homogeneous): সমস্ত কী-এর একই টাইপ হতে হবে এবং সমস্ত ভ্যালুর একই টাইপ হতে হবে।

হ্যাশ ম্যাপের ভ্যালু অ্যাক্সেস করা (Accessing Values in a Hash Map)

আমরা হ্যাশ ম্যাপ থেকে একটি ভ্যালু পেতে পারি get মেথডে তার কী প্রদান করে, যেমনটি Listing 8-21-এ দেখানো হয়েছে।

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 টিমের সাথে সম্পর্কিত মান থাকবে এবং ফলাফল হবে 10get মেথড একটি Option<&V> রিটার্ন করে; যদি হ্যাশ ম্যাপে সেই কী-এর জন্য কোনো মান না থাকে, তাহলে get None রিটার্ন করবে। এই প্রোগ্রামটি Option হ্যান্ডেল করে copied কল করে একটি Option<&i32>-এর পরিবর্তে একটি Option<i32> পেতে, তারপর unwrap_or ব্যবহার করে score-কে শূন্যে সেট করে যদি scores-এ কী-এর জন্য কোনো এন্ট্রি না থাকে।

আমরা ভেক্টরের মতোই হ্যাশ ম্যাপের প্রতিটি কী-ভ্যালু পেয়ারের উপর ইটারেট করতে পারি, একটি for লুপ ব্যবহার করে:

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 Maps and Ownership)

i32-এর মতো Copy ট্রেইট ইমপ্লিমেন্ট করে এমন টাইপের জন্য, মানগুলো হ্যাশ ম্যাপে কপি করা হয়। String-এর মতো ওনড (owned) ভ্যালুগুলোর জন্য, ভ্যালুগুলো সরানো হবে এবং হ্যাশ ম্যাপ সেই ভ্যালুগুলোর ওনার হবে, যেমনটি Listing 8-22-এ দেখানো হয়েছে।

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 ভেরিয়েবলগুলো ব্যবহার করতে পারব না।

যদি আমরা হ্যাশ ম্যাপে ভ্যালুগুলোর রেফারেন্স ইনসার্ট করি, তাহলে ভ্যালুগুলো হ্যাশ ম্যাপে সরানো হবে না। রেফারেন্সগুলো যে মানগুলোর দিকে নির্দেশ করে সেগুলো অবশ্যই হ্যাশ ম্যাপটি বৈধ থাকা পর্যন্ত বৈধ থাকতে হবে। আমরা চ্যাপ্টার ১০-এ “লাইফটাইম দিয়ে রেফারেন্স বৈধ করা”-তে এই বিষয়গুলো নিয়ে আরও কথা বলব।

একটি হ্যাশ ম্যাপ আপডেট করা (Updating a Hash Map)

যদিও কী এবং ভ্যালু পেয়ারের সংখ্যা বাড়তে পারে, প্রতিটি ইউনিক কী-এর সাথে একবারে শুধুমাত্র একটি ভ্যালু যুক্ত থাকতে পারে (কিন্তু বিপরীতটি সত্য নয়: উদাহরণস্বরূপ, Blue টিম এবং Yellow টিম উভয়েরই scores হ্যাশ ম্যাপে 10 মান সংরক্ষিত থাকতে পারে)।

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

একটি ভ্যালু ওভাররাইট করা (Overwriting a Value)

যদি আমরা একটি হ্যাশ ম্যাপে একটি কী এবং একটি ভ্যালু ইনসার্ট করি এবং তারপর সেই একই কী-তে একটি ভিন্ন ভ্যালু ইনসার্ট করি, তাহলে সেই কী-এর সাথে সম্পর্কিত ভ্যালুটি প্রতিস্থাপিত হবে। যদিও Listing 8-23-এর কোডটি দুবার insert কল করে, হ্যাশ ম্যাপটিতে কেবল একটি কী-ভ্যালু পেয়ার থাকবে কারণ আমরা উভয়বারই Blue টিমের কী-এর জন্য ভ্যালু ইনসার্ট করছি।

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-এর আসল মানটি ওভাররাইট করা হয়েছে।

একটি কী এবং ভ্যালু যোগ করা শুধুমাত্র যদি একটি কী উপস্থিত না থাকে (Adding a Key and Value Only If a Key Isn’t Present)

একটি নির্দিষ্ট কী-এর ஏற்கனவே একটি ভ্যালু সহ হ্যাশ ম্যাপে আছে কিনা তা পরীক্ষা করা এবং তারপর নিম্নলিখিত পদক্ষেপগুলো নেওয়া সাধারণ: যদি কী-টি হ্যাশ ম্যাপে বিদ্যমান থাকে, তাহলে বিদ্যমান মানটি যেমন আছে তেমনই থাকা উচিত। যদি কী-টি বিদ্যমান না থাকে, তাহলে এটি এবং এর জন্য একটি ভ্যালু ইনসার্ট করুন।

হ্যাশ ম্যাপগুলোর জন্য এই কাজের জন্য একটি বিশেষ API রয়েছে যাকে entry বলা হয়, যা আপনি যে কী-টি পরীক্ষা করতে চান সেটি প্যারামিটার হিসাবে নেয়। entry মেথডের রিটার্ন মান হল Entry নামক একটি এনাম, যা এমন একটি মান উপস্থাপন করে যা বিদ্যমান থাকতে পারে বা নাও থাকতে পারে। ধরা যাক, আমরা পরীক্ষা করতে চাই যে Yellow টিমের কী-টির সাথে কোনো ভ্যালু যুক্ত আছে কিনা। যদি না থাকে, তাহলে আমরা 50 মানটি ইনসার্ট করতে চাই, এবং Blue টিমের জন্যও একই কাজ করতে চাই। entry API ব্যবহার করে, কোডটি Listing 8-24-এর মতো দেখায়।

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 কী-এর জন্য ভ্যালুর একটি মিউটেবল রেফারেন্স (&mut V) রিটার্ন করার জন্য যদি সেই কী বিদ্যমান থাকে, এবং যদি না থাকে, তাহলে এটি এই কী-এর জন্য নতুন ভ্যালু হিসাবে প্যারামিটারটি ইনসার্ট করে এবং নতুন ভ্যালুতে একটি মিউটেবল রেফারেন্স রিটার্ন করে। এই কৌশলটি নিজে থেকে লজিক লেখার চেয়ে অনেক বেশি পরিষ্কার এবং এছাড়াও, বোরো চেকারের (borrow checker) সাথে আরও ভালোভাবে কাজ করে।

Listing 8-24-এর কোড চালালে {"Yellow": 50, "Blue": 10} প্রিন্ট হবে। entry-তে প্রথম কলটি Yellow টিমের কী-এর জন্য 50 মান ইনসার্ট করবে কারণ Yellow টিমের ইতিমধ্যেই কোনো ভ্যালু ছিল না। entry-তে দ্বিতীয় কলটি হ্যাশ ম্যাপ পরিবর্তন করবে না কারণ Blue টিমের ইতিমধ্যেই 10 মান রয়েছে।

পুরানো মানের উপর ভিত্তি করে একটি মান আপডেট করা (Updating a Value Based on the Old Value)

হ্যাশ ম্যাপের জন্য আরেকটি সাধারণ ব্যবহারের ক্ষেত্র হল একটি কী-এর মান সন্ধান করা এবং তারপর পুরানো মানের উপর ভিত্তি করে এটি আপডেট করা। উদাহরণস্বরূপ, Listing 8-25 এমন কোড দেখায় যা গণনা করে যে কিছু টেক্সটে প্রতিটি শব্দ কতবার এসেছে। আমরা শব্দগুলোকে কী হিসাবে ব্যবহার করে একটি হ্যাশ ম্যাপ ব্যবহার করি এবং আমরা কতবার সেই শব্দটি দেখেছি তা ট্র্যাক রাখতে মান বৃদ্ধি করি। যদি এটি এমন একটি শব্দ হয় যা আমরা প্রথমবার দেখেছি, তাহলে আমরা প্রথমে 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} প্রিন্ট করবে। আপনি হয়তো একই কী-ভ্যালু পেয়ারগুলো ভিন্ন ক্রমে প্রিন্ট হতে দেখতে পারেন: “হ্যাশ ম্যাপের ভ্যালু অ্যাক্সেস করা” থেকে মনে রাখবেন যে একটি হ্যাশ ম্যাপের উপর ইটারেটিং একটি নির্বিচার ক্রমে ঘটে।

split_whitespace মেথডটি text-এর মানের হোয়াইটস্পেস দ্বারা পৃথক করা সাবস্লাইসের উপর একটি ইটারেটর রিটার্ন করে। or_insert মেথডটি নির্দিষ্ট কী-এর জন্য মানের একটি মিউটেবল রেফারেন্স (&mut V) রিটার্ন করে। এখানে, আমরা সেই মিউটেবল রেফারেন্সটি count ভেরিয়েবলে সংরক্ষণ করি, তাই সেই মানটিতে অ্যাসাইন করার জন্য, আমাদের প্রথমে তারকাচিহ্ন (*) ব্যবহার করে count ডিরেফারেন্স করতে হবে। মিউটেবল রেফারেন্সটি for লুপের শেষে স্কোপের বাইরে চলে যায়, তাই এই সমস্ত পরিবর্তনগুলো নিরাপদ এবং বোরোয়িং নিয়ম দ্বারা অনুমোদিত।

হ্যাশিং ফাংশন (Hashing Functions)

ডিফল্টরূপে, HashMap SipHash নামক একটি হ্যাশিং ফাংশন ব্যবহার করে যা হ্যাশ টেবিল1 জড়িত ডিনায়াল-অফ-সার্ভিস (DoS) আক্রমণের বিরুদ্ধে প্রতিরোধ প্রদান করতে পারে। এটি উপলব্ধ দ্রুততম হ্যাশিং অ্যালগরিদম নয়, তবে পারফরম্যান্সের ড্রপের সাথে আসা আরও ভাল নিরাপত্তার জন্য ট্রেড-অফটি মূল্যবান। আপনি যদি আপনার কোড প্রোফাইল করেন এবং দেখেন যে ডিফল্ট হ্যাশ ফাংশনটি আপনার উদ্দেশ্যের জন্য খুব ধীর, তাহলে আপনি একটি ভিন্ন হ্যাশার (hasher) নির্দিষ্ট করে অন্য ফাংশনে স্যুইচ করতে পারেন। একটি হ্যাশার হল এমন একটি টাইপ যা BuildHasher ট্রেইট ইমপ্লিমেন্ট করে। আমরা চ্যাপ্টার 10-এ ট্রেইটস এবং কিভাবে সেগুলো ইমপ্লিমেন্ট করতে হয় সে সম্পর্কে কথা বলব। আপনাকে শুরু থেকেই নিজের হ্যাশার ইমপ্লিমেন্ট করতে হবে না; crates.io-তে অন্যান্য Rust ব্যবহারকারীদের শেয়ার করা লাইব্রেরি রয়েছে যা অনেক সাধারণ হ্যাশিং অ্যালগরিদম ইমপ্লিমেন্ট করে এমন হ্যাশার সরবরাহ করে।

সারসংক্ষেপ (Summary)

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

  1. পূর্ণসংখ্যার একটি তালিকা দেওয়া হলে, একটি ভেক্টর ব্যবহার করুন এবং তালিকার মিডিয়ান (median) (সাজানো হলে, মাঝের অবস্থানের মান) এবং মোড (mode) (যে মানটি সবচেয়ে বেশি ঘটে; এখানে একটি হ্যাশ ম্যাপ সহায়ক হবে) রিটার্ন করুন।
  2. স্ট্রিংগুলোকে পিগ ল্যাটিনে (pig latin) রূপান্তর করুন। প্রতিটি শব্দের প্রথম ব্যঞ্জনবর্ণটি শব্দের শেষে সরানো হয় এবং ay যোগ করা হয়, তাই first হয়ে যায় irst-fay। যেসব শব্দ স্বরবর্ণ দিয়ে শুরু হয় সেগুলোর পরিবর্তে শেষে hay যোগ করা হয় (apple হয়ে যায় apple-hay)। UTF-8 এনকোডিং সম্পর্কে বিস্তারিত মনে রাখবেন!
  3. একটি হ্যাশ ম্যাপ এবং ভেক্টর ব্যবহার করে, একটি টেক্সট ইন্টারফেস তৈরি করুন যাতে একজন ব্যবহারকারী একটি কোম্পানিতে একটি বিভাগে কর্মচারীর নাম যোগ করতে পারে; উদাহরণস্বরূপ, “Add Sally to Engineering” বা “Add Amir to Sales”। তারপর ব্যবহারকারীকে একটি বিভাগের সমস্ত লোকের তালিকা বা কোম্পানির সমস্ত লোককে বিভাগ অনুসারে, বর্ণানুক্রমে সাজানো তালিকা পুনরুদ্ধার করতে দিন।

স্ট্যান্ডার্ড লাইব্রেরি API ডকুমেন্টেশন ভেক্টর, স্ট্রিং এবং হ্যাশ ম্যাপের মেথডগুলো বর্ণনা করে যা এই অনুশীলনগুলোর জন্য সহায়ক হবে!

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