Functional Programming, A Rust Perspective

Claude OmosaClaude Omosa
6 min read

Functional programming is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state or mutable data. Rust, although primarily an imperative language, it is multi-paradigm offering strong support for functional programming concepts. This blog explores these concepts from a Rust perspective.

First-Class Functions in Rust

In Rust, functions are first-class citizens, meaning they can be assigned to variables, passed as arguments, and returned from other functions. This allows for a high degree of flexibility in how functions are used.

fn add_one(x: i32) -> i32 {
    x + 1
}

fn apply_function(f: fn(i32) -> i32, value: i32) -> i32 {
    f(value)
}

fn main() {
    let result = apply_function(add_one, 5);
    println!("{}", result); // Outputs 6
}

Closures in Rust

Closures are anonymous functions that can capture values from their surrounding environment. Unlike regular functions, closures can access variables in their scope.

fn main() {
    let multiplier = 2;
    let multiply = |x| x * multiplier; // Captures `multiplier` from its scope

    println!("{}", multiply(5)); // Outputs 10
}

Closures can be defined in different syntaxes:

let add = |x: i32, y: i32| -> i32 { x + y }; // Explicit return type

let subtract = |x, y| x - y; // Implicit return type

Higher-Order Functions in Rust

Higher-order functions take functions as arguments or return functions. This is a powerful feature that allows for more expressive and reusable code. Many traits in Rust, including the Iterator trait, provide higher-order functions. Below are some examples of higher-order functions specifically from the Iterator trait:

map, filter, and fold

map

Applies a function to each element of an iterator and returns a new iterator with the transformed values.

fn main() {
    let numbers = vec![1, 2, 3, 4];
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("{:?}", doubled); // Outputs [2, 4, 6, 8]
}

filter

Filters elements based on a predicate function.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let even_numbers: Vec<i32> = numbers.into_iter().filter(|x| x % 2 == 0).collect();
    println!("{:?}", even_numbers); // Outputs [2, 4]
}

fold

Reduces an iterator to a single value using an accumulator function.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let sum = numbers.iter().fold(0, |acc, x| acc + x);
    println!("{}", sum); // Outputs 15
}

Iterators in Rust

Iterators are a core concept in Rust, allowing for efficient data processing without requiring manual indexing. There are multiple ways to create iterators:

Creating Iterators

In Rust, there are three primary methods for creating an iterator from a collection, each with different ownership and borrowing semantics:

1. into_iter()

This method consumes the collection, transferring ownership to the iterator:

fn into_iter(self) -> IntoIter<T>
  • Ownership: Takes ownership of the collection (consumes it)

  • Iterates over: T (owned values)

  • Use case: When you no longer need the original collection

Example:

let v = vec![1, 2, 3];
for val in v.into_iter() {
    println!("{}", val);
}
// v is no longer usable here as it's been consumed

2. iter()

This method borrows the collection immutably:

fn iter(&self) -> Iter<T>
  • Ownership: Borrows the collection immutably

  • Iterates over: &T (references)

  • Use case: When you need to read values but keep the collection

Example:

let v = vec![1, 2, 3];
for val in v.iter() {
    println!("{}", val); // val is &i32 here
}
// v is still usable here

3. iter_mut()

This method borrows the collection mutably:

fn iter_mut(&mut self) -> IterMut<T>
  • Ownership: Borrows the collection mutably

  • Iterates over: &mut T (mutable references)

  • Use case: When you need to modify elements in place

Example:

let mut v = vec![1, 2, 3];
for val in v.iter_mut() {
    *val *= 2; // Multiply each value by 2
}
// v now contains [2, 4, 6]

The IntoIterator Trait

All these methods are related to the IntoIterator trait, which defines how a type can be converted into an iterator:

pub trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;

    fn into_iter(self) -> Self::IntoIter;
}

Collections typically implement IntoIterator in three ways:

  • For self (consuming the collection)

  • For &self (immutable reference)

  • For &mut self (mutable reference)

For Loop Syntax Sugar

When you use a for loop in Rust, it automatically calls into_iter() on the collection:

// These are equivalent:
for item in collection { /* ... */ }

for item in collection.into_iter() { /* ... */ }

This means that a for loop will:

  • Consume a collection if used directly

  • Borrow immutably if used with &collection

  • Borrow mutably if used with &mut collection

This consistency across collections is one of Rust's great strengths in its iteration model.

Iterator Adaptors vs. Consumers

  • Adaptors: Transform iterators into new iterators without consuming them. Examples include map, filter, and chain.

  • Consumers: Consume iterators and return a final value, such as collect, sum, and fold.

let numbers = vec![1, 2, 3];
let squared: Vec<i32> = numbers.iter().map(|x| x * x).collect();
println!("{:?}", squared); // Outputs [1, 4, 9]

Function Traits: Fn, FnMut, FnOnce

These three traits define different kinds of functions and closures in Rust, each with different capabilities regarding how they access their environment:

FnOnce

The closure takes ownership of the captured variables and can be called only once.

  • Consumes itself when called (takes ownership of self)

  • Can only be called once (hence the name)

  • Can move values out of its captured environment

  • All functions implement this trait (at minimum)

  • Use case: Functions that consume their captured variables

Example:

fn consume_and_return<F>(f: F) -> String
    where F: FnOnce() -> String
{
    f() // f is consumed here
}

let s = String::from("hello");
let closure = || s; // This closure moves s
let result = consume_and_return(closure);
// s is no longer available here

FnMut

Allows mutable access to captured variables.

  • Takes &mut self (mutable reference)

  • Can be called multiple times

  • Can mutate its captured environment

  • All FnMut functions are also FnOnce

  • Use case: Iterators like map, filter that might need to keep state

fn call_twice<F>(mut f: F)
    where F: FnMut()
{
    f(); // First call
    f(); // Second call
}

let mut counter = 0;
let mut increment = || {
    counter += 1;
    println!("Counter: {}", counter);
};

call_twice(increment);
// Output:
// Counter: 1
// Counter: 2

Fn

Only requires an immutable reference to captured variables, a function that can be called multiple times without mutating its environment.

  • Takes &self (immutable reference)

  • Can be called multiple times

  • Only borrows its captured values immutably

  • All Fn functions are also FnMut and FnOnce

  • Use case: Callbacks that need to be called repeatedly without modification

fn call_many_times<F>(f: F)
    where F: Fn()
{
    for _ in 0..5 {
        f();
    }
}

let greeting = String::from("Hello");
let say_hi = || println!("{}", greeting);

call_many_times(say_hi);
// greeting is still available here

Hierarchy and Trait Bounds

There's a hierarchy:

  • Fn is a subtrait of FnMut

  • FnMut is a subtrait of FnOnce

This means:

  • A function that implements Fn can be used where FnMut or FnOnce is required

  • A function that implements FnMut can be used where FnOnce is required

  • But not vice versa

When writing generic functions that take closures, you should use the least restrictive trait bound that meets your needs.

Conclusion

Rust provides robust functional programming features while maintaining its emphasis on safety and performance. Understanding first-class functions, closures, higher-order functions, and iterators allows developers to write expressive and efficient Rust code. By leveraging function traits like Fn, FnMut, and FnOnce, we can write flexible and safe functional constructs that make Rust an excellent choice for functional programming enthusiasts.

0
Subscribe to my newsletter

Read articles from Claude Omosa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Claude Omosa
Claude Omosa

I am still on my journey of being a better developer and at the same time acquiring the skills needed for the fields I'm interested in. In my blogs I will be sharing bits of what I learn in this journey, Join me! ๐Ÿ˜Š