What is the point of Option<()> ret type?

I came across some code that went like this:

#[must_use]
fn do_thing(&mut self) -> Option<()> {
  // ...
}

I get what it does: do_thing does something, the something doesn't result in any value and merely mutates the state, and the something might or might not do the thing it was supposed to do and it must inform the caller about it. Failure isn't really an error, thus not a Resut. Caller is obligated to check whether the thing was done, thus #[must_use].

What I don't get is, how is that different from bool? #[must_use] can be applied to any function with a return value.

struct A(u32);

impl A {
    #[must_use]
    fn do_thing(&mut self) -> bool {
        unimplemented!()
    }
}

fn main() {
    let mut a = A(0);
    a.do_thing();
}
warning: unused return value of `A::do_thing` that must be used
  --> src/main.rs:12:5
   |
12 |     a.do_thing();
   |     ^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default

warning: 1 warning emitted

So, is there any functional (not ideological) difference between the two? The only one I could think of is that an Option can be "bubbled up" via ?, but this code didn't make use of it.

There's not really any difference. The only reason I can think of for choosing Option<()> over bool is that Option has a ton of methods for letting you choose how to handle the cases, like a.unwrap(). Of course, you can do this with an if statement too, but a.unwrap() is pretty concise :^)

I can imagine that the author wrote too many code like this:

if !a.do_thing() {
    return None;
}

And decided to write a.do_thing()?; instead.

7 Likes

I use this in my code as such:

/// Returns None if the implicated_cid is NOT the key's cid.
///
/// Passing the permission gate requires that the implicated_cid is the key's owning cid
fn permission_gate(implicated_cid: u64, key: MessageGroupKey) -> Option<()> {
    if implicated_cid != key.cid {
        None
    } else {
        Some(())
    }
}

And then, checking permissions is as clean as:

// [...]
GroupBroadcast::End(key) => {
            permission_gate(implicated_cid, key)?;
            let success = session.session_manager.remove_message_group(implicated_cid, timestamp, ticket, key);
            let signal = GroupBroadcast::EndResponse(key, success);
            let return_packet = hdp_packet_crafter::peer_cmd::craft_group_message_packet(pqc_sess, &drill_sess, &signal, ticket, ENDPOINT_ENCRYPTION_OFF, timestamp);
            PrimaryProcessorResult::ReplyToSender(return_packet)
}

A handy feature of Option is that it implements Try, which makes it easy to use with fallible iterator methods, like:

let a: Option<A> = options.iter().try_for_each(|o| a.try_to_add_option(o)).map(|()| a);