ExamplesBy LevelBy TopicLearning Paths
527 Intermediate GitHub

FnOnce for Consuming Closures

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "FnOnce for Consuming Closures" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Some operations are inherently one-time: consuming a network connection, sending a channel message, releasing a one-time authentication token, or running a database transaction. Key difference from OCaml: 1. **Compile

Tutorial

The Problem

Some operations are inherently one-time: consuming a network connection, sending a channel message, releasing a one-time authentication token, or running a database transaction. Languages without linear types struggle to enforce "call at most once" at compile time, leading to runtime errors or logic bugs. Rust's FnOnce trait is the compile-time guarantee that a callable is invoked at most once — the type system physically prevents a second call by consuming the closure on the first. This maps directly to linear/affine types in type theory.

🎯 Learning Outcomes

  • • How FnOnce differs from Fn and FnMut in terms of call constraints
  • • Why closures that move non-Copy values out of captures are automatically FnOnce
  • • How with_resource<R, T, F: FnOnce(R) -> T>(resource, f) implements resource bracketing
  • • How OnceAction<F: FnOnce()> wraps a one-shot action that can be safely called or dropped
  • • Where FnOnce appears in Rust's standard library: thread::spawn, std::fs::File::create
  • Code Example

    // FnOnce: closure that consumes captured values
    pub fn make_consumer(token: Token) -> impl FnOnce() -> String {
        move || token.consume()  // token moved, callable only once
    }
    
    // with_resource consumes the resource
    pub fn with_resource<R, T, F: FnOnce(R) -> T>(resource: R, f: F) -> T {
        f(resource)
    }

    Key Differences

  • Compile-time enforcement: Rust's FnOnce makes double-call a compile error; OCaml has no equivalent — double-call protection must be implemented at runtime with a ref flag.
  • Linear resources: Rust's type system enforces linear use of FnOnce closures, aligning with affine/linear type theory; OCaml's GC-managed closures are always multiply-usable.
  • **Option<F> workaround**: When FnOnce must be stored in a struct and called via &mut self, Rust uses Option::take() to satisfy the borrow checker; OCaml needs no such workaround.
  • Standard library integration: thread::spawn takes F: FnOnce() + Send + 'static — a fundamental guarantee that the closure runs exactly once on the new thread; OCaml's Thread.create has no such type-level contract.
  • OCaml Approach

    OCaml has no FnOnce equivalent — all functions can be called multiple times. One-time semantics are enforced by convention or by using a ref bool flag that raises an exception on second call:

    let make_once f =
      let called = ref false in
      fun () -> if !called then failwith "called twice"
                else (called := true; f ())
    

    This is a runtime check, not a compile-time guarantee.

    Full Source

    #![allow(clippy::all)]
    //! FnOnce for Consuming Closures
    //!
    //! Closures that consume their captured values — callable only once.
    
    /// A resource that can only be "used" once.
    pub struct OneTimeToken {
        value: String,
    }
    
    impl OneTimeToken {
        pub fn new(s: &str) -> Self {
            OneTimeToken {
                value: s.to_string(),
            }
        }
    
        pub fn consume(self) -> String {
            self.value
        }
    }
    
    /// FnOnce: captures and consumes a OneTimeToken.
    pub fn make_token_consumer(token: OneTimeToken) -> impl FnOnce() -> String {
        move || token.consume()
    }
    
    /// Resource cleanup via FnOnce.
    pub fn with_resource<R, T, F: FnOnce(R) -> T>(resource: R, f: F) -> T {
        f(resource)
    }
    
    /// Deferred action: run once, then nothing.
    pub struct OnceAction<F: FnOnce()> {
        action: Option<F>,
    }
    
    impl<F: FnOnce()> OnceAction<F> {
        pub fn new(action: F) -> Self {
            OnceAction {
                action: Some(action),
            }
        }
    
        pub fn run(mut self) {
            if let Some(f) = self.action.take() {
                f();
            }
        }
    }
    
    /// Builder that produces a value once.
    pub struct OnceBuilder<T> {
        builder: Option<Box<dyn FnOnce() -> T>>,
    }
    
    impl<T> OnceBuilder<T> {
        pub fn new(f: impl FnOnce() -> T + 'static) -> Self {
            OnceBuilder {
                builder: Some(Box::new(f)),
            }
        }
    
        pub fn build(mut self) -> Option<T> {
            self.builder.take().map(|f| f())
        }
    }
    
    /// Move-only resource for RAII.
    pub struct FileHandle {
        name: String,
        is_open: bool,
    }
    
    impl FileHandle {
        pub fn open(name: &str) -> Self {
            FileHandle {
                name: name.to_string(),
                is_open: true,
            }
        }
    
        pub fn close(mut self) -> String {
            self.is_open = false;
            format!("Closed: {}", self.name)
        }
    
        pub fn is_open(&self) -> bool {
            self.is_open
        }
    
        pub fn name(&self) -> &str {
            &self.name
        }
    }
    
    /// Use FnOnce with Result for error handling.
    pub fn try_once<T, E, F: FnOnce() -> Result<T, E>>(f: F) -> Result<T, E> {
        f()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::cell::RefCell;
    
        #[test]
        fn test_token_consumer() {
            let token = OneTimeToken::new("secret123");
            let consumer = make_token_consumer(token);
            // Can only call once
            let value = consumer();
            assert_eq!(value, "secret123");
            // consumer(); // ERROR: already consumed
        }
    
        #[test]
        fn test_with_resource() {
            let handle = FileHandle::open("test.txt");
            let result = with_resource(handle, |h| h.close());
            assert_eq!(result, "Closed: test.txt");
        }
    
        #[test]
        fn test_once_action() {
            let counter = RefCell::new(0);
            let action = OnceAction::new(|| {
                *counter.borrow_mut() += 1;
            });
            action.run();
            assert_eq!(*counter.borrow(), 1);
            // action.run(); // ERROR: moved
        }
    
        #[test]
        fn test_once_builder() {
            let builder = OnceBuilder::new(|| vec![1, 2, 3]);
            let result = builder.build();
            assert_eq!(result, Some(vec![1, 2, 3]));
            // builder.build(); // ERROR: moved
        }
    
        #[test]
        fn test_file_handle() {
            let handle = FileHandle::open("data.txt");
            assert!(handle.is_open());
            let msg = handle.close();
            assert!(msg.contains("Closed"));
        }
    
        #[test]
        fn test_try_once_ok() {
            let result: Result<i32, &str> = try_once(|| Ok(42));
            assert_eq!(result, Ok(42));
        }
    
        #[test]
        fn test_try_once_err() {
            let result: Result<i32, &str> = try_once(|| Err("failed"));
            assert_eq!(result, Err("failed"));
        }
    
        #[test]
        fn test_fn_once_in_option() {
            let consume = |s: String| s.len();
            let opt = Some("hello".to_string());
            let result = opt.map(consume);
            assert_eq!(result, Some(5));
        }
    
        #[test]
        fn test_fn_once_trait_bound() {
            fn apply_once<T, F: FnOnce() -> T>(f: F) -> T {
                f()
            }
    
            let s = String::from("owned");
            let result = apply_once(move || s.len());
            assert_eq!(result, 5);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::cell::RefCell;
    
        #[test]
        fn test_token_consumer() {
            let token = OneTimeToken::new("secret123");
            let consumer = make_token_consumer(token);
            // Can only call once
            let value = consumer();
            assert_eq!(value, "secret123");
            // consumer(); // ERROR: already consumed
        }
    
        #[test]
        fn test_with_resource() {
            let handle = FileHandle::open("test.txt");
            let result = with_resource(handle, |h| h.close());
            assert_eq!(result, "Closed: test.txt");
        }
    
        #[test]
        fn test_once_action() {
            let counter = RefCell::new(0);
            let action = OnceAction::new(|| {
                *counter.borrow_mut() += 1;
            });
            action.run();
            assert_eq!(*counter.borrow(), 1);
            // action.run(); // ERROR: moved
        }
    
        #[test]
        fn test_once_builder() {
            let builder = OnceBuilder::new(|| vec![1, 2, 3]);
            let result = builder.build();
            assert_eq!(result, Some(vec![1, 2, 3]));
            // builder.build(); // ERROR: moved
        }
    
        #[test]
        fn test_file_handle() {
            let handle = FileHandle::open("data.txt");
            assert!(handle.is_open());
            let msg = handle.close();
            assert!(msg.contains("Closed"));
        }
    
        #[test]
        fn test_try_once_ok() {
            let result: Result<i32, &str> = try_once(|| Ok(42));
            assert_eq!(result, Ok(42));
        }
    
        #[test]
        fn test_try_once_err() {
            let result: Result<i32, &str> = try_once(|| Err("failed"));
            assert_eq!(result, Err("failed"));
        }
    
        #[test]
        fn test_fn_once_in_option() {
            let consume = |s: String| s.len();
            let opt = Some("hello".to_string());
            let result = opt.map(consume);
            assert_eq!(result, Some(5));
        }
    
        #[test]
        fn test_fn_once_trait_bound() {
            fn apply_once<T, F: FnOnce() -> T>(f: F) -> T {
                f()
            }
    
            let s = String::from("owned");
            let result = apply_once(move || s.len());
            assert_eq!(result, 5);
        }
    }

    Deep Comparison

    OCaml vs Rust: FnOnce / Consuming Closures

    OCaml

    (* No explicit distinction — GC manages resources *)
    let consume_token token = token.value
    
    (* Callbacks typically work the same way *)
    let with_resource resource f = f resource
    

    Rust

    // FnOnce: closure that consumes captured values
    pub fn make_consumer(token: Token) -> impl FnOnce() -> String {
        move || token.consume()  // token moved, callable only once
    }
    
    // with_resource consumes the resource
    pub fn with_resource<R, T, F: FnOnce(R) -> T>(resource: R, f: F) -> T {
        f(resource)
    }
    

    Key Differences

  • OCaml: GC handles resources, no ownership tracking
  • Rust: FnOnce ensures closure called at most once
  • Rust: Ownership system enforces single-use at compile time
  • Rust: FnOnce > FnMut > Fn (hierarchy of capabilities)
  • Rust: Move semantics guarantee resource cleanup
  • Exercises

  • Transaction closure: Implement with_transaction<F: FnOnce(Transaction) -> Result<(), String>>(db: &mut Database, f: F) -> Result<(), String> that commits on Ok and rolls back on Err.
  • Once-per-test helper: Write run_once_setup(setup: impl FnOnce() -> String) that stores the result in a OnceLock<String> and verifies setup can only be called once from the outside.
  • Deferred drop: Create Defer<F: FnOnce()> that stores a closure and calls it in Drop::drop, implementing a scope-guard pattern without unsafe code.
  • Open Source Repos