RustFunctions

Functions in Rust

Functions are the primary building block of any Rust program. The language was designed with functions front and centre: every program starts in main, every reusable piece of logic lives in a named function, and the rules around functions are deliberately simple and consistent.

Defining a Function

Use the fn keyword, followed by the function name, a parenthesised parameter list, an optional return-type annotation, and a block body. Rust uses snake_case for all function names — this is enforced by the compiler with a warning if you deviate.

RUST
fn greet() {
    println!("Hello from a function!");
}

fn main() {
    greet();
}
Hello from a function!
Naming convention
Function names in Rust are always snake_case: calculate_area, send_email, parse_config. The compiler emits a warning for names like calculateArea or ParseConfig.
Parameters

Every parameter must have an explicit type annotation — Rust never infers parameter types from call sites. This is intentional: function signatures serve as contracts, and those contracts must be unambiguous without looking at any call site.

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

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

fn main() {
    let sum = add(10, 25);
    println!("10 + 25 = {}", sum);

    describe_person("Alice", 30);
}
10 + 25 = 35
Alice is 30 years old
Why explicit types on parameters?
Rust's type inference is powerful inside function bodies, but it stops at function boundaries. Every public function signature is an API surface — forcing explicit types means the contract is always readable without running a type-checker.
Return Values

Declare a return type with -> after the parameter list. The last expression in the function body (without a trailing semicolon) is automatically returned — this is called an implicit return and is idiomatic Rust.

RUST
fn square(n: i32) -> i32 {
    n * n   // no semicolon — this expression is the return value
}

fn circle_area(radius: f64) -> f64 {
    std::f64::consts::PI * radius * radius
}

fn main() {
    println!("5 squared = {}", square(5));
    println!("Area of circle r=3: {:.2}", circle_area(3.0));
}
5 squared = 25
Area of circle r=3: 28.27
Semicolons matter for returns
Adding a semicolon to the last line turns the expression into a statement, causing the function to return () (unit) instead of the value. If you see a type mismatch error on the last line of a function, a stray semicolon is often the culprit.
The return Keyword and Early Returns

The return keyword exists for early returns — exiting a function before reaching the last expression. Using return at the very end of a function works but is considered non-idiomatic when an implicit return suffices.

RUST
fn safe_divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        return 0.0; // early return to avoid division by zero
    }
    a / b // implicit return for the normal path
}

fn classify_temperature(celsius: f64) -> &'static str {
    if celsius < 0.0 {
        return "freezing";
    }
    if celsius < 15.0 {
        return "cold";
    }
    if celsius < 25.0 {
        return "comfortable";
    }
    "hot"
}

fn main() {
    println!("{}", safe_divide(10.0, 2.0));
    println!("{}", safe_divide(5.0, 0.0));
    println!("{}", classify_temperature(-5.0));
    println!("{}", classify_temperature(22.0));
    println!("{}", classify_temperature(35.0));
}
5
0
freezing
comfortable
hot
The Unit Type ()

When a function has no -> return type annotation, it implicitly returns () (pronounced "unit"). The unit type is Rust's equivalent of void in C — it carries no information. You will rarely write () explicitly, but knowing it exists helps when reading error messages.

RUST
fn log_message(msg: &str) {
    println!("[LOG] {}", msg);
    // implicitly returns ()
}

// These two signatures are identical:
fn say_hi_a() { println!("hi"); }
fn say_hi_b() -> () { println!("hi"); }

fn main() {
    let result = log_message("server started");
    println!("log returned: {:?}", result);
}
[LOG] server started
log returned: ()
Returning Multiple Values with Tuples

Rust functions can only return a single value, but tuples let you bundle multiple values together. Destructuring the tuple at the call site is clean and idiomatic.

RUST
fn min_max(numbers: &[i32]) -> (i32, i32) {
    let mut min = numbers[0];
    let mut max = numbers[0];
    for &n in numbers.iter() {
        if n < min { min = n; }
        if n > max { max = n; }
    }
    (min, max)
}

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

fn main() {
    let data = [3, 1, 4, 1, 5, 9, 2, 6];
    let (lo, hi) = min_max(&data);
    println!("min={} max={}", lo, hi);

    let (x, y) = (10, 20);
    let (x, y) = swap(x, y);
    println!("after swap: x={} y={}", x, y);
}
min=1 max=9
after swap: x=20 y=10
Functions as Values — Higher-Order Functions

In Rust, functions are first-class values. You can pass a function as an argument using a function pointer type written as fn(ParamType) -> ReturnType. This enables higher-order functions — functions that operate on other functions.

RUST
fn double(x: i32) -> i32 { x * 2 }
fn triple(x: i32) -> i32 { x * 3 }
fn square(x: i32) -> i32 { x * x }

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

fn apply_to_list(f: fn(i32) -> i32, numbers: &[i32]) -> Vec<i32> {
    numbers.iter().map(|&n| f(n)).collect()
}

fn main() {
    println!("apply double to 5: {}", apply(double, 5));
    println!("apply triple to 5: {}", apply(triple, 5));
    println!("apply square to 5: {}", apply(square, 5));

    let nums = [1, 2, 3, 4, 5];
    let doubled = apply_to_list(double, &nums);
    println!("doubled list: {:?}", doubled);
}
apply double to 5: 10
apply triple to 5: 15
apply square to 5: 25
doubled list: [2, 4, 6, 8, 10]
fn vs Fn
fn(i32) -> i32 is a plain function pointer — it only works with named functions and non-capturing closures. For closures that capture their environment, use the Fn, FnMut, or FnOnce trait bounds instead.
Nested Functions

