What's the difference between using the return statement and omitting the semicolon in Rust?
Asked Answered
N

3

9

I'm writing a function that returns a serde_json::Value upon success (and failure). Previously in Rust I have been omitting the semicolon to return data from a function, like in the code example below:

use serde_json::{Result, Value};
use core::result::Result as ResultCore;

fn returning_function() -> ResultCore<Value, Value> {
    let data = r#"
    {
        "status": "ok",
        "response": {
            "data": "secret message!"
         }
    }
    "#;

    match str_to_json(data) {
        Ok(json_data) => match json_data["status"].as_str() {
            Some(status_str) => {
                if status_str == "ok" {
                    Ok(json_data["response"].clone())
                }
            }
            None => eprintln!("\"status\" was not a string")
        }
        Err(error) => eprintln!("something went wrong! here's what: {}", error)
    }

    Err(serde_json::Value::Null)
}

fn str_to_json(json_data: &str) -> Result<Value> {
    Ok(serde_json::from_str(json_data)?)
}

Here comes the part I don't understand: this doesn't compile. Rust's compiler tells me "mismatched types", and that it expected type (), but found type serde_json::value::Value. Now, I found a solution to this that does compile, and it is as follows:

use serde_json::{Result, Value};
use core::result::Result as ResultCore;

fn returning_function() -> ResultCore<Value, Value> {
    let data = r#"
    {
        "status": "ok",
        "response": {
            "data": "secret message!"
         }
    }
    "#;

    match str_to_json(data) {
        Ok(json_data) => match json_data["status"].as_str() {
            Some(status_str) => {
                if status_str == "ok" {
                    return Ok(json_data["response"].clone());
                    // ^ added return statement here
                }
            }
            None => eprintln!("\"status\" was not a string")
        }
        Err(error) => eprintln!("something went wrong! here's what: {}", error)
    }

    Err(serde_json::Value::Null)
}

fn str_to_json(json_data: &str) -> Result<Value> {
    Ok(serde_json::from_str(json_data)?)
}

By adding the return statement the compiler suddenly is happy and the compiler doesn't have anything to say about it any more. Why is this? I was under the impression that omitting the semicolon and using the return statement had the same implications ­— why does it differ here?

Neume answered 23/11, 2019 at 23:52 Comment(1)
return returns from the whole function, whereas the last expression of a block (if a semicolon is omitted) implicitly returns from that block only. See the answers to What type is the “type ()” in Rust? for some more discussion.Subirrigate
K
12

A return statement, otherwise known as an early return, will return an object from the last/innermost function-like scope. (Function-like because it applies to both closures and functions)

let x = || {
    return 0;
    println!("This will never happen!");
};
fn foo() {
    return 0;
    println!("This won't happen either");
}

An absent semicolon will instead evaluate the expression, like a return, but only return to the last/innermost scope, or in other words, it returns from within any set of {}.

let x = {           // Scope start
    0
};                  // Scope end

fn foo() -> usize { // Scope start
    0
}                   // Scope end

return statement will break out of any amount of nested scopes until it hits a function-like scope:

fn foo() -> usize {// <------------------------------------------\
    {                                                      //    |
        {                                                  //    |
            {                                              //    |
                {                                          //    |
                    {                                      //    |
                        {                                  //    |
                            {                              //    |
                                {                          //    |
                                    {                      //    |
                                        {                  //    |
                                            {              //    |
                                                 return 0; // ---/
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

The return statement also has a type of its own, that is to say that let x = return; will actually compile.

A return statement will evaluate to !, AKA the never type. You can't name it in stable rust right now, but it will eventually become stable and usable.

Kibbutz answered 24/11, 2019 at 4:6 Comment(1)
This was a gotcha for me. A really great explanation.Waxler
E
5

As it says in The Book:

In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function.

In other words - it is not the fact that an expression does not have a semicolon that makes it the return value, it is the fact that it is the final expression in the function. A semicolon is used to separate expressions, so this:

fn foo() -> i32 {
    5;
}

is equivalent to an expression yielding the value 5, followed by an empty expression that does not yield anything. Thus the function above would not compile.

Where the return keyword comes in handy is if you want to return from a function early, before reaching the final expression. This is what you are trying to do in your example.

Also note that all potential return values have to have the same type as the return value of the function itself.

None of the above fully explains the compiler error you were getting though. Your inner match looks like this:

match json_data["status"].as_str() {
    Some(status_str) => {
        if status_str == "ok" {
            Ok(json_data["response"].clone())
        }
    }
    None => eprintln!("\"status\" was not a string")
}

One of the rules of match blocks is that each of the arms has to evaluate to the same type. But in the case above, one arm potentially evaluates to std::result::Result<serde_json::value::Value, _>, while the other does not evaluate to anything (or to be more precise, evaluates to the empty value ()).

Inserting the return avoids that error, because the Some arm now returns from the function altogether, rather than evaluating to a value of type std::result::Result<serde_json::value::Value, _>.

Epoxy answered 25/11, 2019 at 0:12 Comment(0)
C
0

The above error is actually caused by the implicit return () of the if expression without else.

Censor answered 18/4, 2023 at 3:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.