Rust Functions Tutorial

In this Rust tutorial we learn how to group one or more code statements together into functions to facilitate code reuse throughout our projects.

We cover how to define and call functions, how to pass values to it by value or reference and how to return an output value from a function.

We also take a look at how to pass and return strings and how to call functions inside other function definitions.

What is a function

A function is one or more code statements grouped together. Functions allow us to reuse these sections of code throughout our application without rewriting it each time.

As an example, let’s consider some code logic that determines if a number is even or odd.

Example: simple logic
fn main() {

    let num = 5;

    if num % 2 == 0 {
        println!("{} is an even number", num);
    } else {
        println!("{} is an odd number", num);
    }

}

The example above will check if a number is even or odd. It does so by dividing the number by 2 and checking if there is any left over.

If there is anything left over, the number is odd. Otherwise, the number is even.

This functionality can be placed inside a function, enabling us to reuse it as much as we want.

A function consists of two parts.

  • A function definition, where we tell Rust what the function will look like and how it should be have. In other words, the function logic.
  • A function call, where we use the function that we defined.

How to define a function

A function definition consists of 5 parts.

  1. The fn keyword.
  2. An identifier, a unique name.
  3. A optional parameter list inside parentheses.
  4. A function body with one or more statements.
  5. An optional returned value.

So let’s see what the syntax for a function looks like.

Syntax:
fn function_name(param_1, param_2, ...) {

    // one or more statements

    return return_value;
}

Let’s start by writing the most basic of functions, with no parameters or return statement yet.

Example:
fn main() { }

fn message() {

    println!("Hello World");
}

In the example above, we’ve created a function called. In its code block it prints out a simple “Hello World” message to the console.

So let’s break it down.

  1. The fn keyword tells Rust that this is going to be a function.
  2. Then we give it a name to refer to it later when we use it.
  3. Inside the code block, we write our logic.

That’s all there is to defining a function.

Note that even though we didn’t specify any parameters between the parentheses, they still have to be present.

How to use (call) a function

Now that we have a basic definition of a function in Rust, let’s use it, otherwise known as calling or invoking a function. When we call a function, we tell the compiler to execute it.

To call a function we write its name, followed by the parentheses, and terminate the call with a semicolon.

Syntax:
  fn function_name(param_1, param_2, ...);

Let’s call the simple function that we made earlier.

Example:
fn main() {

    message();

}

fn message() {

    println!("Hello World");
}

When we run the example above, it will print the “Hello World” message we defined in the function.

How to define a function with parameters

Now that we known how to write a basic function, and then call it, we can add some input parameters.

Parameters allow us to pass values to the function, and have those values influence the logic.

Parameters are written inside the parentheses of the function.

Syntax:
fn function_name(param: param_data_type) {

    // logic

}

A parameter must be declared with its data type. We simply write a colon, followed by the data type.

Example:
fn message(txt: &str) {

	println!("{}", txt);

}

In the example above, we have a parameter called txt of type string (&str).

We can then use the parameter anywhere inside the function. In the example, we simply use it as a value to print to the console.

Now when we call the function, we will have to pass a value, otherwise known as an argument, to the parameter list.

Note that this argument must match the type of the parameter.

Example:
fn main() {

    message("Hello World");

}

fn message(txt: &str) {

    println!("{}", txt);
}

In the example above, we pass a string “Hello World” as an argument to the txt parameter.

The compiler will replace every instance of the txt parameter inside our function, with the string “Hello World”. In the example, it will simply print it out to the console.

The main benefit to a function is the fact that we can can use it multiple times, with different results, without having to rewrite the code. So let’s call the function a few more times.

Example:
fn main() {

    message("Hello there\n");

    message("General Kenobi...");
    message("You are a bold one");

}

fn message(txt: &str) {

    println!("{}", txt);

}

In the example above, we use the same function multiple times to print a different message to the console with each call.

How to pass a value by reference

When we pass a parameter value to a function, a new storage space is created in memory and the value is then copied over to that location. This is called passing by value.

This means that any changes to the parameter in the call, have no effect on outside data containers like variables.

Consider the following example.

Example:
fn main() {

    let num = 5;

    mutator(num);
}

fn mutator(mut x: i32) {

    x = 0;
    println!("Fn value: {}", x);
}

In the example above, we change the parameter x to 0. Any value passed to the function will automatically changed to 0 before being printed to the console.

When we run the example, it prints the word 0 as we expect. But what happened to the num variable, did it change too?

No, the variable is passed by value so it’s copied to a new location in memory instead of overwritten in the same location. We can test this by printing the variable after the function call.

Example:
fn main() {

    let num = 5;

    mutator(num);

    println!("Variable: {}", num);
}

fn mutator(mut x: i32) {

    x = 0;
    println!("Fn value: {}", x);
}

This time, we see the function call prints 0 as we expect, but when the variable is printed, it still shows the original value of 5.

We can directly overwrite a value that’s passed to the function, otherwise known as passing by reference.

This will access the same memory location and overwrite the value there, nothing is copied to a new location.

Example:
fn mutator(x: &mut i32) {

    * x = 0;
    println!("Fn value: {}", x);

}

This one gets a little hairy, but let’s walk through it step by step.

  1. First, to pass by reference, we use the & operator in front of the type in the function definition. The & operator gets a reference to the memory location of whatever is passed to the parameter.
  2. Next, we prefix our parameter with a * operator. The * operator as prefix to a variable dereferences it, and allows us to overwrite the value in the memory location that we got with the & operator.

