Functions

Write a Rust Function

Rust by example - Functions

Functions are fundamental building blocks in Rust, used to encapsulate reusable blocks of code. Functions are defined with fn, followed by the function name, optional parameters between parentheses (...), optional -> followed by the return type, and curly braces { ... } for the function body. Rust uses snake_case for function names by convention.

Here are examples of functions with or without parameters or return value:


/// Function with no parameters, no return type.
fn say_hello() {
    println!("Hello, Rust!");
}

/// Function with one parameter called `name` of type `&str` (string slice).
fn greet(name: &str) {
    println!("Hello, {name}!");
}

// Function with two parameters: `a` and `b`, both of type `i32`.
fn add_and_display(a: i32, b: i32) {
    let sum = a + b;
    println!("{a} + {b} = {sum}");
}

/// Function that takes two `i32` values and returns their sum as an `i32`.
fn multiply(x: i32, y: i32) -> i32 {
    x * y // No semicolon: this expression's value is returned.
}

/// Function bodies follow the same rules than blocks:
#[allow(clippy::let_and_return)]
fn foo(x: i32) -> i32 {
    let y = {
        let z = 3;
        x + z // Blocks return the value of their last expression, if there is no semi-colon.
    };
    y // Similarly, functions return the value of their last expression, if there is no semi-colon.
}

/// Function that implicitly returns the unit type `()`.
/// This is equivalent to `fn print_coordinates(x: i32, y: i32) -> ()`.
fn print_coordinates(x: i32, y: i32) {
    println!("Coordinates: ({x}, {y})"); // Note the semicolon.
    // Implicitly return `()`.
}

/// Function using an explicit `return` keyword (rarely needed).
#[allow(clippy::manual_find)]
fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &num in numbers {
        if num % 2 == 0 {
            return Some(num); // Return early, breaking out of the `for` loop.
        }
    }
    None // Return `None` if no even number is found (last expression).
}

fn main() {
    // Call the functions above.
    say_hello();
    greet("Rustacean");
    add_and_display(5, 3);
    println!("2 * 3 = {}", multiply(2, 3));
    println!("Foo: {}", foo(1));
    print_coordinates(1, 2);
    println!(
        "First even number: {:?}",
        find_first_even(vec![1, 2, 3].as_slice())
    );
}

Write a Generic Function

Rust by example - generic functions

Functions can be generic, meaning they can operate on parameters of various types without needing to be rewritten for each type:


// This is a generic function that can take any type.
// The type parameter `T` is written between < and >.
// It often can be inferred from the argument passed to the function.
fn generic<T>(_: T) {
    println!("Argument type: {}", std::any::type_name::<T>());
}

/// A generic function can take multiple type parameters.
///
/// This function takes two arguments of potentially different types:
fn take_two<T, U>(_a: T, _b: U) {
    println!(
        "Argument types: {} {}",
        std::any::type_name::<T>(),
        std::any::type_name::<U>()
    );
}

fn main() {
    // The type parameter `T` is inferred to be `char` in this case.
    generic('a');

    // The type parameter `T` is inferred to be `i32` in this case.
    generic(1);

    // We can also explicitly specify the type parameter `char` for `generic()`.
    // Note the use of the 'turbofish' notation: `::<>`
    generic::<char>('a');

    take_two(Some(3), "4");
}

Type parameters can be constrained to implement specified traits:

use std::fmt::Debug;
use std::fmt::Display;

/// Generic functions can accept trait bounds.
// Here we define a generic function `print_value` that takes one argument
// `value` of any type `T`. The `T: std::fmt::Debug` part is a trait bound,
// meaning `T` must implement the `Debug` trait, so that it can be printed using
// the `{:?}` format specifier.
fn print_value<T: Debug>(value: T) {
    println!("The value is: {value:?}"); // `{:?}` can be used, because `value` implements `Debug`.
}

/// Type constraints can be written in a separate `where` clause for clarity.
/// You can also combine multiple trait bounds with `+`.
fn print_value2<T>(value: T)
where
    T: Debug + Display,
{
    println!("Compare {value} and {value:?}");
}

/// All type parameters have an implicit bound of `Sized`.
/// The special syntax `?Sized` can be used to remove this bound if it is not
/// appropriate.
///
/// This function can work with types that may not have a known size at compile
/// time.
fn generic<T: ?Sized + Display>(t: &T) {
    println!("{t}");
}

fn main() {
    // Call the generic function with an integer.
    print_value(10);

    // Call the generic function with a floating-point number.
    print_value(2.14);

    // Call the generic function with a string slice.
    print_value("hello");

    // Call the generic function with a custom struct that implements `Debug`.
    #[allow(dead_code)]
    #[derive(Debug)]
    struct MyStruct {
        value: i32,
    }
    print_value(MyStruct { value: 42 });

    let s = String::from("hello");
    generic(&s[..]);
    generic(&s);
}

