Functional Programming, A Rust Perspective


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
, andchain
.Consumers: Consume iterators and return a final value, such as
collect
,sum
, andfold
.
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 alsoFnOnce
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 alsoFnMut
andFnOnce
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 ofFnMut
FnMut
is a subtrait ofFnOnce
This means:
A function that implements
Fn
can be used whereFnMut
orFnOnce
is requiredA function that implements
FnMut
can be used whereFnOnce
is requiredBut 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.
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! ๐