Functions in Rust are declared with the fn keyword and form the backbone of every Rust program. What distinguishes Rust's approach is how deeply integrated functions are with the ownership and borrowing system. Arguments can transfer ownership, borrow immutably, or borrow mutably — and the types make this explicit.
Defining a Function
Every function in Rust is introduced with the fn keyword:
fn my_function() {
// function body
}
The function name should use snake_case by convention. The body is enclosed in curly braces.
The `main` Function
The entry point of every Rust binary is the main function:
fn main() {
my_function();
}
When main returns, the program exits. The function must exist in binary crates and has no arguments or return value by default (though it can return Result in modern Rust).
Calling Functions
Functions are called by name followed by parentheses:
fn greet() {
println!("Hello from a function!");
}
fn main() {
greet(); // call the function
}
In Rust, function definitions do not need to appear before their callers in the same file. The compiler processes the whole module before resolving names.
Function Arguments — Passing by Value
Arguments specify what data the function requires. Each argument must have an explicit type:
fn print_value(x: i32) {
println!("value = {}", x);
}
fn main() {
print_value(33);
// value = 33
}
When you pass a primitive type like i32 by value, the value is copied into the function. The original variable is unchanged:
fn double(x: i32) -> i32 {
x * 2
}
let a = 5;
let b = double(a);
println!("a = {}, b = {}", a, b); // a = 5, b = 10 (a is unchanged)
For non-Copy types like String or Vec<T>, passing by value moves ownership into the function, and the original variable can no longer be used after the call.
Function Arguments — Passing by Reference
To allow a function to read or modify a value without taking ownership, pass a reference:
// Immutable reference — borrow, cannot modify
fn print_string(s: &String) {
println!("{}", s);
}
// Mutable reference — borrow, can modify
fn increase(x: &mut i32) {
*x += 1; // dereference to modify the value behind the reference
}
fn main() {
let s = String::from("hello");
print_string(&s); // borrows s, s is still valid after
println!("{}", s); // hello
let mut z = 1;
increase(&mut z); // passes a mutable reference
println!("z = {}", z); // z = 2
}
Passing semantics:
+------------------------+-----------------------------+
| Syntax | Behavior |
+------------------------+-----------------------------+
| fn f(x: T) | Move or copy ownership |
| fn f(x: &T) | Immutable borrow |
| fn f(x: &mut T) | Mutable borrow |
+------------------------+-----------------------------+
Return Values
Functions return values using -> in the signature. The last expression in the function body (without a semicolon) is implicitly returned:
fn product(x: i32, y: i32) -> i32 {
x * y // no semicolon = implicit return
}
fn main() {
let a = 3;
let b = 5;
let p = product(a, b);
println!("{} * {} = {}", a, b, p); // 3 * 5 = 15
}
You can also use return for early returns:
fn absolute(x: i32) -> i32 {
if x < 0 {
return -x; // early return
}
x // implicit return for the common path
}
Methods
A method is a function associated with a type. In Rust, methods are defined inside impl (implementation) blocks attached to structs or enums:
struct Point {
x: f64,
y: f64,
}
struct Line {
start: Point,
end: Point,
}
impl Line {
// &self = immutable borrow of the Line instance
fn len(&self) -> f64 {
let dx = self.start.x - self.end.x;
let dy = self.start.y - self.end.y;
(dx * dx + dy * dy).sqrt()
}
}
fn methods() {
let p = Point { x: 3.0, y: 4.0 };
let p2 = Point { x: 5.0, y: 10.0 };
let myline = Line { start: p, end: p2 };
println!("length = {}", myline.len());
// length = 6.324...
}
The first parameter of a method is self (or a reference to it):
| Parameter | Meaning |
|---|---|
self | Takes ownership of the instance |
&self | Immutable borrow of the instance |
&mut self | Mutable borrow of the instance |
Associated Functions
Methods that do not take self are called associated functions (similar to static methods in other languages). They are called with :: syntax:
impl Point {
// Associated function — no self parameter
fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
}
let p = Point::origin(); // call with :: not .
String::from("hello") and Vec::new() are examples of associated functions in the standard library.
Closures
A closure is an anonymous function that can capture variables from its surrounding environment. Closures are defined inline and can be stored in variables, passed to functions, or returned from functions.
Storing a Function in a Variable
You can store a named function in a variable:
fn say_hello() { println!("hello"); }
fn main() {
let sh = say_hello; // store function pointer in a variable
sh(); // call it through the variable
// hello
}
Closure Syntax
fn closures() {
// Basic closure: |params| -> return_type { body }
let plus_one = |x: i32| -> i32 { x + 1 };
let a = 6;
println!("{} + 1 = {}", a, plus_one(a)); // 6 + 1 = 7
// Closure that captures from environment
let two = 2;
let plus_two = |x| x + two; // captures `two` from the environment
println!("{} + 2 = {}", 3, plus_two(3)); // 3 + 2 = 5
}
The types of closure parameters can often be inferred:
let add = |x, y| x + y; // both inferred as i32 from usage
let result = add(3, 4); // 7
Closures Capturing by Reference
let s = String::from("hello");
let print_s = || println!("{}", s); // captures &s (immutable borrow)
print_s(); // hello
println!("{}", s); // s is still accessible
Closures Capturing Mutable State
When a closure mutates a captured variable, it must be declared mut and requires an exclusive borrow:
fn closures_mutable() {
let mut two = 2;
{
let mut plus_two = |x| {
let mut z = x;
z += two;
z
};
println!("{} + 2 = {}", 3, plus_two(3)); // 3 + 2 = 5
} // plus_two is dropped here, releasing the borrow on `two`
let borrow_two = &mut two; // now allowed
}
Using Closures with Iterators
Closures are most commonly used with iterator methods, which is idiomatic Rust:
let numbers = vec![1, 2, 3, 4, 5];
// map: transform each element
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// filter: keep only elements matching a predicate
let evens: Vec<&i32> = numbers.iter().filter(|&&x| x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4]
// fold (reduce): accumulate a result
let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x);
println!("sum = {}", sum); // 15
Function Types and Higher-Order Functions
Functions and closures can be passed as arguments and returned from other functions:
fn apply<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
fn main() {
let double = |x| x * 2;
let triple = |x| x * 3;
println!("{}", apply(double, 5)); // 10
println!("{}", apply(triple, 5)); // 15
}
The Fn trait (and its variants FnMut and FnOnce) are how Rust types closures:
| Trait | When Used | Can Be Called |
|---|---|---|
Fn | Borrows captures immutably | Multiple times |
FnMut | Borrows captures mutably | Multiple times |
FnOnce | Consumes captures (moves) | Once only |
Complete Example
struct Point { x: f64, y: f64 }
struct Line { start: Point, end: Point }
impl Line {
fn len(&self) -> f64 {
let dx = self.start.x - self.end.x;
let dy = self.start.y - self.end.y;
(dx * dx + dy * dy).sqrt()
}
}
fn product(x: i32, y: i32) -> i32 { x * y }
fn increase(x: &mut i32) { *x += 1; }
fn main() {
// Basic function call
println!("3 * 5 = {}", product(3, 5));
// Mutable reference argument
let mut z = 10;
increase(&mut z);
println!("z after increase = {}", z); // 11
// Method on struct
let line = Line {
start: Point { x: 0.0, y: 0.0 },
end: Point { x: 3.0, y: 4.0 },
};
println!("line length = {}", line.len()); // 5.0
// Closure
let plus_one = |x: i32| x + 1;
println!("6 + 1 = {}", plus_one(6)); // 7
// Closure with iterator
let nums = vec![1, 2, 3, 4, 5];
let sum: i32 = nums.iter().sum();
println!("sum = {}", sum); // 15
}
Conclusion
Rust's function system integrates seamlessly with the ownership model. Passing by value transfers or copies ownership. Passing by reference borrows without transferring ownership. Methods encapsulate behavior on types with clear borrowing semantics. Closures bring functional programming patterns to systems-level code, and when combined with iterators, produce expressive, safe, and highly performant code.
Understanding how functions interact with ownership — copies, moves, and borrows — is perhaps the single most important skill to develop as a Rust programmer.