13. Functional Language Features: Iterators and Closures

Learning objectives

  • Closures
  • Iterators
  • Improve our I/O program from last chapter
  • ‘Zero-cost’ abstractions

Functional Programming

-

R, at its heart, is a functional language - Hadley Wickham, Advanced R

Functional Programming

  • Emphasizes purity: Focuses on side-effect free, “true” functions.

  • Avoid mutability: Promote “assignment free” programming.

  • Encourages declarative style: Evaluating expressions over executing statements.

  • Treats functions as first-class citizens: Functions are values too.

Aside: Why purity and immutability matter

  • Easier to reason about programs.
  • Less concern about evaluation order when composing expressions.
  • Enables safe concurrency and parallelism.

Functional programming in Rust

  • Rust has extensive support for programming in a functional style:

    • Immutability by Default: Encourage pure functions and minimize side-effects.
    • Closures: Functions as values, supporting functional composition.
    • Declarative style:
      • Iterators with lazy evaluation for sequence processing
      • Higher-Order Functions (e.g., map, filter, fold)
      • Enums and pattern matching for sum types.
    • Expressive Error Handling: Types like Option, Result promote predictable, safe error handling.
  • This chapter focuses on the two we have yet to cover: Closures and Iterators.

Iterators and Closures: Why they matter

  • Closures : Function values that capture variables from the environment

  • Iterators : Abstractions for lazy processing of sequences

  • Core to functional programming:

    • Work together: Closures can used with iterators to enable concise, declarative code. (e.g. map, filter)
    • Express complex operations without explicit state.

Closures

Closures in general

Example in R:

example_closure <- function(x) {
    y <- 10
    function() {
        x + y
    }
}

closure_instance <- example_closure(5)
closure_instance()  # Returns 15
  • Closures are functions that can capture variables from surrounding environment.
  • One of the key tools for functional programming.
  • These variables are stored alongside the closure for later use.
  • In R, every regular function is a closure, there is no special syntax.

Rust closures

  • In Rust, closures can capture their environment as well.
  • Ordinary functions are not closures, and do not capture anything.
  • Only anonymous functions can be closures.
  • Closure (and function pointers) can be used as first-class citizens:
    • Stored in variables
    • Passed as arguments
    • Returned from other functions.

Closure syntax

fn   add_one_v1   (num : u32) -> u32 {num + 1}
let  add_one_v2 = |num : u32| -> u32 {num + 1};
let  add_one_v3 = |num|               num + 1 ;

println!("{}",add_one_v3(3)) // 4
  • Arguments inclosed in ||instead of ()
  • Type annotations are optional, rust will generally infer the types.
  • Curly brackets also optional for one liner.
  • Closure is called just like any other function

Inner function vs Closure

fn outer_function() {
    let outer_variable = 10;

    // Define an inner function
    fn inner_function() {
        // Attempt to access outer_variable
        //println!("{}", outer_variable); // Error: can't capture dynamic environment in a fn item
    }
    
        //  Captures `outer_variable` from the environment
    let inner_closure = || {
        println!("{}", outer_variable); 
    };

    inner_function();
    inner_closure();
}

fn main(){
    outer_function();
}

Rust Playground

Example use

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

Example continued

  • Illustrates use of closure in unwrap_or_else.
  • Calls the closure || self.most_stocked() if user_preference is None variant.
  • Closure captures self (immutable reference to Inventory)

Capturing Modes

Closure can capture values in the same three ways that functions can take parameters

Immutable Reference

let list = vec![1, 2, 3];
let print_closure = || println!("{:?}", list);
print_closure();

Mutable reference

let mut list = vec![1, 2, 3];
let mut add_to_list = || list.push(4);
add_to_list();
println!("{:?}", list); // [1, 2, 3, 4]

Capture modes (cont)

Moving ownership

let list = vec![1, 2, 3];
let moved = || drop(list);
moved();
// println!("{:?}", list); // Error: list has been moved

Can also explicitly move ownership:

let list = vec![1, 2, 3];
let moved = move || println!("{:?}", list);
moved();
// println!("{:?}", list); // Error: list has been moved

Rust will use the highest on the list possible: Immutable reference, then mutable if needed, and then move it if needed or requested.

Moving captured values out

  • Closure body can do any of the following:
    1. Move a captured value out
    2. Mutate a captured value
    3. Neither move nor mutate captured values
    4. Capture nothing
  • This determines which traits are implemented
    • FnOnce : Can be called at least once. All closures implement this.
    • FnMut : Can be called multiple times. Implemented by 2-4.
    • Fn : Can be called multiple times safely. Implemented by 3-4.

FnOnce example

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}
  • F is the generic type, with trait bound FnOnce() -> T
  • This means F can be any closure, since we only call it once.

FnMut example

impl<T> [T] {
pub fn sort_by_key<K, F>(&mut self, f: F)
where
    F: FnMut(&T) -> K,
    K: Ord,
// snip
  • Standard library function on slices that takes a function that produces the sort key K.

