Functions

Functions are a fundamental element of any programming language. They are used to define a piece of code that can be executed multiple times, without having to rewrite the same code. Functions are used to break down a program into smaller, more manageable pieces.

In Rust, the naming convention for functions is to use snake_case. This means that function names should be all lowercase, with words separated by underscores.

Functions in Rust are defined using the fn keyword, followed by the function name, a list of parameters enclosed in parentheses, and a block of code enclosed in curly braces.

Let's take a look at a simple function in Rust:

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

In the example above, we have defined a function called greet that prints Hello, World! to the console. To call this function, we simply write its name followed by parentheses:

fn main() {
    greet();
}

When we run the program, it will output Hello, World! to the console.

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Function Parameters

Functions are powerful when they can accept input values, called parameters. Parameters allow us to pass data to a function, which the function can then use to perform its task.

Parameters are defined inside the parentheses after the function name. Each parameter consists of a name followed by a colon and the parameter type. Multiple parameters are separated by commas. The Rust compiler expects the type of each parameter to be specified.

Here's an example of a function that takes two parameters:

fn add(a: i32, b: i32) {
    let sum = a + b;
    println!("The sum of {} and {} is {}", a, b, sum);
}

In the example above, we have defined a function called add that takes two parameters a and b, both of type i32 (a 32-bit signed integer). The function calculates the sum of the two parameters and prints the result to the console.

To call this function, we need to provide values for the parameters:

fn main() {
    add(5, 3);
}

Add function Add function

Expression vs Statement

The difference between an expression and a statement is that an expression evaluates to a value, while a statement does not.

In the function we defined earlier, the a + b is an expression because it evaluates to a value, if we provide 5 for a and 3 for b, the expression will evaluate to 8, therefore it is considered an expression.

Statements on the other hand do not evaluate to a value. For example, the let sum = a + b; statement is not an expression because it does not evaluate to a value. It is a statement that assigns the value of a + b to the variable sum, but the statement itself does not evaluate to a value.

Example of a statement:

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

Example of an expression:

fn main() {
    let x = 5;
    let y = 10;
 
    x + y // Expression
}

Statements perform actions without directly producing a value, while expressions evaluate to a value.

In Rust you can create a new scope using curly braces {}. The last expression in a block is considered the return value of the block. This is useful when you want to declare variables in a block that doesn't pollute the outer scope.

fn main() {
    let x = {
        let y = 5;
        y + 1 // This expression will be the value of `x`
    };
 
    println!("The value of x is: {}", x);
}

New block expression New block expression

Function Return Values

Functions are more powerful and useful when they return a value. In fact, it's preferred for functions to return a value rather than mutating outside state. The returned value then can be used wherever the function is called.

We also need to explicitly specify the return type of a function. The return type is specified after an arrow (->) following the parameter list. The return type is the type of the value that the function will return.

Let's change the add function to return the sum of two numbers instead of printing it:

fn add(a: i32, b: i32) -> i32 { // Added `-> i32` to specify the return type
    return a + b;
}

In the example above, we have changed the add function by adding an arrow (->) followed by i32 to specify that the function will return a 32-bit signed integer. The function now returns the sum of the two parameters a and b.

There is also a shorthand syntax for returning a value from a function. We can remove the return keyword and the semicolon from the last expression in the function, and Rust will automatically return the value:

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

Remember that you also need to remove the semicolon from the last expression in the function. If you add a semicolon, it will turn the expression into a statement, and the function will not return a value.

To use the returned value, we need to assign it to a variable or use it directly:

fn main() {
    let result = add(5, 3);
 
    println!("The sum of 5 and 3 is {}", result);
}

This will output The sum of 5 and 3 is 8 to the console. Another way to do this is to directly use the returned value:

fn main() {
    println!("The sum of 5 and 3 is {}", add(5, 3));
}

Pure Functions

Functions that do not modify external state and always produce the same output for the same input are called pure functions. In functional programming, pure functions are preferred because they are more predictable and they do not cause any side effects that could be difficult to debug.

Here's an example of a pure function (without side effects):

use sha256::digest;
 
fn generate_sha_256_hash(input: &str) -> String {
    let hash = digest(input.as_bytes());
 
    hash
}

In the example above, we have defined a function called generate_sha_256_hash that takes a string as input and returns a SHA-256 hash of the input string. This function is a pure function because it always produces the same hash as long as the input is the same.

To run the function add the sha256 crate by running cargo add sha256 in the terminal.

use sha256::digest;
 
fn generate_sha_256_hash(input: &str) -> String {
    let hash = digest(input.as_bytes());
 
    hash
}
 
fn main() {
    let input = "Hello, world!";
    let hash = generate_sha_256_hash(input);
 
    println!("The SHA-256 hash of '{}' is: {}", input, hash);
}

SHA256 generator Rust SHA256 generator Rust

Side Effects

Side effects are changes to external state that are caused by a function. Functions with side effects modify external state, such as writing to a file, or modifying a global variable. Side effects can make the behavior of a program harder to predict and debug.

Let's take a look at an example of a function with side effects:

fn write_to_file(data: &str, filename: &str) {
    let mut file = File::create(filename).expect("Failed to create file");
    file.write_all(data.as_bytes()).expect("Failed to write to file");
}

In the example above, we have defined a function called write_to_file that takes a string data and a filename as input and writes the data to a file with the specified filename. This function has side effects because it modifies external state (the file system) by writing data to a file.

Function Overloading

Rust does not support function overloading in the traditional sense. Function overloading is the ability to define multiple functions with the same name but different parameter types or numbers of parameters. In languages that support function overloading, the compiler determines which function to call based on the number and types of arguments provided.

The language's powerful type system, including generics and traits, provides flexible ways to achieve similar outcomes without the ambiguity or complexity that can arise from traditional function overloading. We will be exploring these concepts in more detail in later chapters.

Conclusion

Functions are a fundamental building block of Rust programs. They allow us to break down a program into smaller, more manageable pieces, and they can accept input values and return output values. Functions can be used to perform specific tasks, and they can be called multiple times from different parts of the program.


In the next chapter, we will learn about comments and documentation in Rust. This will help us write clear and understandable code that is easy to maintain and share with others.