Rust allows you to define functions inside other functions. Nested functions are scoped to their enclosing function — they are invisible outside it. This is useful for helper logic that is only meaningful in one specific context.

RUST
fn process_data(data: &[i32]) -> f64 {
    fn sum(numbers: &[i32]) -> i32 {
        numbers.iter().sum()
    }

    fn count(numbers: &[i32]) -> usize {
        numbers.len()
    }

    let total = sum(data);
    let n = count(data);
    total as f64 / n as f64
}

fn main() {
    let readings = [10, 20, 30, 40, 50];
    let average = process_data(&readings);
    println!("Average: {}", average);
}
Average: 30
Nested functions do not capture
Unlike closures, nested functions cannot access variables from the enclosing scope. They are full standalone functions that happen to be scoped locally. If you need to capture surrounding variables, use a closure instead.
Diverging Functions — !

Some functions never return — they either loop forever, terminate the process, or always panic. These are called diverging functions and their return type is written as ! (pronounced "never"). The ! type is special: it can coerce to any other type, which lets diverging expressions appear in branches where a value is expected.

RUST
fn fatal_error(message: &str) -> ! {
    println!("FATAL: {}", message);
    std::process::exit(1);
}

fn parse_or_die(s: &str) -> i32 {
    // The else branch diverges (-> !), so the whole if/else has type i32
    if let Ok(n) = s.parse::<i32>() {
        n
    } else {
        fatal_error("could not parse integer")
    }
}

fn main() {
    let n = parse_or_die("42");
    println!("Parsed: {}", n);
}
Parsed: 42

Return type

Meaning

Common example

-> i32

Returns an i32 value

fn add(a: i32, b: i32) -> i32

(no annotation)

Returns () — unit, like void

fn log(msg: &str)

-> (i32, f64)

Returns a tuple

fn stats() -> (i32, f64)

-> !

Never returns — diverges

fn crash(msg: &str) -> !

Documentation Comments

Rust uses /// (triple-slash) for documentation comments above functions. These comments are processed by rustdoc and support Markdown formatting. The standard library is entirely documented this way, and running cargo doc --open generates beautiful HTML docs for your own crate.

RUST
/// Computes the nth Fibonacci number iteratively.
///
/// # Arguments
///
/// * `n` - The position in the sequence (0-indexed)
///
/// # Examples
///
/// ```
/// assert_eq!(fibonacci(0), 0);
/// assert_eq!(fibonacci(7), 13);
/// ```
fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut a = 0u64;
            let mut b = 1u64;
            for _ in 2..=n {
                let next = a + b;
                a = b;
                b = next;
            }
            b
        }
    }
}

fn main() {
    for i in 0..10 {
        print!("{} ", fibonacci(i));
    }
    println!();
}
0 1 1 2 3 5 8 13 21 34
Good Naming — Verb Phrases

Good function names describe what the function does. Because functions perform actions, a verb phrase is almost always the right choice.

  • calculate_area(radius) — not area(radius) or AreaCalc

  • send_notification(user, msg) — not notification(user, msg)

  • parse_config(path) — not config(path) or get_config(path)

  • is_valid_email(addr) — predicates start with is_, has_, or can_

  • find_user_by_id(id) — finders start with find_ or get_

  • assert_non_empty(list) — assertion helpers start with assert_

Putting It All Together

Here is a realistic program that combines explicit parameter types, early returns, tuple returns, a higher-order function, and doc comments.

Temperature converter

RUST
/// Converts Celsius to Fahrenheit and Kelvin.
/// Returns a tuple (fahrenheit, kelvin).
fn celsius_to_all(c: f64) -> (f64, f64) {
    let f = c * 9.0 / 5.0 + 32.0;
    let k = c + 273.15;
    (f, k)
}

fn format_reading(label: &str, value: f64, unit: &str) -> String {
    format!("{}: {:.2} {}", label, value, unit)
}

fn print_conversion(convert: fn(f64) -> (f64, f64), celsius: f64) {
    let (f, k) = convert(celsius);
    println!("{}", format_reading("Celsius   ", celsius, "C"));
    println!("{}", format_reading("Fahrenheit", f, "F"));
    println!("{}", format_reading("Kelvin    ", k, "K"));
    println!();
}

fn main() {
    print_conversion(celsius_to_all, 0.0);
    print_conversion(celsius_to_all, 100.0);
    print_conversion(celsius_to_all, -40.0);
}
Celsius   : 0.00 C
Fahrenheit: 32.00 F
Kelvin    : 273.15 K

Celsius   : 100.00 C
Fahrenheit: 212.00 F
Kelvin    : 373.15 K

Celsius   : -40.00 C
Fahrenheit: -40.00 F
Kelvin    : 233.15 K
Success
Functions in Rust are predictable and explicit: every parameter is typed, every return is either declared or unit, and the last expression without a semicolon is always the return value. This consistency means you can read any function signature and immediately understand its contract — no hidden state, no surprises.