Passing functions around

In Rust, functions are first-class citizens, which means you can pass them around as arguments to other functions. This is a powerful feature that allows you to write more flexible and reusable code.

Let's say we have a function that takes a string, applies a transformation to it, and returns the result. We can define a type alias for the transformation function, and then pass it as an argument to the main function.

Like this:

type Transformation = fn(String) -> String;

fn main() {
    let input = "Hello, world!".to_string();
    let result = transform_string(input, to_upper_case);
    let result = transform_string(result, reverse);
    println!("The result is: {}", result);
}

fn transform_string(input: String, operation: Transformation) -> String
{
    operation(input)
}

fn to_upper_case(input: String) -> String {
    input.to_uppercase()
}

fn reverse(input: String) -> String {
    input.chars().rev().collect()
}

The transform_string function takes a string and a transformation function, and applies the transformation to the string. In the main function, we pass the to_upper_case and reverse functions as arguments to transform_string.

The type Transformation is a type alias for a function that takes a String and returns a String. This allows us to pass any function that matches this signature as an argument to transform_string.

We can also rewrite the example using closures. The to_upper_case and reverse functions can be replaced with closures that do the same thing. As long as the closure has the same signature as the Transformation type alias, it can be passed as an argument to transform_string.

Like this:

type Transformation = fn(String) -> String;

fn main() {
    let to_upper_case = |input: String| input.to_uppercase();
    let reverse = |input: String| input.chars().rev().collect();

    let input = "Hello, world!".to_string();
    let result = transform_string(input, to_upper_case);
    let result = transform_string(result, reverse);
    println!("The result is: {}", result);
}

fn transform_string(input: String, operation: Transformation) -> String
{
    operation(input)
}

Because the to_upper_case and reverse closures have the same signature as the Transformation type alias, they can be stored in an array of transformations, and we can iterate over the array to apply each transformation to the input string.

type Transformation = fn(String) -> String;

fn main() {
    let to_upper_case = |input: String| input.to_uppercase();
    let reverse = |input: String| input.chars().rev().collect();
    let transformations = [to_upper_case, reverse];

    let mut input = "Hello, world!".to_string();
    for transformation in transformations {
        input = transform_string(input, transformation);
    }

    println!("The result is: {}", input);
}

fn transform_string(input: String, operation: Transformation) -> String
{
    operation(input)
}

You can see how powerful this feature is. It allows you to write more flexible and reusable code by passing functions around as arguments to other functions. This is a common pattern in Rust, and you'll see it used in many libraries and frameworks.

Callbacks

Another common use case for passing functions around is callbacks. A callback is a function that is passed as an argument to another function and is called by that function at a later time. It is often used to provide the caller with a way to display events or notifications.

Let's look at an example of using callbacks in Rust.

type CallbackFn = fn(u8);

fn main() {
    let callback = |progress: u8| println!("Progress: {progress:03}%");
    copy_imaginary_file("file.txt", callback);
}

fn copy_imaginary_file(filename: &str, callback: CallbackFn)
{
    println!("Copying file: {filename}");
    for i in 0..=10 {
        callback(i * 10);
    }
    println!("File copied!");
}

Exercises

Let's revisit the Exercises chapter and see if we can solve them with your knowledge on iterators and functional programming. Good luck!