Rust Intermediate

Intermediate Rust

Rust Intermediate

Advanced Functions and Closures

Advanced Functions and Closures

By now, you must be good at writing functions in Rust. If you have prior programming experience, you would know that functions sometimes accept pointers as parameters. If you know functional programming, you must have heard about higher-order functions and closures. Let’s take them step by step.

Closures: syntax, capturing, and usage

Closures are anonymous functions that can capture variables from their surrounding scope. They are flexible and powerful tools for encapsulating behavior concisely.

Syntax

Here’s a simple example of a closure that adds two numbers:

pub fn do_closure() {
    let add_closure = |x, y| x + y;
    let result = add_closure(10, 5);
    println!("Result: {}", result);
}

fn main() {
    // Use do_closure.
    do_closure();
}

Now, let’s move on to Capturing.

Capturing

Closures can capture variables from their enclosing scope. They can capture variables by reference or by value. Consider this example:

pub fn do_capture() {
    let base = 10;
    let add_to_base = |x| x + base;
    let result = add_to_base(5);
    println!("Result: {}", result);
}

fn main() {
    // Use do_capture.
    do_capture();
}

The examples show how closures work in Rust: one or more variables are captured into an anonymous function denoted by pipes, the operation is carried out right after the capture, and the entire process is assigned to a variable. The variable can then be used as a function all along.

Let’s see how this is done in a real system. We will use closures to customize the formatting of log messages based on their severity levels:

pub struct Logger {
    pub log_level: LogLevel,
}

pub enum LogLevel {
    Info, 
    Warning,
    Error,
}

impl Logger {
    pub fn new(log_level: LogLevel) -> Self {
        Logger { log_level }
    }

    pub fn log(&self, message: &str, formatter: impl Fn(&str) -> String) {
        if self.should_log() {
            let formatted_message = formatter(message);
            println!("{}", formatted_message);
            
        }
    }

    pub fn should_log(&self) -> bool {
        match self.log_level {
            LogLevel::Info => true,
            LogLevel::Warning => true,
            LogLevel::Error => true,
        }
    }
}

fn main() {
		let logger = Logger::new(LogLevel::Warning);

    let info_formatter = | message: &str | format!("INFO: {}", message);
    let warning_formatter = | message: &str | format!("WARNING: {}", message);
    let error_formatter = | message: &str | format!("ERROR: {}", message);

    logger.log("This is an information message", info_formatter);
    logger.log("This is a warning message", warning_formatter);
    logger.log("This is an error message", error_formatter);
}

In this example, we've defined a Logger struct with a log method that takes a message and a closure to format the message based on its severity level. The Logger struct also has a should_log method to determine whether a message should be logged based on the log level set during initialization.

We then define three formatting closures (info_formatter, warning_formatter, and error_formatter) that prepend the appropriate log level to the message.

In the main function, we create a Logger instance with a log level of Warning. We then log messages with different severity levels using the corresponding formatting closures. Since the log level is set to Warning, only the warning and error messages are logged.

This example showcases how closures can encapsulate behavior and customize functionality. In a backend scenario, you could extend this logging system to include additional features such as writing logs to files, sending logs to remote servers, or integrating with third-party logging libraries.

Function pointers and higher-order functions

Rust supports functional programming. Thus, you can write functions to receive anything as value, including other functions, and to return anything as a result, including other functions. This opens the door to using function pointers and higher-order functions.

Function pointers

A function pointer refers to the memory location of a function. They're useful for scenarios where you want to pass functions as arguments or store them in data structures. Here's a basic example:

pub fn add(x: i32, y: i32) -> i32 {
    x + y
}

pub fn subtract(x: i32, y: i32) -> i32 {
    x - y
}

pub fn calculate(operation: fn(i32, i32) -> i32, x: i32, y: i32) -> i32 {
    operation(x, y)
}

The identity of the function pointer is not known at compile time.

Higher-Order functions

These are functions that take other functions as arguments or return functions. They allow you to create more abstract and reusable code. Consider the following example that implements a higher-order function for applying a function to each element of a vector:

pub fn apply_operation(operation: fn(i32) -> i32, number: i32) -> i32 {
    operation(number)
}

pub fn square(x: i32) -> i32 {
    x * x
}

pub fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
		let num = 5;
    let squared_result = apply_operation(square, num);
    println!("Square of {} is {}.", num, squared_result);

    let doubled_result = apply_operation(double, num);
    println!("Double of {} is {}.", num, doubled_result);
}

Using a real-world example, let’s see how higher-order functions that have closures are written. Below, we simulate a data processing pipeline for user data:

pub struct User {
    pub id: u32,
    pub name: String,
    pub age: u32,
}

fn main() {
		// Higher-order functions with closures.
    let hof_users = vec![
        advanced::hof::User { id: 1, name: "Alice".to_string(), age: 18 },
        advanced::hof::User { id: 2, name: "Bob".to_string(), age: 32 },
        advanced::hof::User { id: 3, name: "Charlie".to_string(), age: 24 },
    ];

    // Higher-order function: map
    let names: Vec<String> = hof_users.iter().map(|user| user.name.clone()).collect();
    println!("User names: {:?}", names);

    // Higher-order function: filter
    let adults: Vec<&User> = hof_users.iter().filter(|user| user.age >= 18).collect();
    println!("Adult users: {:?}", adults);

    // Higher-order function: reduce (fold)
    let total_age: u32 = hof_users.iter().map(|user| user.age).fold(0, |acc, age| acc + age);
    println!("Total age of all users: {}", total_age);
}

In this example, we have a User struct representing user data. Our goal is to use higher-order functions to process this data differently.

Map

We start by using the map higher-order function to transform the user data. We create a new vector containing only the names of the users using the map function and a closure that extracts the name field from each user. This demonstrates how higher-order functions can transform data into a different format.

Filter

Next, we use the filter higher-order function to select specific users based on a condition. We create a new vector containing only the adult users (those aged 18 or older) using the filter function and a closure that checks the age field. This showcases how higher-order functions can be used for data filtering.

Reduce (Fold)

Finally, we utilize the fold higher-order function to perform a reduction operation on the user data. We calculate the total age of all users using the fold function and a closure that accumulates the ages. This demonstrates how higher-order functions can be used for data aggregation.

Exercise

  1. Higher-Order Functions: Write a higher-order function that takes a vector of integers and returns a new vector containing only the even numbers.

  2. Closures: Create a program that simulates an online auction. Implement a bidding mechanism using closures to define the behavior of placing bids and determining the winner.

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board