Work with functions in Rust

Completed

Functions are the primary way code is executed within Rust. You've already seen one of the most important functions in the language, the main function. In this unit, we'll cover more of the details about how to define functions.

Define a function

Function definitions in Rust start with the fn keyword. After the function name, we specify the function's input arguments as a comma-separated list of data types inside parentheses. The curly brackets tell the compiler where the function body begins and ends.

fn main() {
    println!("Hello, world!");
    goodbye();
}

fn goodbye() {
    println!("Goodbye.");
}

We call a function by using its name along with its input arguments in parentheses. If a function doesn't have any input arguments, we leave the parentheses empty. In our example, both the main and goodbye functions have no input arguments.

You might have noticed that we defined the goodbye function after the main function. We could have defined the goodbye function before we defined main. Rust doesn't care where in the file you define your functions, as long as they're defined somewhere in the file.

Pass input arguments

When a function has input arguments, we name each argument and specify the data type at the start of the function declaration. Because arguments are named like variables, we can access the arguments in the function body.

Let's modify our goodbye function to take a pointer to some string data as an input argument.

fn goodbye(message: &str) {
    println!("\n{}", message);
}

fn main() {
    let formal = "Formal: Goodbye.";
    let casual = "Casual: See you later!";
    goodbye(formal);
    goodbye(casual);
}

We'll test our function by calling it from the main function with two different argument values, and then check the output:

Formal: Goodbye.
Casual: See you later!

Return a value

When a function returns a value, we add the syntax -> <type> after the list of function arguments and before the opening curly bracket for the function body. The arrow syntax -> indicates that the function returns a value to the caller. The <type> portion lets the compiler know the data type of the value returned.

In Rust, the common practice is to return a value at the end of a function by having the last line of code in the function be equal to the value to return. The following example shows this behavior. The divide_by_5 function returns the result of dividing the input number by 5 to the calling function:

fn divide_by_5(num: u32) -> u32 {
    num / 5
}

fn main() {
    let num = 25;
    println!("{} divided by 5 = {}", num, divide_by_5(num));
}

Here's the output:

25 divided by 5 = 5

We can use the return keyword at any point in the function to halt execution and send a value back to the caller. Usually, the use of the return keyword is used in combination with a conditional test.

Here's an example that explicitly uses the return keyword to return early from a function if the value of num is 0:

fn divide_by_5(num: u32) -> u32 {
    if num == 0 {
        // Return early
        return 0;
    }
    num / 5
}

When you explicitly use the return keyword, you end the statement with a semicolon. If you send back a return value without using the return keyword, you don't end the statement with a semicolon. You might have noticed that we didn't use the ending semicolon for the num / 5 return value statement.

Review the signature

The first part of the declaration for a function is called the function signature.

The signature for the goodbye function in our example has these characteristics:

  • fn: The function declaration keyword in Rust.
  • goodbye: The function name.
  • (message: &str): The function's argument or parameter list. One pointer to string data is expected as the input value.
  • -> bool: The arrow points to the type of value this function will always return.

The goodbye function accepts one string pointer as input and outputs a boolean value.

You can interact with the example code in this Rust Playground.