Rust closure learning experience

  • move && Fn / FnMut / FnOnce

move affects how the closure captures variables in the environment, and whether external variables can continue to be used after the closure is initialized.

How to use variables in the closure body determines what kind of Trait the closure implements and affects the type of closure itself. However, the way the closure body uses variables also affects whether external variables can continue to be used after closure initialization.

In other words, whether environment variables can continue to be used after closure initialization is actually affected by two points, whether the closure has a move parameter, and how variables are used in the closure. Therefore, this may happen: the use of variables in the closure body is the slightest immutable reference and does not take away the ownership, but the move parameter is added when initializing the closure, which makes the environment variables unavailable after the closure is initialized.

How to use variables determines what kind of trail closure implements. What does it mean?

  • The closure method implements Fn by accessing the closure of its captured variables through immutable references
  • The method of closure implements FnMut by accessing the closure of its capture variable through variable reference
  • If the closure method can only be called once, it implements FnOnce. (indicates that the closure consumes the captured variables).

Rust will deduce how to capture variables in the environment according to the closure body and whether there is a move. The mode priority order of variables in the closure capture environment is: immutable borrowing, variable borrowing and moving.

fn main() {
    // In the closure body of the following code, it does not obtain the ownership of variables or transfer the ownership of variables,
    // So the type of this closure is FN () - > box < string >
    // After the closure captures s, the original variable can still be used
    let s = String::from("gef");
    let t = || {
        let s = &s;
        let g = Box::new((*s).clone());
        g
    };
    println!("{}", s);

    // There is only one way of forcibly taking away the closure, that is, this closure and move will affect the ownership.
    // ****Even if the * * * * closure appears to be a variable in the environment used to operate with the most common immutable reference, it doesn't seem to take away the ownership of the variable
    let s = String::from("gef");
    let t = move || {
    let s = &s;
    let g = Box::new((*s).clone());
    g
    };
    // Without comments, the compilation will fail, and s ownership has been taken away
    // println!("{}",s);
    // Even if move is used, the type of this closure is still FN () - > box < string >
    let fuck:Box<dyn Fn()->Box<String>>=Box::new(t);

    let s=String::from("gef");
    let t=||s;

    // This line of code cannot be compiled because the ownership of s has been moved to the closure, but if the closure body is & s, it can be compiled
    // println!("{}",s)
    // Whether move is used or not, the type of this closure is fnonce() - > string, because what kind of trail the closure implements only depends on how to use the captured variables
    // However, the code of the closure body affects how to capture variables to a certain extent. Rust will decide whether to transfer the ownership of variables to the closure body through derivation
    // For types that do not implement copy trail, if you do not add move, the ownership of environment variables will be taken away, depending on the use method in the closure body
    // **However, if move is added, no matter how the closure is written, the captured environment variable (copy trail is not implemented) will be taken away**
}

The initialization behavior of a closure will only be executed once, that is, at the closure definition, not at the closure execution. In this process, variables will be captured only once at the definition of closure, and no capture operation will be carried out at later execution.

fn main(){
    let s=String::from("111");
    let g=||s;
    g();
    let g=||s; // 🔺
    g(); // 🔺
}

Even if the environment variable is recovered, the closure will not be captured again. At the closure definition, the variable has been moved to the closure body, and then the variable is consumed in the first execution, and the variable capture action will not be carried out in the second execution, so the variable is gone. Therefore, the second execution of closure will report an error.

It can be seen from here that if a closure can only be executed once, it must take the ownership of environment variables, so this closure must be of FnOnce type.

In fact, FnOnce is a trail implemented by each closure, because each closure can be executed at least once, and then gradually specialize its implemented trail according to the way the closure uses variables.

If the closure takes away the ownership of the environment variable and cannot be executed again, the type of this closure must be FnOnce

However, the reason why the second execution cannot be performed may be the move keyword of the closure. However, the move keyword does not affect the type of closure. What kind of trail a closure implements still depends only on how variables are used inside the closure. For example, the following code can be compiled

fn test(_t:impl FnOnce()){

}

fn main() {
    let s = String::from("abc");
    let f = move || println!("{}",s);
    test(f);
}
//

If it is Fn, the closure only captures the immutable reference of the variable, or even does not capture the variable.

An example in Rust by Example:

fn main(){
    let mut t=5;
    // Mut must be added here, because the closure body is a variable used in mut mode
    let mut x=||{
        t+=1; // How closures use variables
    };
    x();
    println!("{}",t);
}
  • Receive closures as parameters & & return closures (the method of receiving closures has one more trail bounds than returning closures)

