Skip to content

Revision 2026-10-02

Published 2 October 2026

Why Option::map takes FnOnce

fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U>

FnOnce is a closure that can be called at least once. It may consume what it captured, so Rust can’t promise a second call. map calls f at most once (zero times for None), so a one-shot closure is all it needs:

let label = String::from("car");
let result = Some(5).map(|n| {
    drop(label); // closure consumes `label`, so it can only run once
    n * 2
});

FnOnce is the least restrictive closure trait. Every closure implements it, and closures that don’t consume their captures also implement FnMut and Fn. So map accepts both.

as_ref(): map without losing the Option

map takes self by value, so name.map(...) consumes name:

let name: Option<String> = Some("Karthik".to_string());
let len = name.map(|s| s.len());
// println!("{name:?}"); // error: use of moved value

as_ref() turns &Option<String> into Option<&String>, a borrow of the same string. Nothing is copied or moved:

let name: Option<String> = Some("Karthik".to_string());
let len = name.as_ref().map(|s| s.len());
println!("{name:?}"); // still usable
println!("{len:?}");  // Some(7)

But doesn’t map still consume something?

Yes. map always consumes its receiver. What changes is which Option that is:

name                Option<String>   still owns the String
name.as_ref()       Option<&String>  new temporary holding a borrow
.map(...)           consumes that temporary, not `name`

Consuming the temporary only drops a reference, so the String stays where it was.

Why the closure body doesn’t change

The closure’s parameter type follows the Option:

let name = Some(String::from("Karthik"));
name.clone().map(|s: String| s.len());   // Option<String>  -> s: String
name.as_ref().map(|s: &String| s.len()); // Option<&String> -> s: &String

s.len() compiles either way. Why? Auto-deref.

Auto-deref

Method-call autoderef

When you write x.method(), Rust looks for method on the type of x, then follows references (&T → T, and anything implementing Deref) until it finds one, and auto-borrows x to match self. You never write (*s).len() or (&s).len().

String::len takes &self:

fn len(&self) -> usize
let owned: String = String::from("car");
let borrowed: &String = &owned;

owned.len();    // auto-borrow:  String::len(&owned)
borrowed.len(); // already a ref: String::len(borrowed)

Both calls resolve to the same method, which is why the closure body is identical for Option<String> and Option<&String>.

saturating_sub on u64

Unsigned subtraction that would go below zero panics in debug builds (and wraps in release). saturating_sub clamps at 0 instead:

let a: u64 = 3;
let b: u64 = 5;
// a - b;                  // panics in debug: attempt to subtract with overflow
assert_eq!(a.saturating_sub(b), 0);
assert_eq!(b.saturating_sub(a), 2);

Result: why wrap the error in Err?

Result is a plain enum with two variants:

enum Result<T, E> {
    Ok(T),  // success, carries a T
    Err(E), // failure, carries an E
}

A function returning Result<(), RentalError> must hand back one of those variants: Ok(()) (success, nothing to return) or Err(some_rental_error). RentalError::TripAlreadyStarted alone is just the error value, so Err(...) is what puts it in the failure case. Nothing converts it automatically:

pub fn start_trip(&mut self, order_id: &str, odometer_reading: u64) -> Result<(), RentalError> {
    let booking = self
        .bookings
        .get_mut(&OrderId(order_id.to_string()))
        .ok_or(RentalError::BookingNotFound)?; // Option -> Result, then `?`
    if booking.start_odometer.is_some() {
        return Err(RentalError::TripAlreadyStarted);
    }
    booking.start_odometer = Some(odometer_reading);
    Ok(())
}

? goes the other way: on Err(e) it returns Err(e) from start_trip, and on Ok(booking) it unwraps the value and carries on.