RUST: Functions, Methods, and Closures in Rust

Functions are the primary units of code organization in Rust. From basic functions with value and reference parameters to methods on structs and closures stored in variables, Rust's function system is both expressive and safe, leveraging the ownership model at every level.

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:

rust
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:

rust
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:

rust
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:

rust
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:

rust
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:

rust
// 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
}

text
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:

rust
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:

rust
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:

rust
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):

ParameterMeaning
selfTakes ownership of the instance
&selfImmutable borrow of the instance
&mut selfMutable 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:

rust
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:

rust
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

rust
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:

rust
let add = |x, y| x + y;   // both inferred as i32 from usage
let result = add(3, 4);    // 7

Closures Capturing by Reference

rust
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:

rust
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:

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:

rust
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:

TraitWhen UsedCan Be Called
FnBorrows captures immutablyMultiple times
FnMutBorrows captures mutablyMultiple times
FnOnceConsumes captures (moves)Once only

Complete Example

rust
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.