Enforced return type of ()?

So, this is utterly baffling to me and I can't figure out why Rust has decided to do this. I have this function declaration, which seems perfectly valid to me:

pub fn parse_command(command_string: String) -> Option<ParsedCommand> {

Yet, for some reason, I get this when trying to build it:

error[E0308]: mismatched types
  --> src\command_parser.rs:63:1
   |
63 | None
   | ^^^^ expected (), found enum `std::option::Option`
   |
   = note: expected type `()`
              found type `std::option::Option<_>`

What is making the compiler do this, and is there (anyway) way I can permanently prevent this from happening? The entire function is below:

pub fn parse_command(command_string: String) -> Option<ParsedCommand> {
    let mut cmd_iter = command_string.split_whitespace();
    let (_, mut upper) = cmd_iter.size_hint();
    // Verify that we indeed do have a fully interpretable command string
    if upper.is_none() {
        None
    }
    let upper = upper.unwrap();
    if upper == 0 {
        None
    }
    let cmd = cmd_iter.next().unwrap();
    let mut args: Vec<Vec<u8>> = Vec::new();
    let mut count: usize = 0;
    let mut hash: Vec<u8> = Vec::new();
    loop {
        let mut arg = cmd_iter.next();
        if arg.is_none() {
            break;
        }
        let arg = arg.unwrap();
        count += 1;
        // What to do with this argument?
        if count == upper {
            // This is a hash. We don't include it in the arg list.
            match BASE64.decode(arg.as_bytes()) {
                Ok(h) => {
                    hash = h;
                    break;
                }
                Err(_) => return None,
            }
        } else {
            // This is a command argument
            match BASE64.decode(arg.as_bytes()) {
                Ok(a) => args.push(a),
                Err(_) => return None,
            }
        }
    }
    // Before we do anything else, confirm that this hash is valid.
    let mut buf: Vec<u8> = Vec::new();
    for arg in args.iter() {
        buf.reserve_exact(arg.len());
        buf.extend(arg);
        buf.push(0x20);
    }
    if buf.ends_with(&[0x20]) {
        buf.pop();
        buf.shrink_to_fit();
    }
    let mut generated_hash = [0u8; 64];
    hash_xof(MessageDigest::shake_256(), buf.as_slice(), &mut hash).unwrap();
    if hash != generated_hash.to_vec() {
        // Invalid hash, abort command parsing
        None
    }
    // Return this data, we're done
    Some(ParsedCommand {
        command: cmd.to_owned(),
        args: args,
    })
}

Only the last expression in a block gets treated as its expression value, including the return value of a function. So you're writing None in a lot of places that don't represent return values. In particular an if without else can only have the () type, since we don't have any value to use when the if condition is false. You should write return None to leave the function at this point.

Sometimes you will see bare values like you've tried, but it always needs to come at the end of the function block. Something like:

fn foobar() -> Option<i32> {
    setup();
    ...
    if foo() {
        None
    } else {
        match bar() {
            Err(_) => None,
            Ok(x) => Some(x + 1),
        }
    }
}
2 Likes

If you're using implicit returns with a block with multiple branches, each branch must return the same type.

In the case of your if:

if hash != generated_hash.to_vec() {
    // Invalid hash, abort command parsing
    None
}
// Return this data, we're done
Some(ParsedCommand {
    command: cmd.to_owned(),
    args: args,
})

Your if statement's else block returns () (Because it doesn't exist), or in other words an if (Not if-else) block must always return ().

You can fix this by using an else statement or a return statement, although implicit return is usually more idiomatic.

Thanks. I don't think the Rust book clerifies this properly -- if it doesn't it'd be nice if it explicitly indicated when this happens (or, even better, if this just didn't happen at all and rustc just didn't search for an "else" block if one doesn't exist, thereby eliminating this issue, maybe?). Thanks for the answers.

@OptimisticPeach Here if() is a statement, it is not “the same type” problem which happens here. Try this

fn main() {
    if true {
        true
    }
    if true {
        true
    } else {
        false
    }
    ()
}

You here have main() explicitly return () (so that second if will not count as main()’s return), one if with mismatching types and one if with matching types which are not (). How many errors compiler produces? It is three, when if is a statement all branches must not produce matching types, they all must produce (). Compiler errors for some reason are misleading though, if you make all if()s have type compatible with main() return type it will still complain about mismatched types. Code

fn main() -> Result<(), ()> {
    if true {
        Result::<(), ()>::Ok(())
    }
    if true {
        Result::<(), ()>::Ok(())
    } else {
        Result::<(), ()>::Err(())
    }
    Result::<(), ()>::Ok(())
}

Just in first variant it complains about “expected () because of default return type”, pointing to the return type of main(), in the second variant it complains about mismatched types and expecting () without pointing to the return type of main().

@Ethindp It looks that you understand what happens incorrectly. Code

if cond {
  None
}
Some(value)

is the equivalent of

{
  let _throw_away: () = if cond {
    None
  }
}
Some(value)

. I.e. None is never returned, return happens only either from the very last statement or from some kind of return statement (candidates that I know about are return statement itself, ? operator or some macros which uses either of that). What happened if compiler allowed your code is that it would silently let you ignore conditions under which you return None and proceed until you get error in runtime.

I think that compiler should actually do what it does, but provide error message which clearly states that if used as a statement must return ().

I think this thread does demonstrate a slight wierdness about rust's "everything is an expression, except sometimes not" approach. I think it's because Rust straddles between the FP and imperative worlds. Rust is like imperative++, but in these cases it is very much imperative.

I think I would have enforced semi-colons after if expressions/statements that aren't the last expression of the block. so you would have

let x;
if true {
  x = 2;
} else {
  x = 3;
};
x

for example.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.