Mastering Rust Functions: A Beginner's Guide to Efficient Code

In this lesson, we will dive into Rust functions, including syntax, closures, higher-order functions, and best practices for writing efficient, clean, and reusable code.

At this point, we are already somewhat familiar with functions. Since we need to use the main function, which is the entry point of a Rust program, and we have seen a couple of examples in previous lessons, we understand how a function in Rust looks. However, in this lesson, we are going to learn the ins and outs of functions.

Functions

fn say_hello() {
    println!("Hello, world!");
}

fn main() {
    say_hello();
}

// Hello, world!

We define a function in Rust using the keyword fn, followed by the name of the function. Like variables, the function name follows the same convention: it should be in snake_case, such as say_hello in the example above.

fn say_hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    say_hello("John");
}

// Hello, John!

If we want to pass data to a function, we can do so using function arguments. These need to be specified in parentheses () along with their data types. If a function has more than one argument, they must be separated by commas. In the example above, the say_hello function accepts only one argument of type &str.

fn say_hello(name: &str) -> String {
    return format!("Hello, {}!", name);
}

fn main() {
    let message: String = say_hello("John");
    println!("{}", message);
}

// Hello, John!

Return values

A function can also return a value. If a function returns a value, it must be specified using the -> annotation followed by the return type. In the example above, the say_hello function returns a String value. To return a value, we use the return statement followed by the value to be returned. Ideally, this should be the last statement in the function body, but it can appear anywhere in the code.

fn say_hello(name: &str) -> String {
    if name == "unknown" {
        return format!("Hello, world!");
    }

    return format!("Hello, {}!", name);
}

fn main() {
    println!("[1] {}", say_hello("John"));
    println!("[2] {}", say_hello("unknown"));
}

// [1] Hello, John!
// [2] Hello, world!

In the example above, we have an if statement in the say_hello function body. If the name argument value is "unknown", the body of the if block is executed, which contains a return statement. When Rust encounters a return statement, it immediately returns the specified value, and any statements or code below it are not executed.

fn say_hello(name: &str) -> String {
    println!("Executing say_hello()");

    format!("Hello, {}!", name)
}

fn main() {
    println!("{}", say_hello("John"));
}

// Executing say_hello()
// Return: Hello, John!

To return a value from a function, we don’t necessarily need to use a return statement. In the Control Flow lesson, we saw how if/else can also be an expression and how its value is evaluated. The same concept applies to functions. In Rust, when the last line in a function body is an expression, the function implicitly returns that value. In the example above, since format!("Hello, {}!", name) is the last line of code in the function body and it is an expression (with no semicolon at the end), it is returned.

fn say_hello(name: &str) -> String {
    if name == "unknown" {
        format!("Hello, world!")
    } else {
        format!("Hello, {}!", name)
    }
}

fn main() {
    println!("[1] {}", say_hello("John"));
    println!("[2] {}", say_hello("unknown"));
}

// [1] Hello, John!
// [2] Hello, world!

In the example above, the say_hello function implicitly returns one of the format!() values based on the if condition. But how does this happen? What we understood is that this implicit return only occurs when the last line of code in the function body is an expression, and none of the format!() expressions are on the last line. To understand this better, let’s look at the code below.

fn say_hello(name: &str) -> String {
    return if name == "unknown" {
        format!("Hello, world!")
    } else {
        format!("Hello, {}!", name)
    };
}

fn main() {
    println!("[1] {}", say_hello("John"));
    println!("[2] {}", say_hello("unknown"));
}

// [1] Hello, John!
// [2] Hello, world!

This example is the same as the previous one. However, the only change made was in the say_hello function. Here, we are explicitly returning the if/else expression. Therefore, we can see that the last line of code in the previous say_hello function was indeed an expression; however, it was spread over multiple lines. Apologies for my incorrect use of terminology.

A function that doesn’t have an explicit or implicit return mechanism returns () (the unit type) by default.

fn say_hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let result = say_hello("John");
    println!("result: {:?}", result);
}

// Hello, John!
// result: ()

In the example above, say_hello doesn’t return anything, but if we still store its return value in a variable and print it, it would show up as (). It’s like having the function declaration as fn say_hello(name: &str) -> (). The main function we are familiar with doesn’t take any arguments and returns ().

fn count_until(max: u8) -> String {
    let mut counter: u8 = 0;

    loop {
        if counter == max {
            return format!("Stopped at {}.", max);
        }

        println!("Current counter is: {}", counter);
        counter += 1;
    }
}

