ExamplesBy LevelBy TopicLearning Paths
527 Intermediate

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