Polonius Borrow Checker Concepts
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
contains_key + separate get, or the entry APICode 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
HashMap::entry was specifically designed to work around NLL limitations — it is the idiomatic solution; OCaml has no equivalent API pattern needed.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"));
}
}#[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
Exercises
get_or_insert using only the Entry API with or_insert, or_insert_with, and or_default variants — compare the verbosity of each.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).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.