When we pass a value to the x parameter, the compiler will find it’s memory location. Then, it will overwrite it directly in that memory location.

Simply put, the outside variable that we pass as a parameter will be affected. Let’s see it in action.

Example:
fn main() {

    let mut num = 5;

    mutator(&mut num);

    println!("Variable: {}", num);
}

fn mutator(x: &mut i32) {

    * x = 0;
    println!("Fn value: {}", x);
}

In the example above, when we call the function, we add our & before the variable name that we pass.

The function value is 0, as we except. But now, the outside variable num has also been affected and shows 0 as well.

To recap, when we pass by value, the outside variable is not affected because it’s copied to a new memory location.

When we pass by reference, it overwrites the value we pass because we access its location in memory directly.

Don’t worry if this is a little confusing right now, it’s a more advanced concept. Feel free to come back to this tutorial at the end of the course and it will be a lot easier.

How to return a value from a function

A function can return a value as a result of its logic when called. As an example, let’s consider a function that adds two numbers together.

We would need to access and temporarily store the result of the addition in order to reuse it for whatever purpose we need.

To return a value from a function, we typically need 3 things.

  1. The type of value that the function is allowed to return.
  2. An actual value that we want to return.
  3. Optionally, the return keyword.

For the function type, we can simply specify the data type of the value that will be returned. For example, if our function adds two integer numbers together and returns an integer result, we can choose i32 as the function type.

To specify a data type, we write a -> operator followed by the type, after the parameter list parentheses.

Example:
fn sum() -> i32 {
    // logic
}

When we add a data type to a function, it only affects the return value. It doesn’t mean that the function can only contain i32 types, it means that the function can only return an i32 type of value.

In Rust there are two methods we can use to return a value from a function.

The first is to use the return keyword before the value we want to return.

Example:
fn main() {}

fn sum(a: i32, b: i32) -> i32 {

    return a + b;
}

In the example above, we take two int values as parameters, add them together and return the result. Note that we use the return keyword and terminate the statement with a semicolon.

The second method is to specify only the return value, with no semicolon terminator.

Example:
fn main() {}

fn sum(a: i32, b: i32) -> i32 {

	a + b
}

Even though we can use this method of returning without borrowing, it’s mainly used for it. It’s good practice to only use this method when borrowing because it helps make it clear that we are not returning ownership.

So, now that we’re able to return a value from a function, what can we do with it?

We can either use it directly, or store it in a data container like a variable for later use.

Example:
fn main() {

    let a:i32 = 5;
    let b:i32 = 3;

    // store result
    let result:i32 = sum(a, b);
    // use result
    println!("Stored: {} + {} = {}", a, b, result);

    // use directly
    println!("Direct: {} + {} = {}", a, b, sum(a, b));
}

fn sum(a: i32, b: i32) -> i32 {

    return a + b;
}

Functions and strings

Two questions often come up when working with strings and functions.

  • How to pass a string value to a function parameter.
  • How to return a string from a function.

The answer is simple, we simply specify the string type &str as the parameter type or function return type.

Example:
fn main() {

    let str_msg: &str = "&str Message";

    println!("{}", log_str(str_msg));

}

fn log_str(msg: &str) -> &str {

    return msg;
}

How to call a function inside a function definition

We are allowed to call functions inside the definition of a function.

Example:
fn main() {

    log_result(   5,   3);
    log_result( 189,  14);
    log_result(-847,  56);
    log_result( 554, -61);

}

fn log_result(a: i32, b: i32) {

    println!("\nBasic arithmetic results for {} and {}: ", a, b);
    println!("{} + {} = {}", a, b, add(a, b));
    println!("{} - {} = {}", a, b, sub(a, b));
    println!("{} / {} = {}", a, b, div(a, b));
    println!("{} * {} = {}", a, b, mtp(a, b));
}

fn add(a: i32, b: i32) -> i32 {

    return a + b;
}
fn sub(a: i32, b: i32) -> i32 {

    return a - b;
}
fn div(a: i32, b: i32) -> i32 {

    return a / b;
}
fn mtp(a: i32, b: i32) -> i32 {

    return a * b;
}

In the example above, we call each of our arithmetic functions in a logger function, and then use the logger function multiple times in our main code.

This serves to demonstrate that we can call functions inside other function definitions, as well as keeping our code clean and organized.

Custom is_even() function

Now that we’ve learned about functions, you’ll be able to convert the logic from the start of the tutorial into a function.

Try to convert the following logic into a function called is_even. If you get stuck, see our solution below.

Example: Logic
fn main() {

    let num = 5;

    if num % 2 == 0 {
        println!("{} is an even number", num);
    } else {
        println!("{} is an odd number", num);
    }

}
Example: Our Solution
fn main() {

    is_even(5);
    is_even(10);
    is_even(-879);
    is_even(0);
    is_even(1267);
    is_even(88);

}

fn is_even(num:i32) {

    if num % 2 == 0 {
        println!("{} is an even number", num);
    } else {
        println!("{} is an odd number", num);
    }
}

Summary: Points to remember

  • Functions allow us to group sections of our code into reusable containers.
  • A function definition tells Rust what the function looks like and what it does.
  • A function call is when we actually use the function.
  • Functions can accept parameters that enables a user to pass values to it.
    • When we pass by value, a copy is made to a new memory location, and the original value is not affected.
    • When we pass by reference, the same memory location is overwritten, and the original value is affected.
  • A function can return a value as output, either with or without the return keyword.
  • Functions can be called inside the definition of other functions.
  • If we want a function to accept or return a string value, we specify &str as the data type.