Update moved variable


#1

Hey guys, i’m currently working on the following situation: https://play.rust-lang.org/?gist=7b64e70978e68c36c990c6763a14fa11&version=nightly

I talked about this earlier on #rust and understand what’s happening.

  1. outer_t is moved into for_loop = inner_t
  2. inner_t is consumed and replaced with an updated inner_t
  3. inner_t is dropped because scope of for loop ends
  4. next iteration starts and outer_t is inaccessible

I’m curious about what can be done to make this example compile with minimal changes. Can anyone help me make this work?
The main issue here is the move into for_loop which basically implies i need a Copy type, so i must wrap T first into one of the smart pointers but that’s not ideal.
No, i can not change L to take a mutable reference, because T is an object which can be consumed by Into operations… L is always guaranteed to return the exact same struct type that it consumed.

On the other hand i found this RFC which basically does what i need, but only applies to references.
There might be another feature out there which would make this example compile? So far i couldn’t find any.


#2

The problem is the error case. If your function returns an error (which it doesn’t, but the signature claims it might), then t was moved (as per the function signature), and no new value was assigned.

Look at https://play.rust-lang.org/?gist=8e722e1ff9d10a53cfd7b2e4e6acc89a&version=nightly, this works, just by changing the signature and removing the match. You wrote

L is always guaranteed to return the exact same struct type that it consumed

which isn’t what you linked, so maybe this change is what you need. If not, you need to tell more about what you want to do and what the constraints are.


#3

This isn’t true btw, t is only really dropped at the end, where it’s scope ends.


#4

Change continue to panic!() and your code will run.


#5

Ah! Seems like i made the mistake by creating a too narrow reproduction.

I updated the example to reflect more of the actual implementation and incorporated your argument about the Err invariant. The result can be found here: https://play.rust-lang.org/?gist=a782efce768f00f6554ca166a387041f&version=nightly

My issue is solved now.

Thanks again for your help! :smiley:


#6

Ok, so what would be the correct assessment for that example then?
There is no move, so there is no inner_t created?


#7

I’m not really understanding that inner_t vs outer_t argument, t isn’t shadowed anywhere… maybe there’s something I don’t see.

The assessment would be what I wrote above, it’s not really a problem of scope/lifetimes, but you’re trying to consecutively apply functions, but the return type of the first one isn’t the input type for the second, so it’s not going to work. Your t was the proper type to input into the first function, but the assignment you wanted to do with the match would not work out because of that fundamental problem.

Your solution is to break of the loop once a function doesn’t return the correct enum variant. That certainly works, but is just a tad different from what you tried first, so it works.

Btw, just to clarify since it’s somewhat similar to that for loop “problem”:

    match run_listeners(t) {
        Ok(t) => println!("OK: {:?}", t),
        Err(t) => println!("Expected fail: {:?}", t),
    };

You’re shadowing t here, there are new bindings created in each match arm. You might want to use

    match run_listeners(t) {
        Ok(s) => println!("OK: {:?}", s),
        Err(e) => println!("Expected fail: {:?}", e),
    };

for clarity.


#8

Another way to fix the original version of your code:

#[derive(Debug)]
struct Target {
    data: &'static str
}

fn demo(x: Target) -> Result<Target, (Target, String)> {
    println!("HIT");
    Ok(x)
}

fn main() {
    let mut t = Target{ data: "some data" };
    let listeners = vec![demo, demo];
    for l in listeners.iter() {
        t = match (l)(t) {
            Ok(v) => v,
            Err((v, _)) => {t = v; continue},
        };
    }
    
    println!("{:?}", t);
}

This way t is always moved back to t, even if function has failed.


#9

One way to change the original code to compile, is set to again to the default.

Err(_) => { t = Target{}; continue },

There is no inner/outer which makes what you describe confusing.
t has two primary states. Initialised and Uninitialised.
Your let mut ... = ... use assignment = so t starts Initialised.
t is then moved by calling a function. At which point it becomes Uninitialised.
The t = match only performs another Initialisation when Ok. The compiler requires t to be fully initialised so gives the error.


#10

Returning the value back in the Err case is a canonical way to handle these style of APIs. For example, Streams and Sinks in the futures crate do this routinely because a lot of their APIs take self.


#11

You don’t even need the assignment and continue here - just yielding v in the Err case as in Ok is sufficient.


#12

It just depends on what he wants to do. Seems to me he has a bunch of functions he wants to apply one after another, and the question is: What should happen if one fails (i.e. returns an Error)? His solution is "In that case, stop applying functions and return that error, your solution is “In that case, skip that function and keep on going”. It’s just different, right?


#13

I was referring to @rostidev’s snippet there where it was just looping around anyway and the match expression was already being assigned back to t.

In the general case I completely agree.


#14

And I just modified the original topic author’s code with the same continue.

I don’t know why @Bert-Proesmans wrote his original code this way. Could it be an attempt to overcome the aliasing+mutation restriction in Rust? I heard about this restriction in the following lecture of Aaron Turon in Stanford:


see from 16:00

#15

As mentioned above i misunderstood the syntactic implications of my code. As a result i built a reproduction which did not fully encompass all relevant operations from the original code.
My apologies for this causing misunderstanding.

As for the aliasing+mutation restriction, this is not the case. I actually need full ownership and will return a new object each iteration.
What i’m doing in my code is processing state machine states (Target) by consuming them and possibly transforming them into other states. The transitions are either forced A->B->C or defined in terms of pushdown/pullup A<->B<->C.
The state Finished (see the play link in my first response) is a final state which allows no new transitions.