fn main() {
    let result: String = count_until(3);

    println!("result: {}", result);
}
$ cargo run
Current counter is: 0
Current counter is: 1
Current counter is: 2
result: Stopped at 3.

As we learned, a return statement not only returns a value but can also return a value early, terminating the function execution in the process. If there is a loop inside a function, whether it’s a for, while, or loop, the break statement will break the loop but won’t terminate the function. However, a return statement can do both. In the program above, when counter reaches max, return ... is executed, which terminates both the loop and the function, returning a value.

We could also use return; without specifying a value to break the loop and terminate the function early without returning a value (actually, it returns ()).

No optional parameters (arguments)

A dynamic programming language like JavaScript or Python allows you to declare a function that takes optional or a variable number of arguments. With this feature, you can provide an argument or not, and the function will still work. For example, the following TypeScript function declares a sayHello function with an optional argument name. We can also give the name argument a default value, such as function sayHello(name: string = "world").

function sayHello(name?: string): string {
  if (!name) {
    return "Hello, world!";
  }

  return `Hello, ${name}`;
}

console.log(sayHello("John")); // argument passed
console.log(sayHello()); // no argument passed

// "Hello, John"
// "Hello, world!"

However, Rust’s function syntax is quite strict in this case. In Rust, a function must declare all parameters, and all arguments must be explicitly provided when calling the function. The notion of an argument not being provided doesn’t exist in Rust. However, we can use the Option enum to mimic the behavior of an argument being empty, while still complying with Rust’s rules.

fn say_hello(name: Option<&str>) -> String {
    match name {
        Some(text) => format!("Hello, {}!", text),
        None => format!("Hello, world!"),
    }
}

fn main() {
    println!("[1] {}", say_hello(Some("John")));
    println!("[2] {}", say_hello(None));
}

// [1] Hello, John!
// [2] Hello, world!

In the example above, instead of say_hello accepting a &str, it now accepts Option<&str>. We will talk more about the Option enum in detail in an upcoming lesson. The Option enum has two variants: Some and None. The Some variant can store a value. With the Option<&str> type, the variant gets the type Some(&str), which means it stores a &str value. Using a match statement, we can determine whether the argument name is Some or None. Through pattern matching inside a match arm, we can extract the &str value inside Some(&str) and take appropriate action.

Here, say_hello returns a String implicitly because match is used as an expression, and since it’s the only expression, it is returned implicitly.

Recursive functions

Function recursion is supported in Rust, as in many other languages, because function calls are stored on the stack. When a function is called, a stack frame is created, which contains the function’s arguments, local variables, temporary data, and a return address (indicating where the program should return when the function exits). A recursive function calls itself repeatedly, creating additional stack frames. However, there must be a case where the function no longer calls itself and instead returns, which is when stack unwinding begins. This means that each function call returns (exists), and its stack frame is popped off the stack.

fn factorial(n: u32) -> u32 {
    if n == 0 {
        return 1;
    }

    return n * factorial(n - 1);
}

fn main() {
    let result = factorial(4);

    println!("4! = {}", result);
}

// 4! = 24

In the example above, we have a typical factorial function that takes an argument n of type u32, whose factorial we need to calculate and return. The factorial of N, denoted as N!, is equal to N * (N-1)!. For 4!, it’s equal to 4 * 3!, which is equal to 4 * 3 * 2!, which is equal to 4 * 3 * 2 * 1!, and finally 4 * 3 * 2 * 1 * 0!. Since 0! is 1, we no longer need to continue. 0! marks the base case and the end of our recursion.

stack building --->
let result = factorial(4)
                * factorial(3)
                    * factorial(2)
                        * factorial(1)
                            * factorial(0);
                                            <--- stack unwinding

We can visualize this as described above. When we call factorial(4), it checks if n == 0 and executes return 4 * factorial(3);. This won’t return yet because factorial(3) creates a new stack frame and must execute first. This process continues until factorial(0), which is the base case. At this point, it doesn’t create a new stack frame and returns immediately because if n == 0 matches, and return 1 is executed. Then, stack unwinding begins, meaning the previous function calls start returning one by one.

Function pointers

A function’s code is stored in the compiled binary and is referenced by the function’s name. For example, say_hello is the name of a function that refers to its implementation stored in the binary. A function pointer is a pointer to this code.

