357: Entry API
Tutorial
The Problem
A common map operation is "if the key exists, update the value; if not, insert a default." Naively this requires two lookups: get then insert. Rust's entry API solves this with a single lookup that returns an Entry enum β Occupied or Vacant β allowing you to atomically inspect and modify the map without the borrow checker complications of holding a reference to the value while also modifying the map. This pattern, introduced in Rust 1.0, avoids cloning keys unnecessarily and is the idiomatic way to implement counting, memoization, and conditional initialization.
🎯 Learning Outcomes
entry(key).or_insert(default) for insert-if-absententry(key).or_insert_with(f) for lazily computed defaultsentry(key).and_modify(|v| ...).or_insert(default) for conditional updateentry(key).or_default() when V: Defaultentry() takes ownership of the key only when inserting (Vacant)Code Example
#![allow(clippy::all)]
//! # Entry API
//! Efficient insert-or-update with HashMap's Entry API.
use std::collections::HashMap;
pub fn count_chars(s: &str) -> HashMap<char, usize> {
let mut map = HashMap::new();
for c in s.chars() {
map.entry(c).and_modify(|n| *n += 1).or_insert(1);
}
map
}
pub fn get_or_compute<K, V, F>(map: &mut HashMap<K, V>, key: K, compute: F) -> &V
where
K: Eq + std::hash::Hash,
F: FnOnce() -> V,
{
map.entry(key).or_insert_with(compute)
}
pub fn update_with_default<K, V, F>(map: &mut HashMap<K, V>, key: K, default: V, update: F)
where
K: Eq + std::hash::Hash,
F: FnOnce(&mut V),
{
map.entry(key).and_modify(update).or_insert(default);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_counting() {
let counts = count_chars("hello");
assert_eq!(counts[&'l'], 2);
assert_eq!(counts[&'o'], 1);
}
#[test]
fn lazy_compute() {
let mut map = HashMap::new();
let v = get_or_compute(&mut map, "key", || 42);
assert_eq!(*v, 42);
let v2 = get_or_compute(&mut map, "key", || 99);
assert_eq!(*v2, 42); // not recomputed
}
#[test]
fn update_existing() {
let mut map = HashMap::new();
map.insert("k", 10);
update_with_default(&mut map, "k", 0, |v| *v *= 2);
assert_eq!(map["k"], 20);
}
}Key Differences
| Aspect | Rust entry API | OCaml Map.update / Hashtbl |
|---|---|---|
| Lookup count | One (single hash computation) | One (Map) or two (Hashtbl) |
| Key ownership | Taken only if inserting | Always cloned/allocated |
| Chaining | .and_modify().or_insert() fluent | Nested match |
| Lazy default | or_insert_with(f) | match None -> f () |
| Return value | &mut V reference | Value (not reference) |
OCaml Approach
OCaml's Hashtbl requires explicit find + replace:
let get_or_compute tbl key compute =
match Hashtbl.find_opt tbl key with
| Some v -> v
| None ->
let v = compute () in
Hashtbl.add tbl key v; v
(* With Map (functional): *)
module M = Map.Make(String)
let count_chars s =
String.fold_left (fun m c ->
M.update (String.make 1 c) (function
| None -> Some 1
| Some n -> Some (n + 1)) m
) M.empty s
Map.update in OCaml (4.06+) is the closest equivalent to Rust's entry API β it passes None or Some to a function and uses the return value as the new binding. Still requires two logical branches vs Rust's fluent and_modify().or_insert() chain.
Full Source
#![allow(clippy::all)]
//! # Entry API
//! Efficient insert-or-update with HashMap's Entry API.
use std::collections::HashMap;
pub fn count_chars(s: &str) -> HashMap<char, usize> {
let mut map = HashMap::new();
for c in s.chars() {
map.entry(c).and_modify(|n| *n += 1).or_insert(1);
}
map
}
pub fn get_or_compute<K, V, F>(map: &mut HashMap<K, V>, key: K, compute: F) -> &V
where
K: Eq + std::hash::Hash,
F: FnOnce() -> V,
{
map.entry(key).or_insert_with(compute)
}
pub fn update_with_default<K, V, F>(map: &mut HashMap<K, V>, key: K, default: V, update: F)
where
K: Eq + std::hash::Hash,
F: FnOnce(&mut V),
{
map.entry(key).and_modify(update).or_insert(default);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_counting() {
let counts = count_chars("hello");
assert_eq!(counts[&'l'], 2);
assert_eq!(counts[&'o'], 1);
}
#[test]
fn lazy_compute() {
let mut map = HashMap::new();
let v = get_or_compute(&mut map, "key", || 42);
assert_eq!(*v, 42);
let v2 = get_or_compute(&mut map, "key", || 99);
assert_eq!(*v2, 42); // not recomputed
}
#[test]
fn update_existing() {
let mut map = HashMap::new();
map.insert("k", 10);
update_with_default(&mut map, "k", 0, |v| *v *= 2);
assert_eq!(map["k"], 20);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn char_counting() {
let counts = count_chars("hello");
assert_eq!(counts[&'l'], 2);
assert_eq!(counts[&'o'], 1);
}
#[test]
fn lazy_compute() {
let mut map = HashMap::new();
let v = get_or_compute(&mut map, "key", || 42);
assert_eq!(*v, 42);
let v2 = get_or_compute(&mut map, "key", || 99);
assert_eq!(*v2, 42); // not recomputed
}
#[test]
fn update_existing() {
let mut map = HashMap::new();
map.insert("k", 10);
update_with_default(&mut map, "k", 0, |v| *v *= 2);
assert_eq!(map["k"], 20);
}
}
Deep Comparison
OCaml vs Rust: Entry Api
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
entry(n).or_insert_with(|| compute_fib(n, memo)) in a HashMap<u64, u64> passed through recursion.HashMap<String, Vec<String>> of wordβnext-words from a text corpus using entry(word).or_default().push(next); sample random sentences from the resulting Markov chain.apply_defaults(config: &mut HashMap<&str, String>, defaults: &[(&str, &str)]) using entry(k).or_insert(v.to_string()) β only insert if key is absent.