Back to Home

Functional Rust: immutability and iterators

The article explains the advantages of functional style in Rust: immutability instead of mutations, declarative iterators instead of loops, explicit error handling with Result and Option. Code examples and recommendations for middle/senior developers are provided.

Clean code in Rust: functional approach without mutations
Advertisement 728x90

Functional Style in Rust: Immutability, Declarative Code, and Error Handling

Functional programming in Rust addresses common issues with imperative and object-oriented styles: mutability, imperative loops, and implicit error handling. Instead of mutating state, code is built on immutable transformations, declarative operations, and explicit types like Result and Option. This makes programs predictable and less error-prone.

Rust is immutable by default, encouraging developers to rethink familiar patterns. The functional style uses iterators, function composition, and pattern matching for clean, side-effect-free code.

Immutability Over Mutation

Mutable variables create hidden dependencies: values change unexpectedly, complicating debugging. In imperative code, a mut x variable can be altered at any point between declaration and use.

Google AdInline article slot
fn my_function(num: i32) -> i32 {
    let mut x = num;
    // ...30 more lines...
    x += 96;
    // ...50 more lines...
    x
}

Here, the original value is corrupted, and the result is unpredictable due to potential intermediate changes. Immutability solves this: new values are created instead of modifying old ones.

let x = 10;
let new_x = process(x); // x remains unchanged

In Rust, immutability is the norm. Mutation requires explicit mut, but the functional style minimizes its use:

  • Check if a problem can be solved immutably using iterators or map.
  • If mutation is unavoidable, isolate it in the smallest possible block.
  • Prefer clone or borrowing for data propagation.

This reduces race condition risks and simplifies concurrency.

Google AdInline article slot

Declarative vs. Imperative Code

Imperative code describes execution steps: loops, conditions, mutations. Functional code describes the desired outcome. To get squares of numbers from 0 to 9, the imperative version requires a vector and push:

let mut squares = Vec::new();
for num in 0..10 {
    squares.push(num * num);
}

The declarative approach uses an iterator chain:

let squares: Vec<i32> = (0..10)
    .map(|x| x * x)
    .collect();

Advantages of the declarative style:

Google AdInline article slot
  • Less code: iterator chains replace loops.
  • Immutability: no collection mutations.
  • Composability: functions combine easily.
  • Concurrency: .par_iter() enables multithreading without locks.

Iterators in Rust are lazy: map doesn't execute until collect or for_each, optimizing memory.

Explicit Error Handling

Exceptions in other languages hide errors: try-catch allows ignoring types and context. In Rust, Result<T, E> and ? require explicit handling or propagation.

Bad example with unwrap:

old_toaster().unwrap(); // Panics on error

Correct approach uses match or map_err:

let toast = match old_toaster() {
    Ok(t) => t,
    Err(e) => {
        eprintln!("Error: {}", e);
        std::process::exit(1);
    }
};

Tips for handling:

  • match for full control.
  • map_err for error transformation.
  • ? only if the caller handles it.
  • Libraries like anyhow or thiserror for convenience.

This prevents panics in production and forces error consideration at compile time.

Eliminating Implicit Emptiness

In imperative languages, null lurks everywhere, causing NullPointerException. Rust uses Option<T>: Some(value) or None explicitly.

let maybe_candies: Option<Vec<Candy>> = get_candies();
let candies = match maybe_candies {
    Some(c) => c,
    None => Vec::new(),
};

Pattern matching ensures both cases are handled. The compiler won't overlook None.

Key Takeaways

  • Immutability by default reduces bugs from state changes.
  • Iterators and map replace loops, making code declarative.
  • Result and Option enforce explicit error and emptiness handling.
  • Function composition simplifies testing and refactoring.
  • The functional style suits 80% of tasks in Rust; mutations are only for IO or performance hotspots.

— Editorial Team

Advertisement 728x90

Read Next