fn say_hello(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    let say_hello_ptr: fn(&str) -> String = say_hello;

    println!("{}", say_hello_ptr("John"));
}

// Hello, John

In the example above, say_hello_ptr is a function pointer that points to the code of the say_hello function. Function pointers are first-class citizens in Rust, meaning they can be stored in a variable, passed to functions as arguments, and returned from functions. Their type is the type of the function, and it is represented essentially as the function signature, but with the function and argument names stripped, such as fn(&str) -> String in the case above.

Functions are immutable in Rust, meaning they can’t be modified at runtime. Therefore, we can’t mutate them through a function pointer. Unlike some data types, such as String or Vec, where data can be cloned using the .clone() method, this concept does not apply to function pointers.

However, a function pointer is not exactly the same as an anonymous function, like those you might be familiar with in other programming languages. A closely related feature in Rust would be closures.

const functions

A const function (also called constant function) is similar to a regular function defined with the fn keyword, but it uses the const keyword as a prefix. However, unlike a regular function, a const function can be evaluated at compile time. When used in a constant context, such as when providing the initial value of a constant or static variable, or determining the length of an array (which all must be known at compile time), the const function call can be used, and the Rust compiler will evaluate it at compile time and substitute the result.

const fn get_squre(num: usize) -> usize {
    num * num
}

const SQUARE_OF_2: usize = get_squre(2);
static SQUARE_OF_3: usize = get_squre(3);

fn main() {
    let a: [i32; 4] = [1; get_squre(2)];

    println!("a: {:?}", a);
}

// a: [1, 1, 1, 1]

In the above example, the get_squre function is a const function. Since value of const and static variables should be known at the compile time, we can call the get_squre() function to provide that value since it’s evaluated at the compile time. Since array length should also be known at compile time, we can also use the return value of a const function.

Usually, const functions do not even end up in the compiled binary since they are evaluated at compile time and have no purpose at runtime, unless they are used at runtime. This happens when they are called in a non-constant context, such as when a const function is used to assign the returned value to a let variable, like let a = get_square(2), where the result is computed at runtime.

There are several limitations on const functions since they are evaluated at compile time. Some of the common ones are as follows:

  • No Heap Allocation: You cannot allocate memory on the heap from within the function, such as for String, Vec, etc. Only stack-based, fixed-size data can be used.
  • No Mutability: const functions must be stateless and cannot create side effects, such as mutating a variable.
  • No I/O Operations: You cannot perform input/output operations (e.g., no file reading or writing) because I/O cannot be guaranteed to happen at compile time.
  • No Non-Constant Function Calls: You can only call other functions within it if they are also marked as const.

Function optimization

#[inline(always)]
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let result = add(1, 2);
    println!("{}", result);
}

Sometimes compiler inlines the function code where the function has been called to avoid function call overhead at runtime. In the above example, #[inline(always)] attribute on function add will force it to expand where it is called. So while compilation, let result = add(1, 2) would transform into let result = 1 + 2. But inlining may cause other downsides like increasing the binary size and reduce debuggability. Therfore, it’s should be kept to Rust’s compiler to take an appropriate action or use different variations of #[inline] macro as per our need.

  • Use #[inline] for small, frequently called functions where avoiding a function call overhead would be beneficial.
  • Use #[inline(always)] for critical performance paths where function call overhead is unacceptable.
  • Use #[inline(never)] when inlining would increase binary size unnecessarily or when you want to explicitly avoid inlining for debugging purposes.
#[cold]
fn rare() {
    // --snip--
}

#[hot]
fn common() {
    // --snip--
}

We can also use the #[cold] or #[hot] attribute to instruct the compiler to optimize functions.

  • #[cold] tells the compiler that this function is used rarely, so it should keep it out of the way to make frequently used code run faster.
  • #[hot] tells the compiler that this function is used frequently, so it should keep it handy and optimize it to run as fast as possible.

Closures

A closure in Rust is similar to JavaScript’s arrow function or Python’s lambda function. If you’re unfamiliar with them, let’s break it down. A normal function in Rust, defined with the fn keyword, has a name and is immutable and stateless. However, a closure is much like a function but can capture data from its environment and is anonymous (meaning it doesn’t have a name). They are declared using the || <expr> syntax. Since they are anonymous, they must either be assigned to a variable, passed as an argument, or returned as a value. Like function pointers, closures are also first-class citizens.

