Functions in Rust
In this 4th lesson, we will learn about functions in Rust.
If you prefer a video version
What is a function?
A function is a set of statements that performs a task.
Functions are used to organize code into logical blocks, making it easier to read and maintain.
Functions are also used to avoid code duplication.
Basics
we know that the main function is essential in every Rust program. For instance:
fn main() {
println!("Hello, world!");
}
Let's declare another_function and call it from main.
We use the snake_case naming convention for functions.
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("another function in snake_case");
}
Will it compile?
Yes, in Rust, the order of function declarations is not important, allowing flexibility in organizing code.
Parameters
Parameters are special variables part of the function's signature, used to pass data to the function.
Although the terms parameters and arguments are often used interchangeably, technically, parameters refer to the variables in the function's definition, while arguments are the concrete values passed to the function.
Let's add a parameter to our function:
fn main() {
another_function(42);
}
fn another_function(num) {
println!("The values is, {}!", num);
}
Will it compile?
No. This is because Rust is a statically typed language. This means that the compiler must know the type of every variable at compile time.
Here, the compiler doesn't know the type and it cannot infer it from the function body.
In function signatures, you must declare the type of each parameter.
This is a deliberate decision in Rust’s design: requiring type annotations in function definitions means the compiler almost never needs you to use them elsewhere in the code to figure out what type you mean.
The compiler is also able to give more helpful error messages if it knows what types the function expects.
fn main() {
function2(42);
}
fn function2(num: i32) {
println!("The values is, {}!", num);
}
Will it compile?
Yes. Now the compiler knows that num
is an i32
.
Multiple Parameters
When defining multiple parameters, you must separate them with a comma.
fn main() {
function2(42, 'a');
}
fn function2(num: i32, letter: char) {
println!("The values are, {} and {}!", num, letter);
}
Statements and Expressions
Understanding the difference between statements and expressions is crucial in Rust:
Statements are instructions that perform some action but do not return a value.
Expressions evaluate to a resulting value.
In Rust, functions can be considered as expressions, meaning they evaluate to a value.
For example, creating a variable is a statement, as it does not return a value:
fn main() {
let x = 42;
}
Can we do this?
fn main() {
let x = (let y = 42);
}
No, because let y = 42
is a statement and statements cannot be used as expressions.
Note that this would be valid in other languages, such as C, where the assignment operator returns the value assigned.
Expressions can be used as statements, but they will not return a value.
fn main() {
let x = 5;
let y = 6;
let z = x + y;
}
- Calling a function is an expression.
- Calling a macro is an expression.
- A new scope block created with curly brackets is an expression, for example:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
This block evaluates to 4, which is then assigned to y.
Note the absence of a semicolon in the last expression of the block.
Return Values
Functions can return values to the code that calls them.
We don't name return values, but we do declare their type after an arrow (->
).
Example:
fn sum() -> i32 {
1 + 2
}
fn main() {
let x = sum();
println!("The value of x is: {x}");
}
This works because the last expression in the function is 1 + 2
, which evaluates to 3, and 3 is returned from the function.
fn sum(num1:i32, num2:i32) -> i32 {
num1 + num2
}
fn main() {
let x = sum(5, 7);
println!("The value of x is: {x}");
}
We can return multiple values by separating them with a comma, and the return type must be a tuple:
//Return values in functions
fn sum_diff(num1:i32, num2:i32) -> (i32, i32) {
(num1 + num2, num1 - num2)
}
fn main() {
let x = sum_diff(10,8);
println!("Sum and Diff is {:?}", x);
}
In this case, we return a tuple containing the sum and the difference of the two numbers.
The {:?} is a debug print
Early Returns
Functions can return early with the return
keyword.
//Return values in functions
fn sum_diff(num1:i32, num2:i32) -> (i32, i32) {
return (num1 + num2, num1 - num2);
}
fn main() {
let x = sum_diff(10,8);
println!("Sum and Diff is {:?}", x);
}
Functions can return early with the return
keyword.
//Return values in functions
fn sum_diff(num1:i32, num2:i32) -> (i32, i32) {
return (num1 + num2, num1 - num2);
println!("This line will not be executed");
}
fn main() {
let x = sum_diff(10,8);
println!("Sum and Diff is {:?}", x);
}
in this case, the last line in the sum_diff function will not be executed.
Recap
To make a recap:
- they use the
fn
keyword - they are snake_case
- they can be declared anywhere
- they can be declared with or without parameters
- they can return only one type, but it can be any type, including compund and custom types
- they can return early with the
return
keyword
If you prefer a video version
You can find me here: Francesco Ciulla