Rust: The story of closures

What is closure?

From functions to closure

  • When I call the same function with the same arguments, should I get the same result? In other words, can a function maintain its own state? Some programming languages allow this through “global variable” or “static value”, which allows them to access some intermediate calculation results in the last call and use these information to produce different results.
  • If we can have states, can the function callers have access to the state so they can prepare the state before the function being called? Before closures, the solution of this is either “no” or “yes, use global variables”. The later is less preferred as developer believed uncontrolled use of global variables lead to terrible design.

How Rust is different?

  • How many times a closure can be called? In many programming languages, the answer is “I am not checking it. Call it if you dare”. Unless the function is side-effect-free (like in Haskell), calling a function more than a specific time can lead to problems (double-free for example), but most programming languages just leave this to the developer.
  • If multiple calls are allowed, can a closure get called simultaneously? Again, this is important in multi-thread programming. But before Rust no other languages attempted to address this in the language level.
  • When called again, would the new call use a modified internal state, or roll back to the initial state? Programming language handles this varguly, but most answer is the former as this is the most programmers expect.

The Fn trait family

How can I use a closure and what the closure can do with its state?

  • Fn trait closures let the caller call on only a shared reference without restrictions, but the callee will not be able to modify the internal state, and they cannot move things out of the internal state. All Fn closures can be used as FnMut or FnOnce.
  • FnMuttrait closures let the caller call on a unique reference, and can only be called subsquently. The callee are free to modify the internal state, and move things out if they want, as long as the internal state remains ready for the next call. All FnMut closures can be used as FnOnce.
  • FnOnce trait closures can only be called once. The callee can do everything with the internal state, including destroy it completly.

Can I call the closure with the exact same internal state?

  • Closures that is Copy + Clone: they are almost as powerful as Fn, because they can be called without restrictions. Combine with FnOnce it gives the best flexibility: the callee have no restrictions on what they can do with the state as well (but the state have to be copy)! However, the trade off is that everything you did with the internal state in the callee code, will be lost as the next call will be working on the initial state. There is more to say about FnMut + Copy + Clone, as there will be two ways to call the object, one is using the initial state and the other using the modified state. I will catch up this later.
  • Closures that is just Clone: Like the above, but you now have to manually call clone method on the closure to keep a specific state.
  • Closure with none of the traits: the closure hold a unique state, and you cannot replicate it.

Deeper duscussions

fn call_fn_once(f: impl Copy + FnOnce()) {
f();
f();
}
fn call_fn_mut(mut f: impl FnMut()) {
f();
f();
}
// Demostrates how to turn `Copy + FnOnce` into `FnMut`
// It also works for native `Copy + FnMut`, but it never
// mutates its state.
fn as_fn_mut(mut f: impl Copy + FnOnce()) -> impl FnMut() {
move || (&mut f)()
}
//Demostrates how to turn `Copy + FnOnce` into `Fn`
//It always behave the same as calling the `FnOnce`
fn as_fn(mut f: impl Copy + FnOnce()) -> impl Fn() {
move || f()
}
fn test(f: impl Copy + FnMut()) {
call_fn_once(f);
call_fn_mut(f);
call_fn_mut(as_fn_mut(f)); //calls the converted`FnMut`
}
fn main() {
let mut i = 0;
test(move || {
i += 1;
dbg!(i);
});
}
[src/main.rs:24] i = 1
[src/main.rs:24] i = 1
[src/main.rs:24] i = 1
[src/main.rs:24] i = 2
[src/main.rs:24] i = 1
[src/main.rs:24] i = 1

--

--

--

地球发动机

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Schema Activation? How is schema.org important?

Schema Activation? How is schema.org important?

Salesforce “How To” ABCs: I

LeetCode Day #18 -Minimum Path Sum

################# Project General Overview #################

From Dev to Ops: Infrastructure is Hard, but Tooling and Visibility are the Key for Full Cycle…

ADF Pipelines + with CICD, the YAML approach

Spring Boot dead letter queue implementations

Run Jenkins With Docker image

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Earth Engine

Earth Engine

地球发动机

More from Medium

A Beginners Guide to Learning Rust

Rust programing for beginners [part two]

LCM reduction of an Array.

Rust Tales — step 0