Functions form the backbone of any programming language. They help make a program easier to maintain by reducing complexity. Even during development, breaking a program into logical blocks or functions can lead to a better design that is easier to test. Functions in Rust Programming Language are no different.

Functions in Rust can be declared by using the fn keyword. A function declaration should also specify if it needs any parameters as input. We also need to define return types in the declaration in case the function should return some values.

In this post, we will look at various aspects of Rust functions in detail. In case you are totally new to Rust, I would suggest you go through our detailed post on Getting Started with Rust Programming Language.

1 – What are functions in Rust?

Functions are quite common in Rust. For that matter, functions form the basis of almost all programming languages.

Whenever we create a brand new Rust program, we have to deal with the main() function. The main function is the entry point to a Rust program.

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

We can declare new functions using the fn keyword. For example, let’s create a new function named display_welcome_msg. This function prints another line of text.

fn display_welcome_msg() {
   println!("Welcome to Rust Function Demo")
}

We can call our new function inside the main() function.

fn main() {
   println!("Hello, World");
   display_welcome_msg()
}

If we run the program now using the cargo run command, we will see the two messages displayed in the terminal. First, we have the Hello, world and then Welcome to Rust Function Demo.

In Rust it doesn’t matter where we declare the function. For example, if we move display_welcome_msg above main() and then run the program again, we get the same output.

fn display_welcome_msg() {
   println!("Welcome to Rust Function Demo")
}
 
fn main() {
   println!("Hello, World");
   display_welcome_msg()
}

2 – Rust Functions with Parameters

Rust Functions can also have parameters. These are special variables that are part of the function’s definition.

Let’s declare a function with parameters. We will call it display_msg_count.

fn display_msg_count(count: u32) {
   println!("There are {} unread messages", count);
}

Within the function brackets, we specify the parameter count. The type of count is also mentioned as a 32 bit unsigned integer or u32.

We can now call this function from the main() function with some dummy value for count.

fn main() {
   println!("Hello, World");
   display_welcome_msg();
   display_msg_count(87);
}

The println macro prints the message in display_msg_count. Basically, it substitutes the curly braces with the value of count coming as input parameter.

We can also have multiple parameters in function declarations. For example, we create a function named display_msg_info.

fn display_msg_info(read: u32, unread: u32) {
   println!("You have {} read messages and {} unread messages", read, unread);
}

The above function takes two inputs – read and unread. Both have the same type i.e. a 32-bit unsigned integer. We can call the method just like earlier. Only difference being that we are now passing two parameters instead of one.

fn main() {
   println!("Hello, World");
   display_welcome_msg();
   display_msg_count(87);
   display_msg_info(500, 87)
}

3 – Rust Statement vs Expression

In Rust, a function body is made up of statements and expressions.

A statement is an instruction that performs some action but does not return anything. For example, let’s have a function get_number().

fn get_number() {
   let number = 5;
}

It has an instruction let number = 5. This is basically a statement. And statements do not return anything.

Therefore, we can’t do something like below:

fn get_number() {
   let anotherNumber = (let number = 5);
}

If we try to run the code, we get a compilation error saying that an expression was expected but found a statement. Basically, this means that let anotherNumber = needs an expression. The result of the expression would be bound to variable anotherNumber. However, let number = 5 is a statement that does not return anything.

This may not seem like a big deal. However, the fact is that such an assignment is valid for other languages such as C or Ruby. We could write something like anotherNumber = number = 5 in C and both anotherNumber and number will get the value of 5.

Expressions, on the other hand, evaluate to a value. In fact, expressions can be a part of other statements. For example, in the statement let number = 5, 5 is the expression. In other words, it evaluates to a value of 5.

Below is another example of an expression in Rust.

fn get_number() {
   let number = 5 + 4;
}

In this statement, 5 + 4 is an expression that evaluates to 9.

4 – Return Values in Rust Functions

Functions in Rust can also return values. In the function definition, we have to specify the type of the return value. See below example:

fn get_number() -> u32 {
}

Here, the return value is an unsigned 32 bit integer. 

As we saw a while back, statements do not return anything. They are just statements that perform some action.

However, if a function is returning some value, then it means that the last instruction to be executed in a function should be an expression. Below is a valid way of returning value from the function.

fn get_number() -> u32 {
   5
}

If we call this get_number() function from the main() function and then run this program using the cargo run command, this is going to return the number 5.

fn main() {
   println!("Hello, World");
   display_welcome_msg();
   display_msg_count(87);
   display_msg_info(500, 87);
 
   let number = get_number();
   println!("Number: {}", number)
}

There is no need to have any let statement or any other function calls. Another interesting aspect is the absence of any semicolon after 5.

In Rust, expressions do not include ending semicolons.

If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value.

In fact, if we add a semicolon after 5 and run the program, we get a compilation error saying mismatched types. Basically, our function declaration meant that we would return an unsigned 32-bit integer. But instead we returned an empty response. This empty response in Rust is also known as the unit type.

Of course, we could also have a normal approach of returning values from a function.

fn get_total_msgs(read: u32, unread: u32) -> u32 {
   let total = read + unread;
   return total
}

Here, we return the response using the return keyword.

We could have also cut down the function body by simply removing the let statement and instead just writing read + unread without the semicolon. See below:

fn get_total_msgs(read: u32, unread: u32) -> u32 {
   read + unread
}

The above approach also works just fine. Therefore, it is upto developer preference to go with one approach or another. It might be a good idea to follow one approach on a program level to have better code readability.

5 – Rust Main Function Return Type

Having gone through quite a few examples on Rust functions, one question that begs the attention is the return type of the main() function.

Can the main() function return something?

The main() function can return a result type. The result type helps us set appropriate response or even exit codes in the case of an error.

See below example:

use std::num::ParseIntError;
 
fn main() -> Result<(), ParseIntError>{
   let number_str = "This is a fake number";
   let number = match number_str.parse::<i32>() {
       Ok(number)  => number,
       Err(e) => return Err(e),
   };
   println!("{}", number);
   Ok(())
}

We can change the definition of our main() function to return a Result type consisting of unit type or a ParseIntError. Inside the function body, we declare a random string value.

Then, we declare another variable and we use the parse function to try and parse the number_str. If it is really a number we set Ok and if there was an error while parsing, then we set the error response.

If we run the program now, we will see an error, specifically the ParseIntError with a special type of InvalidDigit.

Error: ParseIntError { kind: InvalidDigit }

Conclusion

Functions in Rust are quite straightforward and intuitive. With proper type-safety in parameters and return values, Rust functions ensure that the compile step detects any errors.

If you have any comments or queries functions in Rust, please feel free to mention them in the comments section below.

Categories: BlogRust

Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *