1 Month Learning Rust - Closure

1 Month Learning Rust - Closure

Daily short news for you
  • These past few days, I've been redesigning the interface for the note-taking app OpenNotas. It's quite strange to think about why I chose DaisyUI back then 😩

    » Read more
  • Previously, there was a mention of openai/codex - a type of agent that runs conveniently in the Terminal from OpenAI, especially since it is open source and they have now added support for other providers instead of just using the chatgpt model as before.

    Recently, Anthropic also introduced Claude Code which is quite similar to Codex, except it is not open source and you are required to use their API. Since I don't have money to experiment, I've only heard that people in the programming community praise it a lot, and it might even be better than Cursor. On the flip side, there's the risk of burning a hole in your wallet at any moment 😨

    » Read more
  • For a long time, I have been thinking about how to increase brand presence, as well as users for the blog. After much contemplation, it seems the only way is to share on social media or hope they seek it out, until...

    Wearing this shirt means no more worries about traffic jams, the more crowded it gets, the more fun it is because hundreds of eyes are watching 🤓

    (It really works, you know 🤭)

    » Read more

The Problem

In JavaScript, a Closure refers to a function that can remember and access variables from its outer scope, even after that outer scope has finished executing. In short, when a function is defined inside another function and accesses variables from the parent function, a closure is created.

Example:

function makeCounter() {
  let count = 0; // variable in the scope of makeCounter

  return function() {
    count++; // closure accesses the variable count
    return count;
  };
}

const counter1 = makeCounter(); // Create closure
console.log(counter1()); // 1
console.log(counter1()); // 2

count only exists in makeCounter, but the returned function is still able to access it. The proof is that each time counter1() is called, count increments.

In Rust, closures are also similar to those in JavaScript; they can remember and use variables from the outer scope where they are defined. However, closures in Rust are much more complex than in JavaScript.

Closures in Rust

Closures in Rust are anonymous functions that you can store in a variable or pass as arguments to other functions. You can create a closure in one place and call it in another.

Unlike functions, closures can capture values from the scope they are defined in. Let’s compare a function with a closure.

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

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

To call add, you must pass two parameters a and b, and the function returns the sum of those two numbers.

Meanwhile, the closure is written as follows.

fn main() {
    let a = 1;
    let add = |b| a + b;

    println!("{}", add(2)); // 3
}

add is a closure; when calling add, it can access the variable a from the outer environment.

A closure can be declared by assigning it to a variable or calling it directly from the execution point. |b| a + b is a shorthand notation for the closure. The full notation looks like this:

fn main() {
  let a = 1;
  let add = |b: u32| -> u32 {
    a + b
  };

  println!("{}", add(2)); // 3
}

Ownership

Closures can capture values from their environment in three ways:

  • Immutable borrowing.

  • Mutable borrowing.

  • Ownership transfer.

The closure will decide which method to use based on the processing in the body of the closure.

Example.

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}

only_borrows simply prints out the list in list, so this closure only borrows immutably.

Consider the following example.

fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}

borrows_mutably calls a push function on list, changing the original list, so this closure borrows mutably.

A closure takes ownership as shown in the example below.

use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}

Fn, FnMut, FnOnce

After a closure has captured a reference or taken ownership of values from the environment where the closure is defined, the code in the body of the closure will determine what happens to the references or values when the closure is executed. A closure can perform any of the following actions: move the captured value out of the closure, change the value, or not affect it at all.

Thus, there are a total of three types of closures implementing the traits Fn, FnMut, and FnOnce, corresponding to the three ways that closures "treat" the ownership of the values from the environment they capture: immutable, mutable, and ownership transfer.

Why implement these three types? Because it determines how closures can be used in certain situations. For example, the sort_by_key method used for sorting lists can only use closures implementing Fn.

#[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:#?}");
}

If you deliberately use a closure with a different trait implementation like FnMut, the program will not run. For example, the following program will report an error cannot move out of value, a captured variable in an FnMut closure.

#[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 },
    ];

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

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}

Conclusion

Closures in Rust, although conceptually similar to those in JavaScript, are much more complex. They allow capturing values from the outer scope in three ways: immutable borrowing, mutable borrowing, and ownership transfer, depending on how they are used in the closure body. This is controlled through three main traits: Fn, FnMut, and FnOnce, each suited for different situations. This difference not only makes closures in Rust more flexible but also ensures memory safety and strict ownership management, a core factor in Rust's design.

Premium
Hello

The secret stack of Blog

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

As a developer, are you curious about the technology secrets or the technical debts of this blog? All secrets will be revealed in the article below. What are you waiting for, click now!

View all

Subscribe to receive new article notifications

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.

Comments (0)

Leave a comment...