🦀 Functional Rust

357: Entry API — Insert or Update Without Double Lookup

Difficulty: 2 Level: Intermediate A single-lookup handle to a map slot that lets you initialize, update, or conditionally modify — the idiomatic Rust alternative to "get then insert".

The Problem This Solves

The naive way to "insert if missing, update if present" in a map involves two lookups: `if map.contains_key(&k) { map.get_mut(&k)... } else { map.insert(k, default) }`. This is not only verbose — it's also subtly wrong with the borrow checker. The immutable borrow from `contains_key` and the mutable borrow from `get_mut` conflict if you're not careful. The entry API resolves this. `map.entry(key)` returns an `Entry` enum — either `Occupied` (key exists) or `Vacant` (key missing). Either way, you hold a handle to that slot in the map without paying for a second lookup. The methods on `Entry` — `or_insert`, `or_insert_with`, `and_modify`, `or_default` — cover all the common patterns cleanly. This matters most in hot loops. Building a frequency map over a billion tokens, accumulating a graph's edge weights, or maintaining per-user session state — every `.entry()` call does exactly one hash and one comparison. The double-lookup version does two.

The Intuition

No direct Python equivalent — Python dicts don't expose a "slot handle". The closest idiom is `d.setdefault(key, default)` (insert if missing) or `d[key] = d.get(key, 0) + 1` (counter pattern). Rust's entry API is more powerful: it chains initialization and modification, and you can run arbitrary closures on the value in one pass. Think of `Entry` as a "pointer to a slot" — it keeps the map locked to that slot until you're done. No one else can touch that slot between `entry()` and the method call. That's the guarantee that lets the borrow checker be happy and lets you avoid the double-lookup.

How It Works in Rust

use std::collections::HashMap;

let mut map: HashMap<&str, Vec<i32>> = HashMap::new();

// or_insert: insert a default if the key is missing
// Returns a mutable reference to the value (new or existing)
map.entry("alice").or_insert_with(Vec::new).push(1);
map.entry("alice").or_insert_with(Vec::new).push(2); // appends to existing

// or_insert: for Copy types, insert a literal
let mut counts: HashMap<&str, u32> = HashMap::new();
*counts.entry("rust").or_insert(0) += 1; // idiomatic counter

// or_default: uses the type's Default implementation
let mut groups: HashMap<&str, Vec<&str>> = HashMap::new();
groups.entry("fruits").or_default().push("apple"); // Vec::default() = vec![]

// and_modify: run a closure only when key already exists
// Chains with or_insert for full insert-or-update
let mut scores: HashMap<&str, i32> = HashMap::new();
scores
 .entry("alice")
 .and_modify(|s| *s += 10) // only called if key exists
 .or_insert(10);            // only called if key was missing

// The Entry enum directly (for advanced branching)
use std::collections::hash_map::Entry;
match map.entry("bob") {
 Entry::Occupied(mut e) => {
     e.get_mut().push(99);  // key exists — mutate in place
 }
 Entry::Vacant(e) => {
     e.insert(vec![99]);    // key missing — insert
 }
}

What This Unlocks

Key Differences

ConceptOCamlRust
Insert-or-update`find` + `replace` (two ops)`.entry().or_insert()` (one lookup)
Insert with factory`find` + conditional `add``.entry().or_insert_with(...)`
Update if present`find` + `replace``.entry().and_modify(v...)`
Full insert-or-updatemanual match`.entry().and_modify(f).or_insert(v)`
Access slot directlyN/A`Entry::Occupied` / `Entry::Vacant`
Works on`Hashtbl`, `Map``HashMap`, `BTreeMap`
use std::collections::HashMap;

fn main() {
    // or_insert: insert default if missing
    let mut scores: HashMap<&str, i32> = HashMap::new();
    scores.entry("Alice").or_insert(0);
    scores.entry("Bob").or_insert(0);
    *scores.entry("Alice").or_insert(0) += 10;
    *scores.entry("Alice").or_insert(0) += 5;
    println!("Scores: {scores:?}");

    // or_insert_with: lazy default (only computed if missing)
    let mut cache: HashMap<u32, Vec<u32>> = HashMap::new();
    for x in 0u32..12 {
        cache.entry(x % 3).or_insert_with(Vec::new).push(x);
    }
    let mut keys: Vec<_> = cache.keys().cloned().collect(); keys.sort();
    for k in keys { println!("mod3={k}: {:?}", cache[&k]); }

    // and_modify + or_insert: update if exists, insert otherwise
    let mut freq: HashMap<char, usize> = HashMap::new();
    for c in "hello world".chars().filter(|c| !c.is_whitespace()) {
        freq.entry(c).and_modify(|n| *n += 1).or_insert(1);
    }
    let mut fv: Vec<_> = freq.iter().collect(); fv.sort();
    println!("Freq: {fv:?}");

    // or_default: uses Default trait
    let mut groups: HashMap<bool, Vec<i32>> = HashMap::new();
    for x in 0..10 { groups.entry(x % 2 == 0).or_default().push(x); }
    println!("Even: {:?}", { let mut e = groups[&true].clone(); e.sort(); e });
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn or_insert_counter() {
        let mut m: HashMap<&str,i32> = HashMap::new();
        *m.entry("x").or_insert(0) += 1;
        *m.entry("x").or_insert(0) += 1;
        assert_eq!(m["x"], 2);
    }
    #[test] fn and_modify() {
        let mut m: HashMap<&str,i32> = [("a",5)].into();
        m.entry("a").and_modify(|v| *v *= 2).or_insert(0);
        assert_eq!(m["a"], 10);
        m.entry("b").and_modify(|v| *v *= 2).or_insert(99);
        assert_eq!(m["b"], 99);
    }
    #[test] fn or_default() {
        let mut m: HashMap<&str, Vec<i32>> = HashMap::new();
        m.entry("k").or_default().push(1);
        m.entry("k").or_default().push(2);
        assert_eq!(m["k"], vec![1,2]);
    }
}
(* OCaml: entry-like patterns with Hashtbl *)

let upsert tbl k f default =
  let v = try Hashtbl.find tbl k with Not_found -> default in
  Hashtbl.replace tbl k (f v)

let () =
  let tbl = Hashtbl.create 8 in
  List.iter (fun w -> upsert tbl w (fun n -> n+1) 0) ["a";"b";"a";"c";"a";"b"];
  Hashtbl.iter (fun k v -> Printf.printf "%s: %d\n" k v) tbl