I have often run into this situation which has been extremely awkward to handle.
Let's say we have an enum like this, and please don't suggest changes to the structure, this is simplified for sake of example and factorings are not trivial:
enum Foo {
A(Bar, Label),
B(Bar),
C
}
match foo {
Foo::A(bar, l) => {
if should_change(l) {
*foo = Foo:B(bar) // error: Bar is not Copy
}
}
}
The issue is that we'd like to use some borrowed data to check a condition, then, if the condition holds, use the owned data to produce a new version of the enum.
One option is to try to add a default "invalid" state to the enum, but this is problematic and unsatisfying:
It obfuscates pollutes our code to handle error states for the 99% of the time that they're impossible
It makes it possible to accidentally leak an invalid state
It's generally antagonistic to design principles where we don't want an implicit null state.
It's possible to return a flag, but this results in ugly code that immediately has to re-check the data, and sometimes place something invalid inside just in case (again, we don't get a checked error)
We could do something unsafe, but at minimum that leads to potential leaked errors.
In some cases, the Rust compiler can determine that you're updating something in place, and not throw errors. But this one is quite messy, even if we only do trivial panic-free operations in the middle.
replace_with is helpful, but fails to address the issue adequately.
In particular, if the check panics, then it becomes a program abort, whereas in an ideal solution, a panicked check is just propagated. We shouldn't have to modify the data until after we check that we need to. In this case, the check is potentially long, arbitrary code that might panic.
I see what you're saying and it's useful, but it's not addressing the main issues for me. The code you wrote is as cumbersome as safer code. The issue I have is in part with the double check and in part with the borrow checker sometimes making lifetimes too long from conditionals.
So you still have to do the same double check to update, except you're also just aborting the program if you wrote a bug.
The hole isn't the main issue I want to deal with. The main issue is about borrowing to check and then having to move out of it. If that were cleaned up, then yes something like replace_with would work because it would clearly be panic free.
There's safe, slightly inefficient code that solves my problem, that the compiler might optimize. But it's clumsy and might not be optimized
While the solution improves the inefficiency, the resulting code is overall more complex and dangerous in my eyes, so it doesn't fix the underlying problem
If you're interested in future directions in Rust that are related, this post by Niko Matsakis on the language team describes how the requirements for unwinding might be relaxed. Swapping without unsafe code is a use case that is mentioned.
Something interesting about this pattern is that it's exactly what async blocks’ generated types do sometimes: they change from one variant (state) to the next while keeping some fields in place and replacing others.
If that functionality were somehow exposed for use by Rust code, it would solve this problem. There would need to be a way to express “A.0 is the same field as B.1”, then a way to transition from A to B while supplying only the new fields.
There are some cases that would still be impossible to handle that way, where constructing Foo::B involves taking state from Foo::A and running a computation that can panic; async block futures handle this by potentially having a state reserved for “I previously panicked”. But we could have the ability to shuffle fields, drop fields, and fill in new fields, if the language had a way to express that relationship between variants of fields.
I sometimes see people running into this and I always wonder what their use cases are, since this is not something I've ever encountered to be such an important problem that you are making it look like.
Instead of foo and bar, can you describe what you are actually trying to do? Maybe we can come up with an ownership-based abstraction that allows you to move the whole checking logic out of a &mut self method, so it becomes trivially safe and panic-free.
So in this case I'm optimizing a syntax tree (not much of a tree). But I've had this come up frequently enough in different contexts that I didn't see reason for the concretion. And the concrete details are far too complicated to fit in a post.
You can try to move it to a &mut function, but it doesn't improve anything since I'd need to copy the code into each bespoke function. I already use abstraction for these locally just to make it remotely nice. But in practice my check is a multiline, multistep condition
I'd instead try to build a separate tree entirely, instead of mutating the original. That's true for basically any sort of IR transformation. Doing it in-place is just way too complicated.
That's really excessive for small transformations, and still pretty wasteful for major ones.
Actually in a lot of cases it's way easier to modify in place. For instance, I can loop over a vector of values and modify the necessary ones only.
Although locally I can build a piece or two of it in place where I have indirections so maybe that will help thanks.