0x55aa
← Back to Blog

Rust HashMap and the Entry API: Stop Writing Null Checks Forever πŸ¦€πŸ“Š

β€’9 min read

Rust HashMap and the Entry API: Stop Writing Null Checks Forever πŸ¦€πŸ“Š

Confession: I have written isset($array[$key]) ? $array[$key] : $default approximately ten thousand times in my career. Maybe more. The number hurts to think about.

Coming from 7 years of Laravel and Node.js, I accepted this as the price of doing business. You check if a key exists, then you act on it. Two steps. Every time. Sometimes three, if you're feeling paranoid. And you're always feeling paranoid.

Then I learned about Rust's Entry API, and I genuinely felt robbed. Robbed of all those years. Someone had already solved this problem elegantly. I just didn't know it yet.

The Problem Every Developer Has Ignored πŸ˜…

Let's say you're counting how many times each word appears in some text. Classic problem. Here's how it goes in PHP:

$counts = [];
foreach ($words as $word) {
    if (isset($counts[$word])) {
        $counts[$word]++;
    } else {
        $counts[$word] = 1;
    }
}

And in JavaScript:

const counts = {};
for (const word of words) {
    counts[word] = (counts[word] ?? 0) + 1;
}

JS at least gets it down to one line with the nullish coalescing trick. PHP makes you write a whole ceremony. Either way, you're checking existence and then acting on it β€” two separate operations that are conceptually one thing: "give me the value, or give me a default if it isn't there yet."

For my RF/SDR hobby projects, I do this constantly. Counting how many times I've seen a specific frequency in a spectrum scan. Tallying signal strengths in a histogram. Every version of this in PHP or JS felt like typing the same boilerplate with a slightly different shape.

Enter Rust HashMap πŸ—ΊοΈ

Rust's HashMap is in the standard library. No dependencies. No imports from npm. Just:

use std::collections::HashMap;

let mut counts: HashMap<String, u32> = HashMap::new();

The basics work exactly like you'd expect:

// Insert
counts.insert("hello".to_string(), 1);

// Get (returns Option<&V>, not the value directly)
if let Some(n) = counts.get("hello") {
    println!("Found: {}", n);
}

// Contains check
if counts.contains_key("hello") {
    println!("Key exists!");
}

// Remove
counts.remove("hello");

You might notice get() returns Option<&V> β€” Rust's way of saying "this might not be there, deal with it." No null pointers. No undefined. A type that forces you to handle both cases. The compiler won't let you forget. πŸ”’

Coming from PHP, where accessing a missing array key either returns null silently or throws an E_NOTICE depending on your error reporting settings... this feels like being handed a helmet before you ride a motorcycle. Initially annoying. Immediately lifesaving.

The Entry API: The Thing That Changed Everything ✨

Here's the word-counting problem in Rust, naive version:

let mut counts: HashMap<String, u32> = HashMap::new();

for word in words {
    if let Some(n) = counts.get_mut(&word) {
        *n += 1;
    } else {
        counts.insert(word, 1);
    }
}

Still two branches. Still checking existence separately. Not great.

Now with the Entry API:

let mut counts: HashMap<String, u32> = HashMap::new();

for word in words {
    *counts.entry(word).or_insert(0) += 1;
}

One line. No if/else. No existence check. It reads like a sentence: "give me the entry for this word, or insert zero if it doesn't exist, then add one to whatever we got."