fn main() {
    let say_hello = || println!("Hello, world!");
    say_hello();
}

// Hello, world!

In the program above, say_hello holds a closure. The closure doesn’t accept any arguments and returns nothing. The expression <expr> in || <expr> is println!("Hello, world!"), which returns (). We can call say_hello like a normal function, as closures are essentially functions.

You can also use a block {} as an expression if you have more than one line of code in the closure, like let say_hello = || { println!("Hello, world!") };.

fn main() {
    let say_hello = |name: &str| format!("Hello, {}!", name);
    println!("{}", say_hello("John"));
}

// Hello, John!

In the example above, the closure accepts an argument of type &str and returns a String value. We changed from println! to format!, which returns this String value. But what is the type of the say_hello variable? If you look at your IDE, you will see impl Fn(&str) -> String inferred for it. So, what is impl Fn? To understand this, we need to look at how closures are different from normal functions.

The most important difference, aside from being anonymous, is that closures can capture variables from their environment. What does that mean? Let’s try to demonstrate this with a normal function.

fn main() {
    let num: i32 = 3;

    // Error: can't capture dynamic environment in a fn item
    fn add(a: i32) -> i32 {
        a + num
    }

    let result = add(2);
    println!("result: {}", result);
}

In the example above, we have declared a function add that is only valid inside the main function, but it tries to use num, which is declared outside the add function body. This is not allowed in Rust because normal functions are stateless and can’t remember or hold onto variables from the surrounding code where they were created. This essentially means that fn functions can’t capture variables from their environment. However, closures are designed to capture data from their environment.

fn main() {
    let num: i32 = 3;

    let add = |a: i32| a + num;

    let result = add(2);
    println!("result: {}", result);
}

// result: 5

Now, add is a closure rather than a function, and it can capture variables from its environment. The program above works just fine. If we look at the IDE again, you will see the type of the variable add is impl Fn(i32) -> i32. Fn is a trait in Rust, which resides in Rust’s standard library and is part of the prelude. The impl keyword indicates that this value implements the Fn trait, which in this case is add. Following the Fn trait is the function signature, similar to what we saw with function pointers. But what exactly does Fn do?

We can’t provide an explicit type of impl Trait to a variable. If you try, for example, let add: impl Fn(i32) -> i32 = ..., the compiler will throw an error:

 `impl Trait` is not allowed in the type of variable bindings
 `impl Trait` is only allowed in arguments and return types of functions and methods

Why does this happen? We will cover this in the Traits lesson. For now, we can rely on Rust’s type inference capabilities, and you should be able to visualize the type of the closure easily in your IDE.

There are three ways closures can capture variables from the environment:

  • By reference: A closure can capture variables from its environment by immutable reference, meaning it does not take ownership of those variables. When it references these variables by their name, Rust internally provides a reference to the original variable. In the example above, num inside the closure body is just a reference to num. A closure that only captures its environment by reference automatically implements the Fn trait, which requires types to implement the call method, and Rust automatically implements this for such closures.
  • By mutable reference: A closure can also capture variables from its environment by mutable reference, allowing the closure to mutate these variables. Such closures automatically implement the FnMut trait, which requires types to implement the call_mut method. Rust automatically implements this method for closures that modify their environment.
  • By value: Lastly, a closure can take complete ownership of variables from its environment. Once these variables are moved into the closure, they can’t be reused outside of it. These closures implement the FnOnce trait, which requires types to implement the call_once method. The Once in FnOnce indicates that the closure can be called only once if it consumes variables. Rust automatically implements call_once method for such closures. This is where move keyword also becomes important with closures.

Fn trait

fn main() {
    let v: Vec<i32> = vec![1, 2, 3];

    let process = || {
        for i in &v {
            println!("i: {}", i);
        }
    };

    process();
    println!("v: {:?}", v);
}
$ cargo run
i: 1
i: 2
i: 3
v: [1, 2, 3]

In the example above, we have declared a variable v that holds a Vector of type i32. The closure process captures it by an immutable reference since it uses &v in the for loop. Therefore, process implements the Fn trait and gets the type impl Fn(). Since process did not take ownership of v, we can still continue to use v after the process closure.

FnMut trait

fn main() {
    let mut v: Vec<i32> = vec![1, 2, 3];

    let mut process = || {
        v.push(4);
    };

    process();
    println!("v: {:?}", v);
}
$ cargo run
v: [1, 2, 3, 4]