Since the impl method does not involve generics, some generic qualifiers cannot be used. More often, code that receives closures tends to use the trail bounds approach.

  • Receive closure

You can use the Box dyn written in Rust book to receive, but there are runtime penalties. You can also use generic + trait bounds or impl trail

dyn mode:

fn fuck(f:Box<dyn Fn(i32)->i32>,arg:i32){
    println!("{}",f(arg))
}

fn main(){
    let f=|num:i32|num+1;
    fuck(Box::new(f), 1);
}

You can also use generic + trail bounds:

fn fuck<F>(f:F,arg:i32)
    where
    F:Fn(i32)->i32
    {
        println!("{}",f(arg))
    }

fn main(){
    let f=|num|num+1;
    fuck(f, 1);
}

impl trait:

fn fuck(f:impl Fn(i32)->i32,arg:i32){
    println!("{}",f(arg))
}

fn main(){
    let f=|num|num+1;
    fuck(f, 1);
}
  • Return closure

When you need to return a Trait, you can generally return the specific type that implements the Trait. However, a closure can only be expressed as a Trait and Trait is a kind of DST, so you can't directly return a closure: it doesn't have a specific type, it can only be expressed as a Triat. The memory size required by the types implementing a trail on the heap is different, but the size of a pointer to them is determined, so Box + dyn can be used to complete this task.

dyn is written to clarify semantics. Otherwise, trail object and trail are too similar.

fn main() {
    let f = return_a_closure();
    let t = f(1);
    assert_eq!(t, 2);
}
fn return_a_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x: i32| x + 1)
}

The above paragraph is from the official tutorial of Rust, but now there are ways to return closures other than trail object. https://doc.rust-lang.org/reference/types/impl-trait.html#abstract-return-types

The following is from Rust by Example:

**// This function returns a closure, but the return value is limited by the type here. It not only writes and implements the trail of Fn,
// It also details the parameter types received by the closure and the parameter types returned**
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn main(){
    println!("{}",returns_closure()(1))
}

impl trait syntax from https://doc.rust-lang.org/std/keyword.impl.html Last paragraph:
The other use of the impl keyword is in impl Trait syntax, which can be seen as a shorthand for "a concrete type that implements this trait". Its primary use is working with closures, which have type definitions generated at compile time that can't be simply typed out.

T rust by example explains: impl trait provides ways to specify unnamed but concrete types that implement a specific trait It can appear in two sorts of places: argument position (where it can act as an anonymous type parameter to functions), and return position (where it can act as an abstract return type).

fn thing_returning_closure() -> impl Fn(i32) -> bool {
    println!("here's a closure for you!");
    |x: i32| x % 3 == 0
}

a concrete type that implements this trait "can explain why the parameter list and return value list of the closure should be written clearly, because impl keyword refers to the concern type that implements this Trait. Since it is concern type, it is necessary to write all kinds of restrictions clearly.

It can also be understood that the type of closure is unique, and the type that cannot be written can only be written through Fn trait.

Impl trail can be written more simply than trail object. And because the Box is wrapped in the trail object, there is a runtime penalty.

  • Complete example
fn receive1<T>(f: T)
where
T: Fn(i32) -> i32,
{
    let num = 4;
    let num = f(num);
    println!("{}", num);
}

fn receive2(f: impl Fn(i32) -> i32) {
    let num = 10;
    let num = f(num);
    println!("{}", num);
}

fn receive3(f: Box<dyn Fn(i32) -> i32>) {
    let num = 15;
    let num = f(num);
    println!("{}", num);
}

fn return1() -> impl FnOnce(String) -> String {
    let greeting = String::from("hello budy, ");
    move |name: String| format!("{} {}", greeting, name)
}

fn return2() -> Box<dyn FnOnce(String) -> String> {
    let greeting = String::from("hello budy, ");
    Box::new(move |name: String| format!("{} {}", greeting, name))
}

fn main() {
    let t=|a| a+1;
    receive1(t);
    receive2(t);
    receive3(Box::new(t));

    println!(" {} ",return1()(String::from("jhon")));
    println!(" {} ",return2()(String::from("nerd")))
}

receive123 are trail boundaries, impl trail, and trail object

return12 is impl trait and trait object respectively

Keywords: Rust

Added by mubarakabbas on Mon, 07 Mar 2022 11:02:52 +0200