ExamplesBy LevelBy TopicLearning Paths
547 Intermediate

Polonius Borrow Checker Concepts

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Polonius Borrow Checker Concepts" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. NLL (Non-Lexical Lifetimes) dramatically improved Rust's borrow checker but still rejects some provably safe code. Key difference from OCaml: 1. **NLL limitation**: Rust's NLL rejects get

Tutorial

The Problem

NLL (Non-Lexical Lifetimes) dramatically improved Rust's borrow checker but still rejects some provably safe code. The most famous example: looking up a key in a map, and if absent, inserting and returning a reference to the new value — the classic "get-or-insert" pattern. NLL conservatively rejects this because the mutable borrow for the lookup overlaps with the mutable borrow for the insert, even though they cannot both be active simultaneously. Polonius, the next-generation borrow checker based on Datalog constraints, accepts this pattern and other currently-rejected safe code.

🎯 Learning Outcomes

  • • What patterns NLL conservatively rejects that are provably safe
  • • The classic "get-or-insert" pattern and why NLL's two-phase borrow check fails on it
  • • How to work around NLL limitations using contains_key + separate get, or the entry API
  • • What Polonius is and how it improves over NLL using Datalog-based constraint solving
  • • Where Polonius matters most: HashMap get-or-insert, conditional borrow returns
  • Code Example

    // NLL rejects: borrow in match conflicts with insert
    // Workaround: check first, then mutate
    pub fn get_or_insert<'a>(
        map: &'a mut HashMap<String, String>,
        key: String,
    ) -> &'a str {
        if !map.contains_key(&key) {
            map.insert(key.clone(), format!("default_{}", key));
        }
        map.get(&key).unwrap()
    }
    
    // Or use entry API
    map.entry(key).or_insert_with(|| compute())

    Key Differences

  • NLL limitation: Rust's NLL rejects get-or-insert patterns even when provably safe; OCaml's lack of borrow checking means no such limitation exists.
  • Entry API: Rust's HashMap::entry was specifically designed to work around NLL limitations — it is the idiomatic solution; OCaml has no equivalent API pattern needed.
  • Polonius timeline: Polonius is a research project that will replace NLL — it accepts all NLL-accepted programs plus additional safe programs; OCaml's model is orthogonal.
  • Workaround cost: NLL workarounds for get-or-insert require double lookups (one contains_key, one get) — a performance cost that Polonius would eliminate; OCaml pays no such cost.
  • OCaml Approach

    OCaml's Hashtbl and functional Map have no borrow checker limitations — get-or-insert is direct:

    let get_or_insert tbl key default =
      match Hashtbl.find_opt tbl key with
      | Some v -> v
      | None -> Hashtbl.add tbl key default; default
    

    No workaround needed — the hash table is always mutably accessible through its reference.

    Full Source

    #![allow(clippy::all)]
    //! Polonius Borrow Checker Concepts
    //!
    //! Patterns where current NLL is conservative; Polonius accepts.
    
    use std::collections::HashMap;
    
    /// Classic Polonius example: get-or-insert.
    /// NLL-friendly workaround using contains_key.
    pub fn get_or_insert<'a>(map: &'a mut HashMap<String, String>, key: String) -> &'a str {
        if !map.contains_key(&key) {
            map.insert(key.clone(), format!("default_{}", key));
        }
        map.get(&key).unwrap()
    }
    
    /// Another workaround: use entry API.
    pub fn get_or_insert_entry<'a>(map: &'a mut HashMap<String, String>, key: String) -> &'a str {
        map.entry(key.clone())
            .or_insert_with(|| format!("default_{}", key))
    }
    
    /// Pattern that Polonius would accept directly.
    /// We work around by returning Option differently.
    pub fn find_or_create(items: &mut Vec<String>, target: &str) -> usize {
        for (i, item) in items.iter().enumerate() {
            if item == target {
                return i;
            }
        }
        items.push(target.to_string());
        items.len() - 1
    }
    
    /// Conditional return pattern.
    pub fn get_cached<'a>(cache: &'a mut HashMap<i32, String>, key: i32) -> &'a str {
        if !cache.contains_key(&key) {
            cache.insert(key, format!("computed_{}", key));
        }
        cache.get(&key).unwrap()
    }
    
    /// Split the logic to help the borrow checker.
    fn compute_if_missing(cache: &mut HashMap<i32, String>, key: i32) {
        if !cache.contains_key(&key) {
            cache.insert(key, format!("value_{}", key));
        }
    }
    
    pub fn get_with_helper<'a>(cache: &'a mut HashMap<i32, String>, key: i32) -> &'a str {
        compute_if_missing(cache, key);
        cache.get(&key).unwrap()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_or_insert() {
            let mut map = HashMap::new();
            let v1 = get_or_insert(&mut map, "key1".into());
            assert!(v1.contains("default_key1"));
        }
    
        #[test]
        fn test_get_or_insert_entry() {
            let mut map = HashMap::new();
            let v1 = get_or_insert_entry(&mut map, "key2".into());
            assert!(v1.contains("default_key2"));
        }
    
        #[test]
        fn test_find_or_create_existing() {
            let mut items = vec!["a".into(), "b".into(), "c".into()];
            let idx = find_or_create(&mut items, "b");
            assert_eq!(idx, 1);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_find_or_create_new() {
            let mut items = vec!["a".into(), "b".into()];
            let idx = find_or_create(&mut items, "c");
            assert_eq!(idx, 2);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_get_cached() {
            let mut cache = HashMap::new();
            let v = get_cached(&mut cache, 42);
            assert!(v.contains("42"));
        }
    
        #[test]
        fn test_get_with_helper() {
            let mut cache = HashMap::new();
            let v = get_with_helper(&mut cache, 7);
            assert!(v.contains("7"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_or_insert() {
            let mut map = HashMap::new();
            let v1 = get_or_insert(&mut map, "key1".into());
            assert!(v1.contains("default_key1"));
        }
    
        #[test]
        fn test_get_or_insert_entry() {
            let mut map = HashMap::new();
            let v1 = get_or_insert_entry(&mut map, "key2".into());
            assert!(v1.contains("default_key2"));
        }
    
        #[test]
        fn test_find_or_create_existing() {
            let mut items = vec!["a".into(), "b".into(), "c".into()];
            let idx = find_or_create(&mut items, "b");
            assert_eq!(idx, 1);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_find_or_create_new() {
            let mut items = vec!["a".into(), "b".into()];
            let idx = find_or_create(&mut items, "c");
            assert_eq!(idx, 2);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_get_cached() {
            let mut cache = HashMap::new();
            let v = get_cached(&mut cache, 42);
            assert!(v.contains("42"));
        }
    
        #[test]
        fn test_get_with_helper() {
            let mut cache = HashMap::new();
            let v = get_with_helper(&mut cache, 7);
            assert!(v.contains("7"));
        }
    }

    Deep Comparison

    OCaml vs Rust: Polonius / Advanced Borrow Checking

    OCaml

    (* No borrow checking — hashtbl mutation is direct *)
    let get_or_insert tbl key =
      try Hashtbl.find tbl key
      with Not_found ->
        let v = "default_" ^ key in
        Hashtbl.add tbl key v;
        v
    

    Rust (NLL workaround)

    // NLL rejects: borrow in match conflicts with insert
    // Workaround: check first, then mutate
    pub fn get_or_insert<'a>(
        map: &'a mut HashMap<String, String>,
        key: String,
    ) -> &'a str {
        if !map.contains_key(&key) {
            map.insert(key.clone(), format!("default_{}", key));
        }
        map.get(&key).unwrap()
    }
    
    // Or use entry API
    map.entry(key).or_insert_with(|| compute())
    

    Key Differences

  • OCaml: No borrow tracking, direct mutation
  • Rust NLL: Conservative in some patterns
  • Rust Polonius: More precise flow analysis (experimental)
  • Rust: Entry API is idiomatic workaround
  • Both: Achieve same functionality, different ergonomics
  • Exercises

  • Entry API mastery: Rewrite get_or_insert using only the Entry API with or_insert, or_insert_with, and or_default variants — compare the verbosity of each.
  • Two-pass workaround: Implement a fn get_or_compute<'a>(map: &'a mut HashMap<i32, i32>, key: i32, f: impl Fn(i32) -> i32) -> &'a i32 using the NLL workaround (two lookups).
  • Find-or-add dedup: Write fn dedup_insert(v: &mut Vec<String>, s: String) -> usize that returns the index of s if already present, or inserts it and returns the new index — without cloning s unnecessarily.
  • Open Source Repos