A Simple Function

Learn to write a simple function in Rust, test it with unit tests, and handle command-line arguments.

What Makes a Function in Rust

Rust programs start executing in the main function, but the moment you want to reuse logic or organize your thoughts, you reach for your own functions. A function is a named block of code that takes inputs, performs work, and optionally returns a result. In Rust the syntax is compact but carries a few rules that keep memory safety guarantees intact even inside functions.

Every function begins with the fn keyword, followed by the name, a parenthesized list of parameters with explicit types, and an optional return type signalled by ->. The body is a block that evaluates to the final expression — and that expression is the return value if no trailing semicolon is present.

Expression‑Based Returns:

In Rust, a block’s return value is the value of its last expression. Adding a semicolon turns that expression into a statement that evaluates to the unit type (), which can cause a type‑mismatch error if the function expects something else. This difference between statements and expressions is one of the first habits you'll form as a Rust developer.

A function does not stand alone — you test it, you feed it data, you build small programs around it. The rest of this page walks you through exactly that: creating a function, proving it works with unit tests, and turning it into a tiny command‑line tool.

Walkthrough – From Function to Tested CLI Tool

1

Step 1: Create a new Rust project

Open a terminal and create a fresh binary project with Cargo. This gives you a main.rs ready to edit.

cargo new simple_function
cd simple_function

Cargo generates a src/main.rs that prints “Hello, world!”. We'll replace that with our own function shortly.

2

Step 2: Write a simple function and call it from `main`

Replace src/main.rs with the following:

src/main.rs
fn is_divisible(num: i32, divisor: i32) -> bool {
    num % divisor == 0
}
fn main() {
    let result = is_divisible(10, 2);
    println!("10 is divisible by 2: {}", result);
}

The function is_divisible takes two signed 32‑bit integers and returns a boolean. It uses the remainder operator % — if the remainder is zero, the divisor divides the number evenly. Notice the lack of a semicolon after num % divisor == 0; that expression becomes the return value.

main stores the boolean and prints it with the println! macro. The ! is part of the macro name and must always be present when you call it.

3

Step 3: Run the program without arguments

Compile and run to see the function in action:

cargo run

You should see:

10 is divisible by 2: true

The function computes a value, and main displays it. Everything is wired together, but the input is hard‑coded. Next, you’ll make sure the logic is solid with tests.

4

Step 4: Write unit tests for the function

Add a test module at the bottom of src/main.rs. The #[cfg(test)] attribute tells the compiler to include this code only when running tests.

src/main.rs
#[cfg(test)]
mod tests {
    use super::is_divisible;
    #[test]
    fn divisor_divides_evenly() {
        assert!(is_divisible(10, 2));
    }
    #[test]
    fn divisor_does_not_divide_unevenly() {
        assert!(!is_divisible(10, 3));
    }
    #[test]
    #[should_panic]
    fn divisor_zero_causes_panic() {
        is_divisible(10, 0);  // division by zero panics at runtime
    }
}

The first two tests check the happy paths. The third one confirms that dividing by zero panics (Rust does not guard against it in debug builds for i32). The #[should_panic] attribute tells the test runner that a panic is the expected outcome — without it, the test would fail.

The line use super::is_divisible; brings the function from the parent module into scope, because the test module is a child of main.

5

Step 5: Execute the tests and read the output

Run all tests with:

cargo test

The output confirms that all three tests pass. You’ll see something like:

running 3 tests
test tests::divisor_zero_causes_panic ... ok
test tests::divisor_does_not_divide_unevenly ... ok
test tests::divisor_divides_evenly ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Tests Passed:

All three assertions succeeded, including the panic test. The function behaves exactly as intended for the cases you defined. When you later refactor or add features, running cargo test again gives you immediate feedback.

6

Step 6: Accept input from the command line

Now modify main to read two numbers from the arguments passed to the program. Rust’s standard library provides std::env::args() for this.

Update src/main.rs to the following — keep the is_divisible function and the test module unchanged:

src/main.rs
use std::env;
fn is_divisible(num: i32, divisor: i32) -> bool {
    num % divisor == 0
}
fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} <num> <divisor>", args[0]);
        return;
    }
    let num: i32 = args[1].parse().unwrap();
    let divisor: i32 = args[2].parse().unwrap();
    println!(
        "{} is divisible by {}: {}",
        num,
        divisor,
        is_divisible(num, divisor)
    );
}

The env::args() iterator yields the program name as the first element, followed by any arguments you supply. We collect them into a Vec<String> and check the length before indexing. If the user provides fewer than two arguments, we print a usage message and return early.

parse() converts a string slice into the target type; we use i32. The calls to unwrap() extract the value or panic if parsing fails.

Unchecked Accesses Will Panic:

Accessing args[1] when args.len() is 1 would make the program panic with an index out‑of‑bounds error. Always verify the length before indexing, or use .get() which returns an Option. The same applies to unwrap() on a failed parse — it panics. In a real application you’d replace unwrap() with proper error handling, but for a quick tour unwrap() keeps the code short.

7

Step 7: Run with command‑line arguments

Pass two numbers after -- so Cargo forwards them to your program:

cargo run -- 10 2

You should see:

10 is divisible by 2: true

Try an example where the divisor doesn’t divide evenly:

cargo run -- 10 3
10 is divisible by 3: false

If you omit arguments, the usage message appears. If you pass non‑numeric strings, the program panics because unwrap() fails — something to handle more gracefully in later chapters.

Functional CLI Tool:

You now have a tiny command‑line program that accepts user input, calls a function you wrote, and reports the result. The same function is covered by unit tests that you can run at any time to confirm nothing broke.