entry() returns an Entry β€” which is either Occupied (key exists) or Vacant (key doesn't exist). The API lets you act on either case with one fluid chain:

// Insert a default if missing, then get a mutable reference
counts.entry("signal".to_string()).or_insert(0);

// Insert a computed default if missing
counts.entry("frequency".to_string()).or_insert_with(|| expensive_computation());

// Modify in place (the or_insert returns &mut V)
*counts.entry("packets".to_string()).or_insert(0) += 1;

// or_default() uses the type's Default trait (0 for integers)
*counts.entry("errors".to_string()).or_default() += 1;

What excited me about this: No more "check, then act" in two separate steps. The entry() API makes it one atomic operation conceptually β€” either the key was there or it wasn't, and the default handles both. Clean. Honest. Fast.

Real-World: Building a Signal Frequency Histogram πŸ“‘

Here's a real thing I built for my SDR work β€” counting how many samples fall into each frequency bucket to build a signal histogram:

use std::collections::HashMap;

fn build_histogram(frequencies: &[f64], bucket_size: f64) -> HashMap<i64, u32> {
    let mut histogram: HashMap<i64, u32> = HashMap::new();

    for &freq in frequencies {
        let bucket = (freq / bucket_size).floor() as i64;
        *histogram.entry(bucket).or_default() += 1;
    }

    histogram
}

That's the entire function. In PHP, this would be:

  • $histogram = []
  • for loop
  • if (isset($histogram[$bucket])) check
  • increment or initialize

Rust compresses the interesting logic β€” the bucketing math β€” and handles the map update pattern with or_default(). I read it and immediately understand what it's doing. No ceremony around the ceremony.

BTreeMap: When Order Actually Matters 🌳

HashMap has no guaranteed iteration order (like JS objects pre-2015, or PHP arrays when you're not careful). Sometimes you need sorted keys. That's BTreeMap:

use std::collections::BTreeMap;

let mut sorted: BTreeMap<String, u32> = BTreeMap::new();
sorted.insert("zebra".to_string(), 3);
sorted.insert("apple".to_string(), 1);
sorted.insert("mango".to_string(), 2);

// Iterates in alphabetical order β€” guaranteed!
for (key, value) in &sorted {
    println!("{}: {}", key, value);
}
// apple: 1
// mango: 2
// zebra: 3

BTreeMap uses a B-tree internally (like a database index). Slightly slower than HashMap for random access, but O(log n) for everything and sorted for free. Entry API works identically on BTreeMap β€” same methods, same ergonomics.

For my SDR histogram: I switched from HashMap to BTreeMap so I could print the spectrum from low frequency to high frequency without a sort step. One type change. Same code everywhere else. 🎯

HashSet: When You Only Care If Something Exists 🎯

Sometimes you don't need counts. You just need to know if you've seen something before. That's HashSet:

use std::collections::HashSet;

let mut seen: HashSet<String> = HashSet::new();

for signal in signals {
    if seen.contains(&signal.frequency) {
        println!("Already saw this frequency!");
    } else {
        seen.insert(signal.frequency.clone());
        process_new_signal(&signal);
    }
}

HashSet is really just HashMap<T, ()> under the hood β€” all the same performance characteristics. It also has proper set operations:

let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();

let union: HashSet<&i32> = a.union(&b).collect();
let intersection: HashSet<&i32> = a.intersection(&b).collect();
let difference: HashSet<&i32> = a.difference(&b).collect();

In PHP/Laravel, set operations meant array_merge, array_intersect, array_diff β€” functions that don't compose cleanly and always make me look them up to remember the argument order. Rust's set methods chain naturally and have obvious names.

Performance: Why This Actually Matters ⚑

Rust's HashMap uses a hashing algorithm called SipHash by default β€” designed to be resistant to hash-flooding attacks (where an attacker crafts input to create worst-case collision behavior). Smart default.

For performance-critical code, you can swap the hasher:

use std::collections::HashMap;
use std::hash::BuildHasherDefault;

// FxHasher is faster for integer keys (no cryptographic protection)
// Useful for trusted data inside your own system
use rustc_hash::FxHashMap;

let mut fast_map: FxHashMap<u64, f32> = FxHashMap::default();

For my SDR processing β€” where I'm hashing integer frequency buckets a million times per second β€” swapping to a simpler hasher gave a noticeable speedup. In PHP or Node.js, you don't get to choose. In Rust, you do. πŸ”§

The Capacity Trick (Stop Reallocating) πŸ—οΈ

HashMap grows dynamically as you insert, reallocating when it hits capacity. If you know roughly how many entries you'll have, pre-allocate:

// Will hold ~1000 entries without reallocation
let mut map: HashMap<String, u32> = HashMap::with_capacity(1000);

PHP equivalent: $array = [] β€” PHP doesn't give you this control. Every push potentially reallocates. In a tight loop processing millions of signal samples, this matters a lot.

When to Reach for What πŸ—‚οΈ

Need Use
Fast key-value lookup, order doesn't matter HashMap
Key-value lookup with sorted iteration BTreeMap
Just membership testing (exists or not) HashSet
Sorted membership testing BTreeSet
You know the key range is small integers Vec indexed directly

The last one surprises people. If your keys are 0..1000, a Vec indexed by position is faster than any map β€” no hashing needed, just array indexing. Coming from PHP where "just use an array" means an associative array with string keys under the hood, this explicit control is refreshing.

TL;DR 🏁

After 7 years of isset() ceremonies and nullish coalescing tricks, Rust's HashMap Entry API is the thing I didn't know I desperately needed.

The highlights:

  • entry().or_insert(default) β€” handles "key exists or not" in one operation, no if/else
  • or_default() β€” uses the zero value for your type (0 for numbers, empty vec for vecs)
  • BTreeMap if you need sorted keys β€” same Entry API, different guarantees
  • HashSet for membership testing β€” proper set operations included
  • HashMap::with_capacity(n) β€” pre-allocate when you know the size

What excited me most isn't the performance (though it's excellent). It's the API design. Someone thought carefully about the most common thing you do with a map β€” "give me this value, or set it to something and give me that" β€” and turned it into one fluent, expressive operation.

In PHP, that pattern requires an if/else or a ternary. In JavaScript, it requires ?? or a helper. In Rust, it's entry().or_default().

Sometimes better design isn't about the big ideas. It's about noticing the small thing you do ten thousand times and making it one line instead of five.

I wish I'd had this ten thousand times ago. πŸ¦€


Want to play with HashMap? Install Rust at rustup.rs and run cargo new map-practice. You'll have it compiling in two minutes and a HashMap in three.

Building something with Rust? Find me on LinkedIn β€” I'm always happy to talk data structures with people who've survived PHP arrays.

More Rust experiments: GitHub β€” including my SDR signal processing tools.

Now go entry().or_default() your way to cleaner code! πŸ¦€πŸ“Šβœ¨