ExamplesBy LevelBy TopicLearning Paths
357 Intermediate

357: Entry API

Functional Programming

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

  • β€’ Use entry(key).or_insert(default) for insert-if-absent
  • β€’ Use entry(key).or_insert_with(f) for lazily computed defaults
  • β€’ Use entry(key).and_modify(|v| ...).or_insert(default) for conditional update
  • β€’ Use entry(key).or_default() when V: Default
  • β€’ Understand that entry() takes ownership of the key only when inserting (Vacant)
  • β€’ Implement memoization and lazy initialization using the entry API
  • 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

    AspectRust entry APIOCaml Map.update / Hashtbl
    Lookup countOne (single hash computation)One (Map) or two (Hashtbl)
    Key ownershipTaken only if insertingAlways cloned/allocated
    Chaining.and_modify().or_insert() fluentNested match
    Lazy defaultor_insert_with(f)match None -> f ()
    Return value&mut V referenceValue (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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Type systemHindley-MilnerOwnership + traits
    MemoryGCZero-cost abstractions
    MutabilityExplicit refmut keyword
    Error handlingOption/ResultResult<T, E>

    See README.md for detailed comparison.

    Exercises

  • Fibonacci memoization: Implement recursive Fibonacci with memoization using entry(n).or_insert_with(|| compute_fib(n, memo)) in a HashMap<u64, u64> passed through recursion.
  • Word adjacency: Build a 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.
  • Configuration defaults: Implement apply_defaults(config: &mut HashMap<&str, String>, defaults: &[(&str, &str)]) using entry(k).or_insert(v.to_string()) β€” only insert if key is absent.
  • Open Source Repos