Functions

Functions are the building blocks of Rust code. We've already seen main(), but Rust has rich function capabilities.

Function Basics

Defining Functions

fn main() {
    println!("Hello from main!");
    another_function();
}

fn another_function() {
    println!("Hello from another function!");
}

Python comparison:

def main():
    print("Hello from main!")
    another_function()

def another_function():
    print("Hello from another function!")

if __name__ == "__main__":
    main()

Key differences:

  • Rust: fn keyword, type annotations required
  • Python: def keyword, type hints optional
  • Rust: Use snake_case for function names (same as Python)

Parameters

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {}{}", value, unit_label);
}

Python comparison:

def print_labeled_measurement(value: int, unit_label: str):
    print(f"The measurement is: {value}{unit_label}")

print_labeled_measurement(5, 'h')

Type annotations are mandatory in Rust (optional in Python).

Return Values

fn five() -> i32 {
    5  // No semicolon - this is an expression
}

fn plus_one(x: i32) -> i32 {
    x + 1  // Expression returns value
}

fn main() {
    let x = five();
    println!("The value of x is: {}", x);
    
    let y = plus_one(5);
    println!("The value of y is: {}", y);
}

Python comparison:

def five() -> int:
    return 5  # Explicit return required

def plus_one(x: int) -> int:
    return x + 1

x = five()
y = plus_one(5)

Key difference: In Rust, the last expression is automatically returned (no return keyword needed). Python always requires explicit return.

Explicit Return

fn add(a: i32, b: i32) -> i32 {
    return a + b;  // Early return with return keyword
}

fn max(a: i32, b: i32) -> i32 {
    if a > b {
        return a;  // Early return
    }
    b  // Final expression
}

Statements vs Expressions

Statements

Don't return values:

fn main() {
    let y = 6;  // Statement
    // let x = (let y = 6);  // ERROR: statements don't return values
}

Expressions

Return values:

fn main() {
    let y = {
        let x = 3;
        x + 1  // Expression (no semicolon)
    };
    
    println!("The value of y is: {}", y);  // 4
}

Key Difference: Expressions don't end with semicolons.

fn main() {
    let x = 5;
    
    let y = {
        let x = 3;
        x + 1  // Expression - returns 4
    };
    
    let z = {
        let x = 3;
        x + 1;  // Statement (semicolon) - returns ()
    };
    
    println!("y = {}, z = {:?}", y, z);  // y = 4, z = ()
}

Multiple Return Values

Use tuples:

fn calculate(x: i32, y: i32) -> (i32, i32, i32) {
    (x + y, x - y, x * y)
}

fn main() {
    let (sum, diff, product) = calculate(10, 5);
    println!("sum: {}, diff: {}, product: {}", sum, diff, product);
}

Method Syntax

Methods are functions defined within a struct, enum, or trait context.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // Method with &self
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    // Method with &mut self
    fn expand(&mut self, amount: u32) {
        self.width += amount;
        self.height += amount;
    }
    
    // Associated function (no self)
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("Area: {}", rect.area());
    
    let mut rect2 = Rectangle { width: 10, height: 10 };
    rect2.expand(5);
    println!("Width: {}", rect2.width);
    
    let sq = Rectangle::square(20);
    println!("Square area: {}", sq.area());
}

self, &self, &mut self

SyntaxOwnershipUse Case
selfTakes ownershipConsuming the value
&selfBorrows immutablyRead-only access
&mut selfBorrows mutablyModifying the value

Closures

Anonymous functions that can capture their environment.

Basic Closure

fn main() {
    let add_one = |x| x + 1;
    println!("{}", add_one(5));  // 6
}

Closure Syntax

fn main() {
    // Minimal
    let add = |x, y| x + y;
    
    // With types
    let add: fn(i32, i32) -> i32 = |x, y| x + y;
    
    // With body block
    let add = |x, y| {
        let result = x + y;
        result
    };
    
    println!("{}", add(2, 3));
}

Capturing Environment

fn main() {
    let x = 4;
    
    let equal_to_x = |z| z == x;  // Captures x
    
    let y = 4;
    println!("{}", equal_to_x(y));  // true
}

Capture Modes

