Intermediate

Functions in Rust

Learn how to define functions in Rust: parameters, return values, expression-based returns, recursion, scope, closures, and higher-order functions with Docker-ready examples

Functions are the building blocks of every Rust program—you have already met main, the special entry point. Beyond main, functions let you name a piece of behavior, give it typed inputs and a typed output, and reuse it anywhere. Rust takes function signatures seriously: every parameter must have an explicit type, and a non-() return type must be declared with ->. The compiler uses these annotations to guarantee correctness before your program ever runs.

What makes functions in Rust distinctive is that the language is expression-based. The body of a function is a block, and a block evaluates to its final expression. That means you usually return a value simply by writing it as the last line without a semicolon—no return keyword required. This blends the imperative style familiar from C with the value-oriented style of functional languages like OCaml and Haskell, both of which influenced Rust.

Rust is also multi-paradigm in its function support. Functions are first-class values: you can pass a function by name, store one in a variable, and accept one as a parameter. On top of that, closures—anonymous functions that capture their surrounding environment—make higher-order programming natural and ergonomic.

In this tutorial you will define functions with parameters and return values, use both implicit and explicit returns, write a recursive function, reason about variable scope, and finish with closures and higher-order functions.

Defining and Calling Functions

Functions are declared with the fn keyword. Parameters always have explicit types, and the return type follows ->. The key idea to internalize: the last expression in the body is the return value when it has no trailing semicolon.

Create a file named functions.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Functions in Rust: definitions, parameters, returns, recursion, scope

// A function with no parameters and no return value.
// The implicit return type is the unit type `()`.
fn greet() {
    println!("Welcome to Rust functions!");
}

// Parameters need explicit types. `-> i32` declares the return type.
fn add(a: i32, b: i32) -> i32 {
    // The final expression (no semicolon) is returned implicitly.
    a + b
}

// The `return` keyword allows early exit before the end of the body.
fn absolute(n: i32) -> i32 {
    if n < 0 {
        return -n;
    }
    n
}

// Recursion: a function that calls itself. Computes n! (factorial).
fn factorial(n: u64) -> u64 {
    if n <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}

// A constant is in scope across the whole module and must be typed.
const PLANET: &str = "Earth";

fn main() {
    greet();

    let sum = add(3, 4);
    println!("3 + 4 = {}", sum);

    println!("absolute(-5) = {}", absolute(-5));
    println!("absolute(8) = {}", absolute(8));

    println!("5! = {}", factorial(5));

    // `sum` and `local` are local to main; PLANET is in scope everywhere.
    let local = sum * 2;
    println!("local = {}, planet = {}", local, PLANET);
}

A few things worth noticing:

  • Order does not matter. main calls factorial, which is defined above it, but Rust does not care about definition order—functions are visible throughout the module.
  • Expression vs. statement. In add, a + b is an expression returned implicitly. In absolute, return -n; is an early-exit statement, while the trailing n is the implicit return for the non-negative case.
  • Scope. sum and local live only inside main. The const PLANET is declared at module level, so it is reachable from any function.

Closures and Higher-Order Functions

Functions in Rust are values. You can pass a function by name using a function-pointer type (fn(i32) -> i32), and you can write closures—anonymous functions that capture variables from the scope where they are defined. A function that takes or returns another function is called a higher-order function.

Create a file named closures.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Closures and higher-order functions in Rust

// A higher-order function: accepts another function via a function pointer.
fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
    f(f(x))
}

// A plain named function we can pass as a value.
fn double(x: i32) -> i32 {
    x * 2
}

// Returning a closure. `impl Fn` describes "some type that is callable".
// `move` transfers ownership of captured variables into the closure.
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

fn main() {
    // A closure is an anonymous function that captures its environment.
    // Here it captures `factor` from the surrounding scope.
    let factor = 10;
    let scale = |x: i32| x * factor;
    println!("scale(4) = {}", scale(4));

    // Pass a named function as a value.
    println!("apply_twice(double, 5) = {}", apply_twice(double, 5));

    // A non-capturing closure coerces to a function pointer.
    println!("apply_twice(|x| x + 1, 5) = {}", apply_twice(|x| x + 1, 5));

    // A returned closure that remembers the value it captured.
    let add_five = make_adder(5);
    println!("add_five(10) = {}", add_five(10));
}

Key ideas in this example:

  • Closures capture context. scale captures factor automatically; a plain fn cannot do that.
  • Functions are first-class. double is passed to apply_twice by name, just like any other value.
  • Coercion. A closure that captures nothing—like |x| x + 1—can be used wherever a fn pointer is expected.
  • Returning closures. make_adder returns impl Fn(i32) -> i32, and move moves the captured n into the returned closure so it stays valid after the function returns.

Running with Docker

You do not need Rust installed locally—the official image compiles and runs both files. Each .rs file is compiled with rustc into a binary named after the file, then executed.

1
2
3
4
5
6
7
8
# Pull the official image
docker pull rust:1.83

# Compile and run the functions example
docker run --rm -v $(pwd):/app -w /app rust:1.83 sh -c 'rustc functions.rs && ./functions'

# Compile and run the closures example
docker run --rm -v $(pwd):/app -w /app rust:1.83 sh -c 'rustc closures.rs && ./closures'

Expected Output

Running functions.rs:

Welcome to Rust functions!
3 + 4 = 7
absolute(-5) = 5
absolute(8) = 8
5! = 120
local = 14, planet = Earth

Running closures.rs:

scale(4) = 40
apply_twice(double, 5) = 20
apply_twice(|x| x + 1, 5) = 7
add_five(10) = 15

Key Concepts

  • fn declares a function, and every parameter must have an explicit type—Rust never infers parameter types.
  • Return types use ->. A function with no -> returns the unit type ().
  • The last expression is the return value. Omit the semicolon to return it implicitly; use return only for early exits.
  • Semicolons change meaning. a + b returns a value; a + b; discards it and evaluates to ()—a common beginner pitfall.
  • Scope is block-based. Variables bound with let are local to their block; const items are visible across the module.
  • Recursion is fully supported, as shown by factorial, though iterators are often idiomatic for collection processing.
  • Functions are first-class values that can be passed by name using fn pointer types.
  • Closures capture their environment, enabling higher-order programming; use move to transfer ownership of captured values into the closure.

Running Today

All examples can be run using Docker:

docker pull rust:1.83
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining