ZetCode

Rust functions

last modified October 25, 2026

In this article we show how to define and use functions in Rust.

A function is a named block of code that can be called from other parts of the program. Functions split a program into small, reusable pieces and give names to its operations.

A function is defined with the fn keyword, followed by its name, a list of parameters in parentheses, an optional return type after ->, and a body in curly braces. By convention, function names use snake_case. Every Rust program starts in the main function. The types of the parameters and of the return value must always be written out; they are not inferred.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

A simple function

The first example defines a function without parameters and without a return value.

main.rs
fn main() {
    print_msg();
    print_msg();
}

fn print_msg() {
    println!("Hello there!");
}

In the program, we define two functions. The main function calls the print_msg function twice.

fn print_msg() {
    println!("Hello there!");
}

The print_msg function prints a message to the console. A function can be defined before or after the place where it is called.

$ cargo run -q
Hello there!
Hello there!

Function parameters

Parameters are listed in the parentheses after the function name; each one has a name and a type.

main.rs
fn main() {
    let name = String::from("John Doe");
    let age = 34;

    print_info(&name, age);
    print_info("Roger Roe", 47);

    println!("{name} is still available");
}

fn print_info(name: &str, age: u32) {
    println!("{name} is {age} years old");
}

The print_info function takes a name and an age.

fn print_info(name: &str, age: u32) {

The name is a string slice, &str. The function only reads the text, so it borrows it instead of taking ownership. Such a parameter accepts string literals as well as borrowed String values.

print_info(&name, age);
print_info("Roger Roe", 47);

We pass a reference to a String and a string literal. Since name was only borrowed, it can still be used after the call. Had the parameter been of type String, the value would be moved into the function. See Rust ownership for details.

$ cargo run -q
John Doe is 34 years old
Roger Roe is 47 years old
John Doe is still available

Return values

The return type is written after an arrow. The value of the last expression in the function body is the return value of the function.

main.rs
fn main() {
    let res = square(7);
    println!("{res}");

    let msg = build_msg("John Doe", 34);
    println!("{msg}");
}

fn square(x: i32) -> i32 {
    x * x
}

fn build_msg(name: &str, age: u32) -> String {
    format!("{name} is {age} years old")
}

The program defines two functions that return a value.

fn square(x: i32) -> i32 {
    x * x
}

The square function returns an i32. The expression x * x has no semicolon; it is the tail expression of the body, and its value is returned. Adding a semicolon would turn it into a statement, and the compiler would report a type mismatch, because the body would then evaluate to the unit type ().

fn build_msg(name: &str, age: u32) -> String {
    format!("{name} is {age} years old")
}

The build_msg function returns a new String created by the format! macro.

$ cargo run -q
49
John Doe is 34 years old

Early return

The return keyword ends the function immediately with the given value. It is used to leave a function before its end.

main.rs
fn main() {
    println!("{}", classify(-4));
    println!("{}", classify(0));
    println!("{}", classify(13));
}

fn classify(n: i32) -> &'static str {
    if n < 0 {
        return "negative";
    }

    if n == 0 {
        return "zero";
    }

    "positive"
}

The classify function returns a string slice describing the number.

if n < 0 {
    return "negative";
}

For negative numbers, the function returns right away and the rest of the body is skipped.

"positive"

The last line is the tail expression; it is used when none of the early returns was taken. The &'static str return type means the returned text lives for the whole run of the program, which is true for string literals.

$ cargo run -q
negative
zero
positive

Returning multiple values

A function returns a single value, but that value can be a tuple. This is the usual way to return several values.

main.rs
fn main() {
    let vals = [4, -2, 9, 0, 7];

    let (min, max) = min_max(&vals);
    println!("min: {min}, max: {max}");
}

fn min_max(vals: &[i32]) -> (i32, i32) {
    let mut min = vals[0];
    let mut max = vals[0];

    for &v in vals {
        if v < min {
            min = v;
        }
        if v > max {
            max = v;
        }
    }

    (min, max)
}

The min_max function finds the smallest and the largest value of a slice.

