FnOnce for Consuming Closures
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
FnOnce differs from Fn and FnMut in terms of call constraintsCopy values out of captures are automatically FnOncewith_resource<R, T, F: FnOnce(R) -> T>(resource, f) implements resource bracketingOnceAction<F: FnOnce()> wraps a one-shot action that can be safely called or droppedFnOnce appears in Rust's standard library: thread::spawn, std::fs::File::createCode 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
FnOnce makes double-call a compile error; OCaml has no equivalent — double-call protection must be implemented at runtime with a ref flag.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.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);
}
}#[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
Exercises
with_transaction<F: FnOnce(Transaction) -> Result<(), String>>(db: &mut Database, f: F) -> Result<(), String> that commits on Ok and rolls back on Err.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.Defer<F: FnOnce()> that stores a closure and calls it in Drop::drop, implementing a scope-guard pattern without unsafe code.