Functions
You were first introduced to the function, arguably the most important part of a program. We have seen the main
function, so let’s learn how to create other functions.
Here are examples of different types of functions in Rust.
Function with No Return Statement or Argument:
fn greet() {
println!("Hello, world!");
}
In this example, the greet
function doesn't take any arguments or return any value. It simply prints out, "Hello, world!".
Function with Argument and No Return Statement:
fn say_hello(name: &str) {
println!("Hello, {}!", name);
}
Here, the say_hello
function takes a single argument of type &str
(a string slice) and prints a personalized greeting using that argument.
Function with Argument and Return Statement:
fn square(n: i32) -> i32 {
n * n
}
In this example, the square
function takes an i32
argument and returns the square of that argument as an i32
.
Function with Multiple Arguments and Return Statements:
fn calculate_power(base: f64, exponent: i32) -> f64 {
if exponent == 0 {
1.0
} else {
let mut result = base;
for _ in 1..exponent.abs() {
result *= base;
}
if exponent < 0 {
1.0 / result
} else {
result
}
}
}
In this example, the calculate_power
function takes a base
of type f64
and an exponent
of type i32
. It calculates the base's power raised to the exponent, considering both positive and negative exponents.
You can call each of these in the main function and run the program with cargo:
fn main() {
fn main() {
greet();
say_hello("John");
println!("{}", square(5));
println!("{}", calculate_power(10.0, 10));
}
/* Output is:
❯ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello`
Hello, world
Hello, John
25
10000000000
*/
Note: The /* … */ is how to make longer, multi-line comments in Rust code. Also, the placeholders ({}
) must be used to print out a message to the console. In Rust, the practice of printing out messages is often called debugging. Sometimes, the Rust compiler will not allow you to use the println
macro for some operations because they do not support debugging.
Modules
If you know software engineering principles like hexagonal architecture, you will be familiar with modules. Generally, breaking down functions into different modules in a growing code base is best.
We can do this in Rust by leveraging the mod.rs
file, a specially recognized and reserved file for Rust projects. Inside the src/
directory, you can also have other subdirectories called modules. Using them in the project must be linked to the main.rs
or lib.rs
file.
In Rust, modules organize code into separate, named units, allowing you to group related functions, types, and other items. This helps improve code organization, maintainability, and readability by breaking your program into smaller, manageable pieces.
Here's how you can work with modules in Rust. Inside the src/
directory, create a new directory called database. In it, create a mod.rs
file and a model.rs
file. The mod.rs
file gives visibility to other files in any subdirectory under the src/
directory. To practically understand this, add the following code to the specified files:
mod.rs
:
pub mod model;
model.rs
:
pub fn create_user(name: &str, age: i32) {
println!("New user created: {} of age {}", name, age);
}
main.rs
:
pub mod database; // make the database module available in this file.
pub use database::*; // give this file access to all public modules and their functions (*) inside of the database module.
fn main() {
let name = "John";
let age = 35;
database::model::create_user(name, age);
}
Note that we are using a clean main.rs
file. Run the code with cargo, and you should see:
❯ cargo run
Compiling hello v0.1.0 (/.../workspace/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running `target/debug/hello`
New user created: John of age 35
What did we do? We created a subdirectory, added a model.rs
file, and exported it with mod.rs
. We then called the subdirectory inside the main function (as we would do in any Rust source file that needs the contents of model.rs
), and then we used the create_user
function in the main
function.
Note: In Rust, the double semicolon notation is used to call methods of a module/package, not the dot notation like in Go. Since we already specified to use all modules public modules and their respective functions using this line pub use database::*;
, we can shorten the calling technique to this: model::create_user(name, age);
.
We can also do this:
pub mod database;
pub use database::model::*;
fn main() {
let name = "John";
let age = 35;
create_user(name, age);
}
Or this:
pub mod database;
pub use database::model::create_user;
// use only the create_user function from the model module in the database package.
fn main() {
let name = "John";
let age = 35;
create_user(name, age);
}
Ensure you get the logic before proceeding to the next section.
Note: Rust is feature-rich. For example, we can create separate modules inside one single Rust file using the mod
keyword. We can also write modules inside the mod.rs
file instead of having a separate model.rs file.
But this way shown above is a more maintainable and readable way to do it, as is the initial goal of modular programming. If your application is small and lightweight, you can do either, but use the approach taught here if it is bound to scale/grow bigger.