  • Ord is a trait for ordering, requires cmp function, implemented by orderable types (e.g. all numbers)

  • Takes a FnMut instead of FnOnce because it must be called multiple times to do the sort.

sort_by_key usage example

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}
  • Closure implements all the traits, including FnMut so OK.

Broken example

// snip
fn main() {
    let mut list = [
     // snip
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value); // Moves capture value out
        r.width
    });
    println!("{list:#?}");
}
  • Closure moves the captured value value out.
  • Implements only FnOnce, not FnMut.
  • Compiler error. Rust Playground

Fixed example


fn main() {
    let mut list = [
      // snip
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}
  • Counts number of calls without moving
  • This time the closure implements FnMut (in addition to FnOnce)

Iterators

What are Iterators?

  • Definition: An Iterator provides a sequence of items (one at a time)
  • Laziness: Iterators do nothing until consumed
    • Calling iter() on a collection by itself does nothing
    • Methods like sum(), collect(), or a for loop consume the iterator
  • Benefit: You avoid writing index-based loops manually
    • Less chance of off-by-one errors
    • Consistent, reusable iteration logic. Functional style.

Example

let v1 = vec![1, 2, 3];

// Creating an iterator (does nothing yet)
let v1_iter = v1.iter(); 

// Consuming the iterator in a for loop
for val in v1_iter {
    println!("Got: {val}");
}

N.B. The for syntax will call iter for you, so for val in v1 will also work.

Iterator trait

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // other default methods elided
}
  • One method required, next.
  • Item is an associated type. More on this in Chapter 19.

Demonstrating next

    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }
  • Note that v1_iter must be mutable. next takes mutable ref to self.
  • This consumes the iterator.
  • Also note that the values are immutable references to the values in v1.
    • iter_mut gives mutable references
    • into_iter gives owned values.

Adaptors

  • Consuming Adaptors:
    • Methods that call next on an iterator.
    • Examples: sum and collect
  • Iterator Adaptors:
    • Methods that transform the iterator into a different one.
    • Examples: filter and map.
  • Chain multiple calls to perform complex actions declaratively.

Example

fn main(){
    let v1: Vec<i32> = vec![1, 2, 3, 4, 5, 6];
    let v2: Vec<_> = v1.iter().filter(|x| *x % 2 == 0).map(|x| x + 1).collect();
    println!("{v2:?}")  // [3, 5, 7]
}
  • Note that we need to dereference the x in the filter since iter gives &&i32
  • The type annotation on v2 is not optional, collect can produce different types of collections.

Bonus example

Rust

fn main(){
    let m = 100;
    let sum_n = |n| (1..).take(n).fold(0, |sum, i| sum + i); 
    println!("Sum first {} integers: {:?}", m, sum_n(m));
}

Haskell

main :: IO ()
main = do
    let m = 100
        sumN n = foldl (+) 0 $ take n [1..]
    putStrLn $ "Sum first " ++ show m ++ " integers: " ++ show (sumN m)

Improving our IO Project

Removing clone using an iterator.

  • IO project from last chapter used inefficient clone calls.
  • This was because Config::Build didn’t own args.
  • We can fix this by having Config::Build take ownership of an iterator to the args.

Updating main

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--
}
  • env::args() returns and iterator.
  • Instead of collecting , just move it into the build function.
  • Won’t compile yet, need to fix build

Updating Config::build signature

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --snip--
  • All we need to know about args is that it implements an iterator that returns String items.
  • Reminder: impl Trait syntax was covered in Chapter 10 and is syntactic sugar for a type variable with a trait bound.

Updating Config::build body

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}
  • Since we own the iterator we can move out the owned String values
  • NO need to clone!

Clean up using Iterator Adaptors

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}
  • This converts the search function to a functional style.
  • We eliminate the mutable state (results in the original code).
  • Use of the higher order filter:
    • Removes need for mutable state (results)
    • Removes need for looping with contains

Comparing Performance: Loops vs. Iterators

Example

Loop

let numbers = vec![1, 2, 3, 4, 5];
let mut result = Vec::new();

for &num in &numbers {
    if num % 2 == 0 {
        result.push(num * 2);
    }
}

println!("Result: {:?}", result);

Rust playground

Iterator

let numbers = vec![1, 2, 3, 4, 5];
let result: Vec<_> = numbers.iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * 2)
    .collect();

println!("Result: {:?}", result);

Rust playground

Zero-cost abstractions

  • Iterators often compile to machine code that is as efficient as hand-written loops. This is referred to as a zero-cost abstraction

  • This enables concise, declarative code without sacrificing performance.

Summary

  • Functional programming emphasizes evaluating expressions over executing statements.

  • Rust provides robust support for functional programming:

    • Closures for capturing and reusing surrounding context.
    • Iterators for lazy, efficient, and declarative data processing.
    • Higher-order functions (e.g., map, filter) for transforming data.
    • Immutability by default
  • Enables concise, safe code without sacrificing performance.