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

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
    }
}

← PreviousNext →