ExamplesBy LevelBy TopicLearning Paths
517 Intermediate

Closure-to-Function-Pointer Coercion

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure-to-Function-Pointer Coercion" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. C and systems programming have long relied on function pointers for callbacks — they are a fixed machine-word size, require no heap allocation, and map directly to a call instruction. Key difference from OCaml: 1. **Uniform representation**: Rust `fn` pointers are a single pointer word with no closure environment; OCaml always has a (pointer, environment) pair even for non

Tutorial

The Problem

C and systems programming have long relied on function pointers for callbacks — they are a fixed machine-word size, require no heap allocation, and map directly to a call instruction. Rust preserves this capability: non-capturing closures and named functions both coerce to fn pointer types, enabling zero-overhead callbacks in FFI-compatible APIs. The constraint is intentional — a capturing closure has extra data that a raw pointer cannot represent. Understanding when coercion works and when it fails helps you choose between fn, impl Fn, and Box<dyn Fn>.

🎯 Learning Outcomes

  • • Why non-capturing closures coerce to fn pointers but capturing ones do not
  • • How named functions serve as fn pointer values
  • • How to build dispatch tables using arrays of fn pointers (uniform size, no fat pointer)
  • • When to use fn vs impl Fn vs Box<dyn Fn> for different API shapes
  • • How FFI callbacks require fn pointer types for ABI compatibility
  • Code Example

    // Non-capturing closure coerces to fn pointer
    let f: fn(i32) -> i32 = |x| x * 2;  // OK
    
    // Capturing closure CANNOT coerce to fn pointer
    let n = 3;
    // let f: fn(i32) -> i32 = |x| x + n;  // ERROR!
    
    // Must use Box<dyn Fn> for capturing closures
    let f: Box<dyn Fn(i32) -> i32> = Box::new(move |x| x + n);

    Key Differences

  • Uniform representation: Rust fn pointers are a single pointer word with no closure environment; OCaml always has a (pointer, environment) pair even for non-capturing functions.
  • FFI compatibility: Rust fn pointers map directly to C function pointers — usable in extern "C" callbacks without wrappers; OCaml requires ctypes machinery or Callback.register.
  • Size guarantees: Rust fn pointers have a known, fixed size enabling [fn(T) -> U; N] arrays; OCaml function values are opaque pointers of uniform size too, but via GC indirection.
  • Type safety: Rust distinguishes fn(i32) -> i32 from fn(i64) -> i32 at the type level with no implicit coercion; OCaml's type system similarly rejects mismatched function types at compile time.
  • OCaml Approach

    OCaml has no function pointer / closure distinction at the value level — all functions are closures, and closed-over environments are heap-allocated. There is no direct coercion concept. For C FFI, OCaml uses ctypes or Callback.register, which wrap OCaml functions behind C-callable thunks. Performance-sensitive dispatch uses arrays of functions just as in Rust, but every entry is a closure regardless.

    let ops = [| (fun x -> x * 2); (fun x -> x * 3) |]
    let apply i x = ops.(i) x
    

    Full Source

    #![allow(clippy::all)]
    //! Closure-to-fn-pointer Coercion
    //!
    //! Non-capturing closures coerce to fn pointers; capturing ones cannot.
    
    /// Accept a function pointer explicitly.
    pub fn apply_fn_ptr(f: fn(i32) -> i32, x: i32) -> i32 {
        f(x)
    }
    
    /// Named functions are fn pointers.
    pub fn double(x: i32) -> i32 {
        x * 2
    }
    
    pub fn triple(x: i32) -> i32 {
        x * 3
    }
    
    pub fn negate(x: i32) -> i32 {
        -x
    }
    
    /// Array of function pointers (all same size — no fat pointers).
    pub fn make_transform_table() -> [fn(i32) -> i32; 4] {
        [
            double,
            triple,
            negate,
            |x| x + 10, // non-capturing closure coerces to fn ptr
        ]
    }
    
    /// Function that stores fn pointers in a Vec.
    pub fn build_pipeline(ops: Vec<fn(i32) -> i32>) -> impl Fn(i32) -> i32 {
        move |x| ops.iter().fold(x, |acc, f| f(acc))
    }
    
    /// Demonstrate coercion rules.
    pub fn coercion_demo() {
        // Non-capturing closure → fn pointer: OK
        let _: fn(i32) -> i32 = |x| x * 2;
    
        // Named function → fn pointer: OK
        let _: fn(i32) -> i32 = double;
    
        // Capturing closure → fn pointer: NOT OK (won't compile)
        // let y = 5;
        // let _: fn(i32) -> i32 = |x| x + y;  // ERROR!
    }
    
    /// C FFI often requires fn pointers.
    pub type Callback = fn(i32) -> i32;
    
    pub fn register_callback(cb: Callback) -> i32 {
        cb(100)
    }
    
    /// When you need to store capturing closures, use Box<dyn Fn>.
    pub fn store_capturing_closures() -> Vec<Box<dyn Fn(i32) -> i32>> {
        let a = 5;
        let b = 10;
        vec![
            Box::new(|x| x + 1),      // non-capturing
            Box::new(move |x| x + a), // capturing
            Box::new(move |x| x * b), // capturing
        ]
    }
    
    /// Size comparison: fn pointers vs closures.
    pub fn size_demo() -> (usize, usize, usize) {
        let fn_ptr_size = std::mem::size_of::<fn(i32) -> i32>();
        let closure_size = std::mem::size_of_val(&|x: i32| x * 2);
        let capturing_size = {
            let y = 42i32;
            std::mem::size_of_val(&move |x: i32| x + y)
        };
        (fn_ptr_size, closure_size, capturing_size)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply_fn_ptr_with_function() {
            assert_eq!(apply_fn_ptr(double, 5), 10);
            assert_eq!(apply_fn_ptr(triple, 5), 15);
            assert_eq!(apply_fn_ptr(negate, 5), -5);
        }
    
        #[test]
        fn test_apply_fn_ptr_with_closure() {
            // Non-capturing closure coerces to fn pointer
            assert_eq!(apply_fn_ptr(|x| x + 1, 5), 6);
            assert_eq!(apply_fn_ptr(|x| x * x, 5), 25);
        }
    
        #[test]
        fn test_transform_table() {
            let table = make_transform_table();
            assert_eq!(table[0](5), 10); // double
            assert_eq!(table[1](5), 15); // triple
            assert_eq!(table[2](5), -5); // negate
            assert_eq!(table[3](5), 15); // +10
        }
    
        #[test]
        fn test_build_pipeline() {
            let pipeline = build_pipeline(vec![double, |x| x + 1, triple]);
            // (5 * 2 + 1) * 3 = 33
            assert_eq!(pipeline(5), 33);
        }
    
        #[test]
        fn test_register_callback() {
            assert_eq!(register_callback(double), 200);
            assert_eq!(register_callback(|x| x / 2), 50);
        }
    
        #[test]
        fn test_store_capturing_closures() {
            let closures = store_capturing_closures();
            assert_eq!(closures[0](10), 11); // +1
            assert_eq!(closures[1](10), 15); // +5
            assert_eq!(closures[2](10), 100); // *10
        }
    
        #[test]
        fn test_fn_pointer_size() {
            let (fn_size, non_cap, _cap) = size_demo();
            // fn pointer is one pointer size
            assert_eq!(fn_size, std::mem::size_of::<usize>());
            // non-capturing closure is zero-sized
            assert_eq!(non_cap, 0);
        }
    
        #[test]
        fn test_explicit_coercion() {
            let f: fn(i32) -> i32 = |x| x * 2;
            assert_eq!(f(21), 42);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_apply_fn_ptr_with_function() {
            assert_eq!(apply_fn_ptr(double, 5), 10);
            assert_eq!(apply_fn_ptr(triple, 5), 15);
            assert_eq!(apply_fn_ptr(negate, 5), -5);
        }
    
        #[test]
        fn test_apply_fn_ptr_with_closure() {
            // Non-capturing closure coerces to fn pointer
            assert_eq!(apply_fn_ptr(|x| x + 1, 5), 6);
            assert_eq!(apply_fn_ptr(|x| x * x, 5), 25);
        }
    
        #[test]
        fn test_transform_table() {
            let table = make_transform_table();
            assert_eq!(table[0](5), 10); // double
            assert_eq!(table[1](5), 15); // triple
            assert_eq!(table[2](5), -5); // negate
            assert_eq!(table[3](5), 15); // +10
        }
    
        #[test]
        fn test_build_pipeline() {
            let pipeline = build_pipeline(vec![double, |x| x + 1, triple]);
            // (5 * 2 + 1) * 3 = 33
            assert_eq!(pipeline(5), 33);
        }
    
        #[test]
        fn test_register_callback() {
            assert_eq!(register_callback(double), 200);
            assert_eq!(register_callback(|x| x / 2), 50);
        }
    
        #[test]
        fn test_store_capturing_closures() {
            let closures = store_capturing_closures();
            assert_eq!(closures[0](10), 11); // +1
            assert_eq!(closures[1](10), 15); // +5
            assert_eq!(closures[2](10), 100); // *10
        }
    
        #[test]
        fn test_fn_pointer_size() {
            let (fn_size, non_cap, _cap) = size_demo();
            // fn pointer is one pointer size
            assert_eq!(fn_size, std::mem::size_of::<usize>());
            // non-capturing closure is zero-sized
            assert_eq!(non_cap, 0);
        }
    
        #[test]
        fn test_explicit_coercion() {
            let f: fn(i32) -> i32 = |x| x * 2;
            assert_eq!(f(21), 42);
        }
    }

    Deep Comparison

    OCaml vs Rust: Closure/Function Pointer Coercion

    OCaml

    (* All functions have the same representation *)
    let double x = x * 2
    let add_n n x = x + n
    
    (* No distinction between fn pointers and closures *)
    let apply f x = f x
    let _ = apply double 5
    let _ = apply (add_n 3) 5
    

    Rust

    // Non-capturing closure coerces to fn pointer
    let f: fn(i32) -> i32 = |x| x * 2;  // OK
    
    // Capturing closure CANNOT coerce to fn pointer
    let n = 3;
    // let f: fn(i32) -> i32 = |x| x + n;  // ERROR!
    
    // Must use Box<dyn Fn> for capturing closures
    let f: Box<dyn Fn(i32) -> i32> = Box::new(move |x| x + n);
    

    Key Differences

  • OCaml: Uniform function representation — no distinction
  • Rust: fn pointers are thin, closures may carry captured data
  • Rust: Non-capturing closures are zero-sized, coerce to fn ptr
  • Rust: Capturing closures need Box<dyn Fn> for storage
  • Rust: fn pointers required for C FFI interop
  • Exercises

  • FFI table: Build a [fn(i32) -> i32; N] dispatch table and write a function that takes an index and applies the corresponding operation, returning an error variant for out-of-bounds indices.
  • Pipeline from strings: Write build_named_pipeline(ops: &[&str]) that looks up named operations ("double", "negate", etc.) in a HashMap<&str, fn(i32) -> i32> and returns a composed fn pipeline.
  • Capturing fallback: Demonstrate that Box<dyn Fn(i32) -> i32> can hold both fn pointers and capturing closures, and write a dispatcher that tries a fn pointer table first and falls back to a Box<dyn Fn> registry.
  • Open Source Repos