ExamplesBy LevelBy TopicLearning Paths
519 Intermediate

Closure Type Inference

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Type Inference" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Type inference for closures is a quality-of-life feature that distinguishes modern functional-leaning languages from older systems languages. Key difference from OCaml: 1. **Polymorphic closures**: OCaml can produce a polymorphic closure `'a

Tutorial

The Problem

Type inference for closures is a quality-of-life feature that distinguishes modern functional-leaning languages from older systems languages. In Rust, the compiler infers closure parameter and return types from the context in which the closure is first used — similar to how Hindley-Milner inference works in ML-family languages. However, unlike full HM inference, Rust locks in a closure's type at its first use site and rejects subsequent calls with different types. Understanding these rules helps avoid cryptic type errors when composing iterators and higher-order functions.

🎯 Learning Outcomes

  • • How Rust infers closure parameter types from their first use
  • • Why closures have unique, anonymous types that must be captured via generics or boxing
  • • When explicit type annotations are required and when they are redundant
  • • Why the same closure cannot be called with two different argument types
  • • How apply<F, T, U>(f: F, x: T) -> U generalizes over closure types
  • Code Example

    // Inferred from first use, then fixed
    let double = |x| x * 2;
    let _ = double(5i32);  // fixes type forever
    
    // Closures are monomorphic — not polymorphic
    let id = |x| x;
    let _: i32 = id(5);    // fixed as i32
    // id("hello");        // ERROR: already fixed

    Key Differences

  • Polymorphic closures: OCaml can produce a polymorphic closure 'a -> 'a; Rust closures have a single unique type — true polymorphism requires a trait bound on a generic parameter.
  • Lock-in rule: Rust fixes the closure type at first use, making later calls with different types a hard error; OCaml unifies types across uses in the same scope.
  • Type annotation frequency: Rust closures rarely need annotations when used immediately with concrete types; OCaml type annotations are often omitted entirely due to HM inference.
  • Error messages: Rust type errors for closures point to the conflicting use site; OCaml errors often point to the unification failure between two constraints, which can be further away.
  • OCaml Approach

    OCaml uses the Hindley-Milner algorithm with full let-polymorphism. Closures infer types independently at each use, and a value-restriction applies to prevent unsound generalization of mutable values. Unlike Rust, OCaml can generalize let f = fun x -> x to 'a -> 'a — a genuinely polymorphic identity closure.

    let apply f x = f x       (* 'a -> 'b inferred *)
    let double = fun x -> x * 2  (* int -> int inferred from * *)
    

    Full Source

    #![allow(clippy::all)]
    //! Closure Type Inference
    //!
    //! How Rust infers closure types and when annotations are needed.
    
    /// Apply a function to a value.
    pub fn apply<F, T, U>(f: F, x: T) -> U
    where
        F: Fn(T) -> U,
    {
        f(x)
    }
    
    /// Demonstrates type inference in closures.
    pub fn inference_demo() -> Vec<i32> {
        // Type inferred from first use
        let double = |x| x * 2;
        let _ = double(5i32); // fixes type as i32
    
        // Explicit input type, inferred return
        let square = |x: i32| x * x;
    
        // Inferred from context
        let nums = vec![1, 2, 3, 4, 5];
        nums.iter().map(|&x| square(double(x))).collect()
    }
    
    /// When type context is needed.
    pub fn needs_annotation<T: std::ops::Add<Output = T> + Copy>(x: T, y: T) -> T {
        let add = |a: T, b: T| a + b;
        add(x, y)
    }
    
    /// Multiple uses must be consistent.
    pub fn consistent_types() {
        let process = |x| x + 1;
        let _: i32 = process(5);
        // process(5.0); // ERROR: already fixed as i32
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_basic_inference() {
            let double = |x| x * 2;
            assert_eq!(double(5), 10);
        }
    
        #[test]
        fn test_explicit_input() {
            let square = |x: i32| x * x;
            assert_eq!(square(4), 16);
        }
    
        #[test]
        fn test_inference_demo() {
            let result = inference_demo();
            // (1*2)^2, (2*2)^2, (3*2)^2, (4*2)^2, (5*2)^2
            assert_eq!(result, vec![4, 16, 36, 64, 100]);
        }
    
        #[test]
        fn test_apply_generic() {
            assert_eq!(apply(|x: i32| x + 1, 5), 6);
            assert_eq!(apply(|s: &str| s.len(), "hello"), 5);
        }
    
        #[test]
        fn test_needs_annotation() {
            assert_eq!(needs_annotation(3i32, 4i32), 7);
            assert_eq!(needs_annotation(3.0f64, 4.0f64), 7.0);
        }
    
        #[test]
        fn test_iterator_context() {
            let nums: Vec<i32> = vec![1, 2, 3];
            let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();
            assert_eq!(doubled, vec![2, 4, 6]);
        }
    
        #[test]
        fn test_closure_in_struct() {
            struct Holder<F> {
                f: F,
            }
    
            let h = Holder { f: |x: i32| x + 1 };
            assert_eq!((h.f)(5), 6);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_basic_inference() {
            let double = |x| x * 2;
            assert_eq!(double(5), 10);
        }
    
        #[test]
        fn test_explicit_input() {
            let square = |x: i32| x * x;
            assert_eq!(square(4), 16);
        }
    
        #[test]
        fn test_inference_demo() {
            let result = inference_demo();
            // (1*2)^2, (2*2)^2, (3*2)^2, (4*2)^2, (5*2)^2
            assert_eq!(result, vec![4, 16, 36, 64, 100]);
        }
    
        #[test]
        fn test_apply_generic() {
            assert_eq!(apply(|x: i32| x + 1, 5), 6);
            assert_eq!(apply(|s: &str| s.len(), "hello"), 5);
        }
    
        #[test]
        fn test_needs_annotation() {
            assert_eq!(needs_annotation(3i32, 4i32), 7);
            assert_eq!(needs_annotation(3.0f64, 4.0f64), 7.0);
        }
    
        #[test]
        fn test_iterator_context() {
            let nums: Vec<i32> = vec![1, 2, 3];
            let doubled: Vec<i32> = nums.iter().map(|&x| x * 2).collect();
            assert_eq!(doubled, vec![2, 4, 6]);
        }
    
        #[test]
        fn test_closure_in_struct() {
            struct Holder<F> {
                f: F,
            }
    
            let h = Holder { f: |x: i32| x + 1 };
            assert_eq!((h.f)(5), 6);
        }
    }

    Deep Comparison

    OCaml vs Rust: Closure Type Inference

    OCaml

    (* Full Hindley-Milner inference *)
    let double = fun x -> x * 2  (* inferred: int -> int *)
    let id x = x                 (* polymorphic: 'a -> 'a *)
    

    Rust

    // Inferred from first use, then fixed
    let double = |x| x * 2;
    let _ = double(5i32);  // fixes type forever
    
    // Closures are monomorphic — not polymorphic
    let id = |x| x;
    let _: i32 = id(5);    // fixed as i32
    // id("hello");        // ERROR: already fixed
    

    Key Differences

  • OCaml: Full polymorphic inference (Hindley-Milner)
  • Rust: Closures are monomorphic — fixed by first use
  • OCaml: let id x = x is polymorphic
  • Rust: Need generics for polymorphism: fn id<T>(x: T) -> T
  • Both infer types from context in many cases
  • Exercises

  • Double then square: Write let f = |x| x * 2 and compose it with let g = |x| x * x using apply, verifying that all types are inferred with no annotations.
  • Generic apply2: Implement apply2<F, A, B, C>(f: F, a: A, b: B) -> C where F: Fn(A, B) -> C and use it with both a named function and a closure.
  • Annotation exploration: Try giving let f = |x| x + 1 an explicit return type -> i64 and verify that calling it with an i32 literal causes a type error, explaining why.
  • Open Source Repos