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.
fn greet() {
println!("Hello from a function!");
}
fn main() {
greet();
}Hello from a function!
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.
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
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.
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
() (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.
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.
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.
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.
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(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.
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
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.
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.
/// 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)— notarea(radius)orAreaCalcsend_notification(user, msg)— notnotification(user, msg)parse_config(path)— notconfig(path)orget_config(path)is_valid_email(addr)— predicates start withis_,has_, orcan_find_user_by_id(id)— finders start withfind_orget_assert_non_empty(list)— assertion helpers start withassert_
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
/// 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