fn min_max(vals: &[i32]) -> (i32, i32) {

The function takes a slice of integers and returns a pair of integers.

let (min, max) = min_max(&vals);

The returned tuple is destructured into two variables.

$ cargo run -q
min: -2, max: 9

Mutable reference parameters

To let a function modify a value of the caller, we pass a mutable reference.

main.rs
fn main() {
    let mut words = vec![String::from("sky")];

    add_word(&mut words, "cup");
    add_word(&mut words, "forest");

    println!("{words:?}");
}

fn add_word(words: &mut Vec<String>, word: &str) {
    words.push(word.to_string());
}

The add_word function appends a word to a vector.

fn add_word(words: &mut Vec<String>, word: &str) {
    words.push(word.to_string());
}

The &mut Vec<String> parameter is a mutable borrow of the vector. The function can change it, while the caller keeps the ownership.

add_word(&mut words, "cup");

The caller must write &mut explicitly, so it is visible at the call site that the function may change the vector. The variable itself must be declared with mut.

$ cargo run -q
["sky", "cup", "forest"]

Recursion

A recursive function calls itself. The computation stops at a base case that does not call the function again.

main.rs
fn main() {
    for n in [0, 1, 5, 10, 20] {
        println!("{n}! = {}", factorial(n));
    }
}

fn factorial(n: u64) -> u64 {
    if n == 0 { 1 } else { n * factorial(n - 1) }
}

The program computes the factorial of several numbers.

fn factorial(n: u64) -> u64 {
    if n == 0 { 1 } else { n * factorial(n - 1) }
}

The factorial of 0 is 1; this is the base case. For other values, the function multiplies n by the factorial of n - 1. The whole body is a single if expression whose value is returned. The factorial of 20 is the largest one that fits into a u64.

$ cargo run -q
0! = 1
1! = 1
5! = 120
10! = 3628800
20! = 2432902008176640000

Functions as values

Functions can be stored in variables and passed to other functions. The type of a function pointer is written as fn(i32) -> i32.

main.rs
fn main() {
    println!("{}", apply(double, 5));
    println!("{}", apply(negate, 5));

    let ops = [double, negate, square];

    for f in ops {
        print!("{} ", f(10));
    }
    println!();
}

fn double(x: i32) -> i32 {
    x * 2
}

fn negate(x: i32) -> i32 {
    -x
}

fn square(x: i32) -> i32 {
    x * x
}

fn apply(f: fn(i32) -> i32, val: i32) -> i32 {
    f(val)
}

We define three simple functions and a function that applies another function to a value.

fn apply(f: fn(i32) -> i32, val: i32) -> i32 {
    f(val)
}

The apply function takes a function pointer and calls it with the given value.

let ops = [double, negate, square];

for f in ops {
    print!("{} ", f(10));
}

The three functions have the same signature, so they can be stored in an array. We loop over the array and call each function.

$ cargo run -q
10
-5
20 -10 100 

Closures

A closure is an anonymous function that can capture variables from its environment. Its parameters are written between vertical bars, and their types are usually inferred.

main.rs
fn main() {
    let num = 3;
    let add_num = |x| x + num;
    println!("{}", add_num(5));

    let mut counter = 0;
    let mut inc = || counter += 1;
    inc();
    inc();
    println!("counter: {counter}");

    let words = ["sky", "forest", "cup", "water"];
    let min_len = 4;
    let long: Vec<_> = words.iter().filter(|w| w.len() >= min_len).collect();
    println!("{long:?}");

    println!("{}", apply_twice(|x| x * 3, 2));
}

fn apply_twice(f: impl Fn(i32) -> i32, val: i32) -> i32 {
    f(f(val))
}

The program creates several closures.

let num = 3;
let add_num = |x| x + num;

The closure captures the num variable from the enclosing function. A regular fn cannot do that.

let mut counter = 0;
let mut inc = || counter += 1;

This closure modifies a captured variable, so it borrows counter mutably; the closure variable must be declared with mut. After the last call of inc, the borrow ends and we can print the counter.

let long: Vec<_> = words.iter().filter(|w| w.len() >= min_len).collect();

Closures are often passed to iterator methods. Here the closure uses the captured min_len value to filter the words.

fn apply_twice(f: impl Fn(i32) -> i32, val: i32) -> i32 {
    f(f(val))
}

A function that accepts closures uses one of the Fn, FnMut, or FnOnce traits. The impl Fn(i32) -> i32 parameter accepts any closure or function with this signature.

$ cargo run -q
8
counter: 2
["forest", "water"]
18

Source

The Rust Programming Language: Functions

The Rust Programming Language: Closures

In this article we have worked with functions in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.