In this example, the process closure is modifying the Vector v by pushing a value into it. It uses the .push() method of Vec to do this. If you take a look at the definition of the .push() method, you can see that it accepts a mutable reference &mut self to the Vector. Because of this, process captures v as a mutable reference. In order to mutate v, we need to mark it as mut, and the process closure must also be marked as mut since modifying v will change the internal state of process.

Since the closure process only captures its environment using a mutable reference, it implements the FnMut trait and gets the type impl FnMut(). In this case, v hasn’t been moved into process because it doesn’t take ownership of it, allowing us to use v normally after the closure.

FnOnce trait

fn main() {
    let v: Vec<i32> = vec![1, 2, 3];

    let process = || {
        for i in v {
            println!("i: {}", i);
        }
    };

    process();

    // Error: borrow of moved value: `v`
    // println!("text: {:?}", v); // <-- uncomment

    // Error: use of moved value: `process`
    // process(); // <-- uncomment
}
$ cargo run
i: 1
i: 2
i: 3

In the example above, the for loop takes ownership of v since we removed &. Therefore, v is moved into the process closure. Now, if we try to use v anywhere after the process closure declaration, we get the error as shown above. The borrow of moved value error message tells us that println! is trying to borrow a moved value, which is v in this case. Since process takes ownership of variables in its environment, it automatically implements the FnOnce trait and is inferred to have the type impl FnOnce().

The println! macro uses a reference (borrow) of the value under the hood, instead of taking ownership of that value. Although we provided v to it, the macro implicitly uses &v due to its implementation.

We also can’t call the process closure twice because the Vector v has already been consumed by the for loop. On the next process() call, there is no v left to pass to the for loop.

move keyword

In the example above, the process closure implicitly takes ownership of v because there is no other option but to take ownership of v for the program to work. However, in cases where a closure would implicitly capture a variable by reference, we can instruct the closure to take ownership of it explicitly by using the move keyword.

fn main() {
    let v: Vec<i32> = vec![1, 2, 3];

    let process = move || {
        for i in &v {
            println!("i: {}", i);
        }
    };

    process();
    process();

    // Error: borrow of moved value: `v`
    // println!("text: {:?}", v); // <-- uncomment
}

In this example, the process closure would not normally take ownership of v since we used &v, meaning only a reference to it is captured by the closure. Therefore, we can still continue to use v after the closure. However, when we use the move keyword, we force the closure to take ownership of the variable it captures—in this case, v. As a result, when we try to use v again after the closure, we get the same borrow of moved value error.

But how can we still call process() again? Even though the process closure is a move closure (due to the move keyword), it doesn’t consume v when the closure executes because the for loop is only using a reference to v (&v). For this reason, the process closure gets the impl Fn() type, allowing us to call it as many times as we want.

Even after using the move keyword, closures do not take ownership of types like i32, u8, f32, &str, etc., since they implement the Copy trait. This allows their values to be copied, so they can still be used in the move closure without being fully consumed.

Higher-order functions

Rust supports the functional programming paradigm by providing first-class support for both function pointers and closures. One of the key features of functional programming is the use of higher-order functions (HOFs). A higher-order function is one that either takes one or more functions as arguments, returns a function as a result, or both. By using higher-order functions, we can make our programs more declarative, increase reusability through function composition, and abstract away complex logic.

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

fn operate_with_2(fun: fn(a: i32, b: i32) -> i32, value: i32) -> i32 {
    fun(2, value)
}

fn main() {
    let add_result = operate_with_2(add, 1);
    println!("add: {}", add_result);

    let subtract_result = operate_with_2(subtract, 1);
    println!("subtract: {}", subtract_result);
}
$ cargo run
add: 3
subtract: 1

In the example above, operate_with_2 takes a function pointer as the first argument with the type fn(a: i32, b: i32) -> i32 and a second argument of type i32. The idea is to call the incoming function pointer with the first argument as 2 and the second argument as the incoming value, then return the computed result. We have defined the add and subtract functions to pass them as function pointer arguments. So when we call operate_with_2(add, 1), it calls add(2,1) internally and returns the result.

In order to achieve higher-order functions with function pointers, we need to define the functions beforehand, even if they are used only once. We can improve this with closures. Since closures are anonymous functions, we don’t have to define them beforehand, and they can be passed as values.

