Rust - Functions
Overview
Estimated time: 45–60 minutes
Learn how to define and use functions in Rust, including parameters, return values, and the distinction between expressions and statements. Functions are the building blocks of Rust programs.
Learning Objectives
- Define functions with parameters and return values
- Understand expressions vs statements in Rust
- Use function parameters and return types effectively
- Organize code with functions and understand scope
Prerequisites
Function Basics
Functions in Rust are declared with the fn
keyword. The main
function is special - it's where execution begins.
fn main() {
println!("Hello, world!");
// Call other functions
greet();
say_hello_to("Alice");
}
fn greet() {
println!("Hello there!");
}
fn say_hello_to(name: &str) {
println!("Hello, {}!", name);
}
Expected output:
Hello, world!
Hello there!
Hello, Alice!
Function Parameters
Functions can accept parameters. Each parameter must have its type specified:
fn main() {
add_numbers(5, 3);
print_info("Rust", 2015, true);
}
fn add_numbers(x: i32, y: i32) {
println!("{} + {} = {}", x, y, x + y);
}
fn print_info(name: &str, year: i32, is_systems_lang: bool) {
println!("Language: {}", name);
println!("Created: {}", year);
println!("Systems language: {}", is_systems_lang);
}
Expected output:
5 + 3 = 8
Language: Rust
Created: 2015
Systems language: true
Return Values
Functions can return values. The return type is specified after an arrow ->
:
fn main() {
let result = add(10, 20);
println!("10 + 20 = {}", result);
let area = calculate_area(5.0, 3.0);
println!("Area: {}", area);
}
fn add(x: i32, y: i32) -> i32 {
x + y // No semicolon - this is an expression that returns
}
fn calculate_area(width: f64, height: f64) -> f64 {
width * height
}
Expected output:
10 + 20 = 30
Area: 15
Early Returns
You can return early from a function using the return
keyword:
fn main() {
println!("Absolute value of -5: {}", absolute_value(-5));
println!("Absolute value of 8: {}", absolute_value(8));
let grade = letter_grade(85);
println!("Grade for 85: {}", grade);
}
fn absolute_value(x: i32) -> i32 {
if x < 0 {
return -x; // Early return
}
x // Final expression
}
fn letter_grade(score: i32) -> char {
if score >= 90 {
return 'A';
} else if score >= 80 {
return 'B';
} else if score >= 70 {
return 'C';
} else if score >= 60 {
return 'D';
}
'F'
}
Expected output:
Absolute value of -5: 5
Absolute value of 8: 8
Grade for 85: B
Expressions vs Statements
Understanding the difference between expressions and statements is crucial in Rust:
fn main() {
// Statement: let binding
let x = 5;
// Expression: can be assigned to a variable
let y = {
let inner = 3;
inner + 1 // Expression - no semicolon
};
println!("x: {}, y: {}", x, y);
// Function calls are expressions
let sum = add_one(10);
println!("Sum: {}", sum);
// If is an expression
let bigger = if x > y { x } else { y };
println!("Bigger: {}", bigger);
}
fn add_one(x: i32) -> i32 {
x + 1 // Expression
}
Expected output:
x: 5, y: 4
Sum: 11
Bigger: 5
Unit Type
Functions that don't return a meaningful value return the unit type ()
:
fn main() {
let result = print_message("Hello!");
println!("Result: {:?}", result); // () - unit type
// These are equivalent
do_something();
do_something_explicit();
}
fn print_message(msg: &str) {
println!("{}", msg);
// Implicitly returns ()
}
fn do_something() {
println!("Doing something...");
}
fn do_something_explicit() -> () {
println!("Doing something explicitly...");
}
Expected output:
Hello!
Result: ()
Doing something...
Doing something explicitly...
Function Organization
Functions help organize code and make it reusable:
fn main() {
let radius = 5.0;
println!("Circle with radius {}:", radius);
println!(" Area: {:.2}", circle_area(radius));
println!(" Circumference: {:.2}", circle_circumference(radius));
let temp_f = 98.6;
let temp_c = fahrenheit_to_celsius(temp_f);
println!("{}°F = {:.1}°C", temp_f, temp_c);
}
fn circle_area(radius: f64) -> f64 {
std::f64::consts::PI * radius * radius
}
fn circle_circumference(radius: f64) -> f64 {
2.0 * std::f64::consts::PI * radius
}
fn fahrenheit_to_celsius(fahrenheit: f64) -> f64 {
(fahrenheit - 32.0) * 5.0 / 9.0
}
Expected output:
Circle with radius 5:
Area: 78.54
Circumference: 31.42
98.6°F = 37.0°C
Common Pitfalls
1. Missing Return Type
When a function should return a value, don't forget the return type:
// Wrong: compiler error
// fn double(x: i32) {
// x * 2
// }
// Correct: specify return type
fn double(x: i32) -> i32 {
x * 2
}
fn main() {
println!("Double of 5: {}", double(5));
}
2. Adding Semicolon to Return Expression
Adding a semicolon turns an expression into a statement:
fn main() {
println!("Triple of 4: {}", triple(4));
}
// Wrong: semicolon makes it return ()
// fn triple(x: i32) -> i32 {
// x * 3; // This is a statement, not an expression
// }
// Correct: no semicolon for return expression
fn triple(x: i32) -> i32 {
x * 3 // Expression that returns the value
}
3. Parameter Type Annotation
All function parameters must have type annotations:
fn main() {
println!("Square of 6: {}", square(6));
}
// Wrong: missing type annotation
// fn square(x) -> i32 {
// x * x
// }
// Correct: all parameters need types
fn square(x: i32) -> i32 {
x * x
}
Advanced Function Examples
Multiple Return Values with Tuples
fn main() {
let (quotient, remainder) = divide_with_remainder(17, 5);
println!("17 ÷ 5 = {} remainder {}", quotient, remainder);
let (min, max) = find_min_max(vec![3, 7, 1, 9, 2]);
println!("Min: {}, Max: {}", min, max);
}
fn divide_with_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
fn find_min_max(numbers: Vec) -> (i32, i32) {
let mut min = numbers[0];
let mut max = numbers[0];
for &num in &numbers {
if num < min {
min = num;
}
if num > max {
max = num;
}
}
(min, max)
}
Expected output:
17 ÷ 5 = 3 remainder 2
Min: 1, Max: 9
Functions with Complex Logic
fn main() {
println!("Is 17 prime? {}", is_prime(17));
println!("Is 15 prime? {}", is_prime(15));
println!("Factorial of 5: {}", factorial(5));
println!("Fibonacci of 10: {}", fibonacci(10));
}
fn is_prime(n: u32) -> bool {
if n < 2 {
return false;
}
for i in 2..=(n as f64).sqrt() as u32 {
if n % i == 0 {
return false;
}
}
true
}
fn factorial(n: u32) -> u32 {
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
Expected output:
Is 17 prime? true
Is 15 prime? false
Factorial of 5: 120
Fibonacci of 10: 55
Checks for Understanding
Question 1: What's the difference between fn calculate() { 5 + 3 }
and fn calculate() -> i32 { 5 + 3 }
?
Answer: The first function returns the unit type ()
and ignores the calculated value. The second function returns i32
and actually returns the result of 5 + 3
. Without the return type annotation, Rust assumes the function returns ()
.
Question 2: Why does this code fail to compile?
fn double(x) -> i32 {
x * 2
}
Answer: The parameter x
is missing a type annotation. In Rust, all function parameters must have explicit types. It should be fn double(x: i32) -> i32
.
Question 3: What's wrong with this function?
fn add(x: i32, y: i32) -> i32 {
x + y;
}
Answer: The semicolon after x + y
turns it into a statement instead of an expression. This makes the function return ()
instead of the sum. Remove the semicolon: x + y
.
Question 4: How would you write a function that takes two parameters and returns the larger one?
Answer:
fn max(x: i32, y: i32) -> i32 {
if x > y {
x
} else {
y
}
}