Non-Lexical Lifetimes (NLL)
Tutorial Video
Text description (accessibility)
This video demonstrates the "Non-Lexical Lifetimes (NLL)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Before Rust 2018's Non-Lexical Lifetimes (NLL), the borrow checker used lexical scopes to determine when borrows ended — a borrow lasted until the end of the enclosing block, even if the borrowed value was never used after its last access point. Key difference from OCaml: 1. **Scope vs use**: Pre
Tutorial
The Problem
Before Rust 2018's Non-Lexical Lifetimes (NLL), the borrow checker used lexical scopes to determine when borrows ended — a borrow lasted until the end of the enclosing block, even if the borrowed value was never used after its last access point. This caused many correct programs to be rejected: you could not borrow from a Vec, compute something, then push to the Vec in the same block, even though the first borrow logically ended before the push. NLL (stabilized in Rust 2018) makes borrows end at their last use, not at the end of their enclosing scope.
🎯 Learning Outcomes
let first = v[0]; v.push(6); now compiles with NLL (borrow ends after v[0])nll_match demonstrates split borrows with pattern matchingCode Example
pub fn nll_basic() -> Vec<i32> {
let mut v = vec![1, 2, 3];
let first = v[0]; // borrow ends here (NLL)
v.push(6); // OK: borrow already ended
v
}
// Pre-NLL: error! borrow lasted until end of blockKey Differences
OCaml Approach
OCaml has no borrow checker — all mutation is safe through GC-managed references. The equivalent patterns work without restriction:
let nll_basic () =
let v = ref [1; 2; 3; 4; 5] in
let first = List.hd !v in
v := !v @ [6];
(first, !v)
There is no concept of a borrow ending — the GC handles everything.
Full Source
#![allow(clippy::all)]
//! Non-Lexical Lifetimes (NLL)
//!
//! Modern borrow checker: borrows end at last use, not end of block.
/// NLL allows mutation after last borrow use.
pub fn nll_basic() -> Vec<i32> {
let mut v = vec![1, 2, 3, 4, 5];
let first = v[0]; // borrow ends after this line
v.push(6); // OK with NLL
assert_eq!(first, 1);
v
}
/// NLL enables conditional borrows.
pub fn nll_conditional(data: &mut Vec<i32>, add: bool) {
let first = data.first().copied();
if add {
data.push(42); // OK: first borrow ended
}
if let Some(f) = first {
println!("First was: {}", f);
}
}
/// NLL with match arms.
pub fn nll_match(opt: &mut Option<String>) -> Option<&str> {
match opt {
Some(s) => Some(s.as_str()),
None => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nll_basic() {
let v = nll_basic();
assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_nll_conditional() {
let mut data = vec![1, 2, 3];
nll_conditional(&mut data, true);
assert_eq!(data.len(), 4);
}
#[test]
fn test_nll_match() {
let mut opt = Some(String::from("hello"));
let result = nll_match(&mut opt);
assert_eq!(result, Some("hello"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nll_basic() {
let v = nll_basic();
assert_eq!(v, vec![1, 2, 3, 4, 5, 6]);
}
#[test]
fn test_nll_conditional() {
let mut data = vec![1, 2, 3];
nll_conditional(&mut data, true);
assert_eq!(data.len(), 4);
}
#[test]
fn test_nll_match() {
let mut opt = Some(String::from("hello"));
let result = nll_match(&mut opt);
assert_eq!(result, Some("hello"));
}
}
Deep Comparison
OCaml vs Rust: Non-Lexical Lifetimes
OCaml
(* No concept of borrows — GC manages memory *)
let example () =
let v = [1; 2; 3] in
let first = List.hd v in
(* No restrictions on v after "borrowing" *)
first
Rust (NLL - Rust 2018+)
pub fn nll_basic() -> Vec<i32> {
let mut v = vec![1, 2, 3];
let first = v[0]; // borrow ends here (NLL)
v.push(6); // OK: borrow already ended
v
}
// Pre-NLL: error! borrow lasted until end of block
Key Differences
Exercises
nll_basic using the pre-NLL workaround (explicit scope with {}) and verify both versions compile correctly.Vec<i32>, removes it (using retain), and appends its square — demonstrate NLL allows this without explicit scoping.fn first_or_default<'a>(v: &'a Vec<String>, default: &'a str) -> &'a str that returns the first element or default, using a match expression on v.first().