ModeSyntaxDescription
BorrowDefault|x| x + env
Mutable borrowDefault if needed|x| { env += x; }
Movemove keywordmove |x| x + env
fn main() {
    let list = vec![1, 2, 3];
    
    // Borrows list
    let only_borrows = || println!("List: {:?}", list);
    only_borrows();
    println!("List still valid: {:?}", list);
    
    // Takes ownership
    let move_closure = move || println!("List: {:?}", list);
    move_closure();
    // println!("{:?}", list);  // ERROR: list was moved
}

Using Closures with Iterators

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    // Map
    let doubled: Vec<i32> = numbers.iter()
        .map(|x| x * 2)
        .collect();
    
    // Filter
    let even: Vec<i32> = numbers.iter()
        .filter(|&x| x % 2 == 0)
        .copied()
        .collect();
    
    // Find
    let first_large = numbers.iter()
        .find(|&&x| x > 3);
    
    println!("Doubled: {:?}", doubled);
    println!("Even: {:?}", even);
    println!("First large: {:?}", first_large);
}

Function Pointers

Functions can be passed as arguments:

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let result = do_twice(add_one, 5);
    println!("Result: {}", result);  // 12
}

Higher-Order Functions

Functions that take or return functions:

fn apply_operation(x: i32, y: i32, op: fn(i32, i32) -> i32) -> i32 {
    op(x, y)
}

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

fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

fn main() {
    println!("10 + 5 = {}", apply_operation(10, 5, add));
    println!("10 * 5 = {}", apply_operation(10, 5, multiply));
}

Common Patterns

Builder Pattern

struct Config {
    host: String,
    port: u16,
    timeout: u64,
}

impl Config {
    fn new() -> Self {
        Config {
            host: "localhost".to_string(),
            port: 8080,
            timeout: 30,
        }
    }
    
    fn host(mut self, host: &str) -> Self {
        self.host = host.to_string();
        self
    }
    
    fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }
    
    fn build(self) -> Self {
        self
    }
}

fn main() {
    let config = Config::new()
        .host("example.com")
        .port(3000)
        .build();
    
    println!("{}:{}", config.host, config.port);
}

Option Returning Functions

fn divide(dividend: f64, divisor: f64) -> Option<f64> {
    if divisor == 0.0 {
        None
    } else {
        Some(dividend / divisor)
    }
}

fn main() {
    match divide(10.0, 2.0) {
        Some(result) => println!("Result: {}", result),
        None => println!("Cannot divide by zero"),
    }
}

Combinator Pattern

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    
    let result: i32 = numbers
        .iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .sum();
    
    println!("Sum of squares of evens: {}", result);  // 20
}

Practice Exercises

Exercise 1: Temperature Conversion

Create functions to convert between Celsius and Fahrenheit:

fn celsius_to_fahrenheit(c: f64) -> f64 {
    // Your code
}

fn fahrenheit_to_celsius(f: f64) -> f64 {
    // Your code
}

Exercise 2: Fibonacci

Write a function that returns the nth Fibonacci number:

fn fibonacci(n: u32) -> u32 {
    // Your code
}

Exercise 3: Filter and Transform

Use closures to:

  1. Filter a list of numbers to only evens
  2. Square each number
  3. Sum the results
fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    // Your code
}

Exercise 4: Calculator

Create a calculator that takes an operation as a function pointer:

fn calculate(a: i32, b: i32, op: fn(i32, i32) -> i32) -> i32 {
    op(a, b)
}

// Define add, subtract, multiply, divide functions

Key Takeaways

  • Functions use snake_case naming
  • Parameters must have type annotations
  • Return types are specified with ->
  • Expressions return values, statements don't
  • Methods are functions in impl blocks
  • Closures are anonymous functions that capture environment
  • Function pointers allow passing functions as arguments
  • Use iterators with closures for powerful data transformations

Next Steps

In the next chapter, we'll explore structs and enums - Rust's way of creating custom data types.

Quick Reference

// Function
fn add(x: i32, y: i32) -> i32 {
    x + y
}

// Method
impl MyStruct {
    fn method(&self) {}
}

// Closure
let add = |x, y| x + y;

// Closure with move
let closure = move || { /* uses captured variables */ };

// Function pointer
fn do_twice(f: fn(i32) -> i32, x: i32) -> i32 {
    f(x) + f(x)
}

// Iterator with closures
vec.iter()
   .filter(|&x| x > 0)
   .map(|x| x * 2)
   .collect()