Functions can also include one or more explicit lifetime parameters, which are also written with < and >. Generic type parameters, lifetime parameters and const generic parameters can be intermixed, but with lifetime parameters first.

/// Function with an explicit lifetime parameter.
///
/// `x` and `y` are string slices that live at least as long as the lifetime
/// `'a`. The reference returned by the longest function will have the same
/// lifetime as the lifetime `'a`.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    // The compiler infers the concrete lifetime for `'a` at the call site,
    // choosing the shorter of the two input lifetimes.
    let result = longest("abcd", "xyz");
    println!("The longest string is {result}");
}

Work with Diverging Functions

Functions that are guaranteed never to return (e.g., they loop forever or exit the process) have the special return type ! (the "never" type).

/// This function diverges, meaning it never returns.
/// It uses the `!` (Never) return type to indicate this.
fn foo() -> ! {
    panic!("This call never returns.");
}

// This function never returns control.
#[allow(dead_code)]
fn exit_program() -> ! {
    println!("Exiting the process...");
    std::process::exit(1);
}

// This function loops forever.
#[allow(dead_code)]
fn forever() -> ! {
    loop {
        println!("and ever...");
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
}

fn main() {
    foo(); // This will panic.
}

Work with Function Pointers

Functions are first-class citizens in Rust. This means you can treat them like any other value: assign them to variables, pass them as arguments to other functions, etc. The type of a function pointer looks like fn(i32) -> i32.

/// Example function: add one to the input.
fn add_one(x: i32) -> i32 {
    x + 1
}

/// This function takes a function pointer in argument.
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let func_ptr: fn(i32) -> i32 = add_one; // Assign a function pointer to a variable.
    println!("Calling via the pointer: {}", func_ptr(10)); // Output: 11.

    let answer = do_twice(add_one, 5); // Pass `add_one` as an argument.
    println!("The answer is: {answer}"); // Output: 12 ( (5+1) + (5+1) )
}

Return a Reference from a Function

You cannot directly return a reference to a local variable created within and owned by a function:

  • Local variables reside on the stack. This memory is typically deallocated when the function finishes executing.
  • Returning a reference to a local variable would result in a dangling reference. The reference would point to a memory location that is no longer valid or might be overwritten later, leading to undefined behavior.

In Rust, the borrow checker prevents returning references to local data, enforcing memory safety:

#![allow(unused)]
fn main() {
// Compile Error[E0515]: cannot return reference to temporary value.
fn try_create<'a>() -> &'a String {
    let s = String::new();
    &s
    // References are pointers to memory locations. Once the function is executed, the local variable is popped off the execution stack.
}

#[test]
fn test() {
    let _ = try_create();
}
}

Instead, you should typically:

  • Return an owned value (String instead of &str, Vec<T> instead of &[T], etc.).
  • Pass a reference as an argument: If the goal is to modify a variable, pass a mutable reference to a variable that exists in the calling scope. The function can then directly modify the original variable.
  • Use a literal, a static or a constant: Since a static variable lives as long as the process runs, its references will be pointing to the same memory location both inside and outside the function.

/// Return an owned type instead of a reference.
fn return_string() -> String {
    String::new()
}

/// Pass a mutable reference,
/// and modify in the function.
fn use_mut_ref(v: &mut Vec<i32>) {
    *v = vec![0; 3];
}

/// Variant: pass a mutable reference,
/// return an immutable reference.
fn use_mut_ref2(v: &mut Vec<i32>) -> &[i32] {
    *v = vec![0; 3];
    &*v
}

/// The following is possible but should be rare:
/// Use a literal.
/// This applies broadly to any item that is composed solely of literals.
fn use_literal() -> &'static str {
    let s: &'static str = "hello";
    s
}

/// Use a static variable.
fn use_static() -> &'static str {
    // Statics have by default a `'static` lifetime.
    static MY_STATIC: &str = "hello";
    MY_STATIC
}

/// Use a constant.
fn use_const() -> &'static str {
    // Constants have by default a `'static` lifetime.
    const MY_CONST: &str = "hello";
    MY_CONST
}

// You might also use `Box::leak`.

fn main() {
    let s = return_string();
    assert!(s.is_empty());

    let mut v = Vec::default();
    use_mut_ref(&mut v);
    println!("{v:?}");
    assert_eq!(v, vec![0; 3]);

    let m = &mut v;
    println!("{:?}", use_mut_ref2(m));
    assert_eq!(v, vec![0; 3]);

    assert_eq!(use_literal(), "hello");
    assert_eq!(use_static(), "hello");
    assert_eq!(use_const(), "hello");
}

You can also use std::borrow::Cow to generalize over owned data and unowned references. See the COW chapter.

  • Closures.
  • Rust Patterns.