Why does explicitly adding return statement remove type mismatch?

The following does not compile, as there is a type mismatch - the if-block is required to return unit type (), and the value that the if-expression evaluates to is bool.

fn bar() -> i32 {
    1
}

fn main() {
    fn foo() -> bool {
        if (bar() == 1) {
            true
        }
        false
    }
}

However, if I add the return statement to the if-block like this

return true,

it does compile. I am confused now. Did the return keyword trigger some kind of a side-effect that prematurely returned from the function? How did it circumvent the requirement on the if-block to evaluate to unit type?

A lonely if without an else cannot be an expression, because it only has a value if the condition is true

1 Like

Hmmm, my compiler says the following.
expected (), found bool
note: if expressions without else evaluate to ()
So it is not an expression, but the compiler still wants me to not return bool from it? Confused with this one.

Yes, statements do not have a value, and not having a value means the same thing as having a value of (). I'm this case, rust is catching a real bug in your code.

A nicer solution than adding a return would be to add an else. But for this particular function a nicer approach yet would be to remove the if and have

fn foo() -> bool {
      bar() == 1
}

But I'm guessing your actual function is more complicated.

1 Like

That's correct. You cannot "return" anything from a lonely if because that value would be lost.
The value is not returned from the function, but only from the block.

The following is valid code:

let x = if (condition) { 1 } else { 0 };

While this is not:

let x = if (condition) { 1 };

What would be the value of x if condition is false?

1 Like

Got your point. return is a statement and has the value of ().

So if, [else if] must be followed by else to make a valid expression? Aha, so your second example is equivalent to my non-working code and they both give E0317 error, so the compiler basically told me about the fact that I cannot return from a lonely if-expression without using the return statement.

In other words, this is what @droundy said about return statement having type ()?

Well, I cannot always construct valid if-else blocks, as sometimes I need to optionally return on some condition. In such cases, I must use return, right (there is no other option)? Previously I tried to avoid writing return because AFAIK it is considered bad style.

return is not bad style, unnecessary returns are, such as those at the end of an expression or function block which would already be returned.

2 Likes

Clear.

I used to do stuff like this because I thought return does the exact same thing

if let Some(some_thing) = thing {
    if let Ok(some_other_thing) = some_thing{
        Some(some_other_thing)
    }
    else{
        None
    }
} 
else{
    None
}

but then I realized I can just do this :sweat_smile:

if let Some(some_thing) = thing {
    if let Ok(some_other_thing) = some_thing{
        return Some(some_other_thing);
    }
} 
return None;
1 Like

For this specific case, you can just do this.

if let Some(Ok(thing)) = thing {
    return Some(thing)
}
None

Hm I know. I was mainly using it as a sort of example of a whole lot of nested 'if's. Should've used a better example I suppose

In this case, yes.

In fact, return and others (break, std::process::exit(), panic!(), unreachable!(), and so on) have ! type (never type) and can be treated as any types as necessary.
Such expressions are called "diverging".

#[allow(unreachable_code)]
#[allow(unused_variables)]
#[allow(dead_code)]
fn foo() -> i32 {
    // `return 0` is `!` type, so it can be treated as `i32`.
    let ret: i32 = return 0;

    // `return 0` is `!` type, so it can be also treated as `String`.
    let ret2: String = return 42;
    
    // `todo!()` is `!` type (because it returns any value at runtime),
    // so it can be treated as `&str`.
    let unr: &str = todo!();
    
    // `todo!()` is `!` type, and can be also treated as `i32`.
    let unr2: i32 = todo!();
    
    // `std::process::exit()`, `panic!()`, `break`, and more friends!
}

You can consider if cond { then } as if cond { then } else { () }, and this makes it clear that then part and if itself is also ().

fn foo() -> i32 {
    // `if` without `else` itself is `()`.
    let _: () = if true {
        // `if` without `else` requires then-clause to be `()` type.
        ()
    };
    
    let _: () = if true {
        // `return` can be any type, and `if` without `else`
        // requires expression here to be `()`,
        // so `return 42` in this case is treated as `()`.
        return 42
    };

    let _: () = if true {
        // Block `{ return 42; }` is "diverging" (i.e. does not return
        // any value at runtime), because `return 42;` is
        // diverging. This allows `{ return 42; }` to be treated as
        // any type (including `()`).
        return 42;
    };
    
    0
}
11 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.