๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Iterator chainsEager โ€” intermediate lists allocatedLazy โ€” single fused loop
ClosuresHeap-allocated (GC captures environment)Monomorphized โ€” inline struct, no allocation
NewtypesConstructor overhead (GC-boxed)Zero cost โ€” same bit pattern as inner type
HOF costReal allocation per callZero โ€” 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 allocations

Newtypes

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