119: Zero-Cost Abstractions
Difficulty: 2 Level: Intermediate Iterator chains, closures, and newtypes compile to the same machine code as hand-written equivalents โ abstraction with no runtime penalty.The Problem This Solves
In many languages, using higher-order functions comes with a cost: closures allocate on the heap, `map`/`filter` create intermediate collections, and calling a function through a pointer prevents inlining. Programmers face a real trade-off between readable, composable code and code that runs fast. Rust breaks this trade-off. Iterator adapters like `filter`, `map`, and `sum` are lazy โ they produce no intermediate `Vec`. The compiler fuses the chain into a single loop. Closures are not heap-allocated; each closure gets its own anonymous struct type and is monomorphized at the call site, where LLVM can inline it completely. The result is assembly that's identical (or better) than a hand-written `for` loop with explicit `if` guards. The same principle applies to newtypes. `struct Meters(f64)` is a distinct type that prevents you from passing a `Seconds` where a `Meters` is expected โ it's a real compile-time safety guarantee โ but at runtime it's just `f64`. No boxing, no indirection, no header.The Intuition
Rust's abstractions are instructions to the compiler, not instructions to the CPU โ they disappear at compile time, leaving only the minimum necessary machine code.How It Works in Rust
// Iterator chain โ no intermediate Vec, compiles to a single loop
let result: i64 = (0..1000)
.filter(|x| x % 2 == 0) // lazy โ no allocation yet
.map(|x| x * x) // lazy โ still no allocation
.sum(); // consumes the chain in one pass
// Exactly equivalent to this hand-written loop:
let mut manual = 0i64;
for x in 0..1000 {
if x % 2 == 0 { manual += x * x; }
}
assert_eq!(result, manual); // same result, same assembly
// Closures โ monomorphized, not heap-allocated
fn make_polynomial(coeffs: Vec<f64>) -> impl Fn(f64) -> f64 {
// The closure captures `coeffs` by move.
// The compiler creates a unique anonymous struct for this closure.
// The returned `impl Fn(f64) -> f64` is inlined at call sites โ no vtable.
move |x| coeffs.iter().enumerate()
.map(|(i, &c)| c * x.powi(i as i32))
.sum()
}
// Newtypes โ zero runtime cost, real compile-time safety
struct Meters(f64);
struct Seconds(f64);
fn speed(d: Meters, t: Seconds) -> f64 { d.0 / t.0 }
// speed(t, d) // ERROR: mismatched types โ the compiler catches the swap
// size_of::<Meters>() == size_of::<f64>() // same bits at runtime
What This Unlocks
- Readable pipelines โ chain `.filter().map().fold()` without worrying that you're writing slow code.
- Type-safe domain models โ `Meters`, `Seconds`, `Euros`, `Pixels` as distinct types prevent unit confusion bugs with zero runtime overhead.
- Returned closures and iterators โ `impl Fn(...)` and `impl Iterator` let you build factory functions whose output is fully inlined at the call site.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Iterator chains | Eager โ intermediate lists allocated | Lazy โ single fused loop |
| Closures | Heap-allocated (GC captures environment) | Monomorphized โ inline struct, no allocation |
| Newtypes | Constructor overhead (GC-boxed) | Zero cost โ same bit pattern as inner type |
| HOF cost | Real allocation per call | Zero โ compiler eliminates abstraction |
// Example 119: Zero-Cost Abstractions
//
// Rust's closures, iterators, and newtypes compile to the same
// machine code as hand-written loops and raw values.
// Approach 1: Iterator chains compile to loops
fn approach1() {
// This chain compiles to a SINGLE loop โ no intermediate allocations
let result: i64 = (0..1000)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();
// Equivalent hand-written loop (same assembly!)
let mut manual_result: i64 = 0;
for x in 0..1000 {
if x % 2 == 0 {
manual_result += x * x;
}
}
assert_eq!(result, manual_result);
println!("Sum of even squares (0..999): {}", result);
}
// Approach 2: Closures monomorphized โ no allocation
fn make_polynomial(coeffs: Vec<f64>) -> impl Fn(f64) -> f64 {
move |x| {
coeffs.iter().enumerate()
.map(|(i, &c)| c * x.powi(i as i32))
.sum()
}
}
fn approach2() {
let poly = make_polynomial(vec![1.0, 2.0, 3.0]); // 1 + 2x + 3xยฒ
let result = poly(2.0); // 1 + 4 + 12 = 17
assert!((result - 17.0).abs() < f64::EPSILON);
println!("p(2) = {:.1}", result);
}
// Approach 3: Newtype pattern โ zero runtime cost
#[derive(Debug, Clone, Copy)]
struct Meters(f64);
#[derive(Debug, Clone, Copy)]
struct Seconds(f64);
#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(f64);
fn speed(d: Meters, t: Seconds) -> MetersPerSecond {
MetersPerSecond(d.0 / t.0)
}
fn approach3() {
let d = Meters(100.0);
let t = Seconds(9.58);
let s = speed(d, t);
// Meters and Seconds are zero-cost โ same as raw f64 at runtime
// But the compiler prevents: speed(t, d) โ type mismatch!
println!("Speed: {:.2} m/s", s.0);
assert!(s.0 > 10.0);
}
fn main() {
println!("=== Approach 1: Iterator = Loop ===");
approach1();
println!("\n=== Approach 2: Monomorphized Closures ===");
approach2();
println!("\n=== Approach 3: Newtypes ===");
approach3();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_iterator_equals_loop() {
let iter_sum: i32 = (1..=100).sum();
let mut loop_sum = 0;
for i in 1..=100 { loop_sum += i; }
assert_eq!(iter_sum, loop_sum);
}
#[test]
fn test_polynomial() {
let p = make_polynomial(vec![1.0, 0.0, 1.0]); // 1 + xยฒ
assert!((p(0.0) - 1.0).abs() < f64::EPSILON);
assert!((p(3.0) - 10.0).abs() < f64::EPSILON);
}
#[test]
fn test_newtype_zero_cost() {
let m = Meters(42.0);
assert_eq!(std::mem::size_of::<Meters>(), std::mem::size_of::<f64>());
assert_eq!(m.0, 42.0);
}
#[test]
fn test_speed_calculation() {
let s = speed(Meters(100.0), Seconds(10.0));
assert!((s.0 - 10.0).abs() < f64::EPSILON);
}
}
(* Example 119: Zero-Cost Abstractions *)
(* OCaml closures and higher-order functions have some overhead
(allocation, indirect calls). Rust compiles them away. *)
(* Approach 1: Iterator-style processing *)
let approach1 () =
let data = List.init 1000 (fun i -> i) in
let result = List.fold_left ( + ) 0
(List.map (fun x -> x * x)
(List.filter (fun x -> x mod 2 = 0) data)) in
Printf.printf "Sum of even squares (0..999): %d\n" result
(* Approach 2: Closure-based computation *)
let make_polynomial coeffs =
fun x ->
let rec eval acc power = function
| [] -> acc
| c :: rest -> eval (acc +. c *. (x ** power)) (power +. 1.0) rest
in
eval 0.0 0.0 coeffs
let approach2 () =
let poly = make_polynomial [1.0; 2.0; 3.0] in (* 1 + 2x + 3x^2 *)
let result = poly 2.0 in (* 1 + 4 + 12 = 17 *)
assert (result = 17.0);
Printf.printf "p(2) = %.1f\n" result
(* Approach 3: Newtype pattern *)
type meters = Meters of float
type seconds = Seconds of float
let speed (Meters d) (Seconds t) = d /. t
let approach3 () =
let d = Meters 100.0 in
let t = Seconds 9.58 in
let s = speed d t in
Printf.printf "Speed: %.2f m/s\n" s;
assert (s > 10.0)
let () =
approach1 ();
approach2 ();
approach3 ();
Printf.printf "โ All tests passed\n"
๐ Detailed Comparison
Comparison: Zero-Cost Abstractions
Iterator Chains
OCaml โ allocates intermediate lists:
๐ช Show OCaml equivalent
let result = List.fold_left ( + ) 0
(List.map (fun x -> x * x)
(List.filter (fun x -> x mod 2 = 0) data))
(* Creates: filtered list, mapped list, then folds *)
Rust โ single fused loop:
let result: i64 = (0..1000)
.filter(|x| x % 2 == 0)
.map(|x| x * x)
.sum();
// Compiles to ONE loop โ no intermediate allocationsNewtypes
OCaml โ constructor has overhead:
๐ช Show OCaml equivalent
type meters = Meters of float
let speed (Meters d) (Seconds t) = d /. t
(* Meters wraps float โ allocation + pattern match *)
Rust โ truly zero-cost:
struct Meters(f64); // same as f64 at runtime
fn speed(d: Meters, t: Seconds) -> MetersPerSecond {
MetersPerSecond(d.0 / t.0)
}
// Compiles to: f64 / f64 โ no wrapper overhead