fn operate_with_2(fun: fn(a: i32, b: i32) -> i32, value: i32) -> i32 {
    fun(2, value)
}

fn main() {
    let add_result = operate_with_2(|a, b| a + b, 1);
    println!("add: {}", add_result);

    let subtract_result = operate_with_2(|a, b| a - b, 1);
    println!("subtract: {}", subtract_result);
}
$ cargo run
add: 3
subtract: 1

In the above example, instead of passing a function pointer to operate_with_2, we pass a closure. But as we learned, a closure has the type impl Fn, impl FnMut, or impl FnOnce, while operate_with_2 accepts a function pointer of type fn. So how does it work? It works because the closure we are passing doesn’t capture any variables from its environment, allowing Rust to coerce it into a function pointer. Rust automatically coerces closures into function pointers (and vice versa) if the closure does not capture any variables from its environment.

fn operate_with_2(fun: impl Fn(i32, i32) -> i32, value: i32) -> i32 {
    fun(2, value)
}

fn main() {
    let add_result = operate_with_2(|a, b| a + b, 1);
    println!("add: {}", add_result);

    let subtract_result = operate_with_2(|a, b| a - b, 1);
    println!("subtract: {}", subtract_result);
}
$ cargo run
add: 3
subtract: 1

But if you would like to accept only a closure that might capture its environment, you can specify its type as impl Fn(i32, i32) -> i32, like we did above. As we learned, this will only allow closures that capture variables from their environment using an immutable reference.

fn print_vector(v: Vec<i32>) -> impl Fn() {
    move || {
        for i in &v {
            println!("i: {}", i);
        }
    }
}

fn main() {
    let v = vec![1, 2, 3];
    let printer = print_vector(v);

    printer();
    printer();
}
$ cargo run
i: 1
i: 2
i: 3
i: 1
i: 2
i: 3

In the above example, the print_vector function accepts a Vector and returns a closure of type impl Fn(). When we pass the Vector v in the print_vector(v) function call, it takes ownership of v, meaning that v can’t be used anymore after that. Inside the print_vector function, we are returning a movable closure since we want the closure to take ownership of v. However, since the for loop inside the closure only borrows v, ownership stays with the closure, and we can call this closure as many times as we want. Therefore, when we return it from the print_vector function, we can give it the impl Fn() type.

fn print_vector(v: Vec<i32>) -> impl FnOnce() {
    move || {
        for i in v {
            println!("i: {}", i);
        }
    }
}

fn main() {
    let v = vec![1, 2, 3];
    let printer = print_vector(v);

    printer();

    // Error: use of moved value: `printer`
    // printer(); // <-- uncomment
}

But if we want the returned closure to not be called more than once, we can mark the return type of print_vector as impl FnOnce(). For example, if the for loop inside the closure takes ownership of v, once the closure is called, it can’t be called again since v is consumed in the first call. Therefore, as in the program above, we changed the return type of the closure to impl FnOnce(), which will prevent printer() from being called twice.

Higher-order functions are a core part of Rust’s standard library. For example, if you want to transform each element of a collection like a Vector, you can use the .map() method on an iterator, which requires a closure to perform the transformation.

fn main() {
    let v: Vec<i32> = vec![1, 2, 3];

    let tv: Vec<i32> = v.iter().map(|i| i * i).collect();
    println!("tv: {tv:?}");
}

// tv: [1, 4, 9]

In the example above, we first created an iterator from v using the .iter() method on the vector. Then, we used the .map() method on it and passed a closure to provide a new element value. The .collect() method is used to convert the iterator back into a vector.

Currying

Currying is a functional programming technique used to transform a function that takes multiple arguments into multiple functions that each take only one argument. This helps create higher-order functions, promoting code composition and reusability. For a typical function with two arguments, such as f(a, b) -> result, currying would convert it into f(a) -> f(b) -> result.

fn add(a: i32) -> impl Fn(i32) -> i32 {
    move |b: i32| a + b
}

fn main() {
    let add_five = add(5);
    let result = add_five(3);
    println!("5 + 3: {}", result);

    // simplified:
    println!("2 + 7: {}", add(5)(3));
}
$ cargo run
5 + 3: 8
2 + 7: 8

In the above program, unlike a typical add function which has the signature fn add(a: i32, b: i32) -> i32, this version only accepts one argument of type i32 and returns a closure that can be called with a second i32 to produce an i32 value.

#rust #functions #closures