Eta-expansion, or what does Rust think it is protecting me from?

There is no real reason per se, I'd say, it's rather an unhappy byproduct of how everything else in Rust works.

In this case, there are two things going on / to be aware of:

  • Expression / statement temporaries, and when they are dropped.

    That is, the "eta-expanded" / "outlined (2nd) arg" version of:

    let x1 = t.touch(t.touch(6));
    

    is actually:

    //            if this expression created temporaries ...
    //             vvvvvvvvvv                |
    let x1 = match t.touch(6) { x0 => { //   |
        t.touch(x0) // -- borrow of `t` ---->|
    }}; // <-- ... they'd be dropped here ---+
    

    That is, the temporaries created by t.touch(6) (if any) are only dropped after the outer call, so if one of these temporaries had drop glue, and borrowed from t, then the compiler would be right in forbidding the t.touch( t.touch(6) ) call.

    • Granted, it is not the case in your example, but a sneakier one could feature it.

    This "problem" would not be present if rewriting the code as:

    let x1 = {
        let x0 = t.touch(6); // ---------+
        // <-- temporaries dropped here -+
        t.touch(x0)
    };
    

    This is to say that no matter what I'll say in the following section, there is a semantic difference between your two versions; if you want to "eta expand" / "outline a temporary as an expression", then the match is the actual way of writing it (featuring let ... in / scoped let semantics).

  • Order of evaluation of function args.

    This has to do with the fact that, if you wrote:

    fn touch_into (amt: i32, at_t: &mut Obj)
      -> i32
    {
        at_t.touch(amt)
    }
    

    then:

    let x0 = touch_into(t.touch(6), &mut t);
    

    compiles fine.

    How so? Well, because the expression t.touch(6) is evaluated first, the borrow has the time to end, and only then is &mut t evaluated, avoiding the conflicting borrow.

    but in your example, if we were to write in in fully desugared form, we'd have:

    t.touch(t.touch(6))
    

    becomes

    Obj::touch(&mut t, t.touch(6))
    

    which becomes:

    match &mut t { arg0 => {
        //    vvvvvvvvvv - Error, t is already being borrowed.
        match t.touch(6) { arg1 => {
            Obj::touch(arg0, arg1)
        }
    }
    

    As you can see, you hit a limitation whereby order of evaluation leads to the borrow on the "left t" to start before the borrow on the "right t".

7 Likes