I was thinking about the idea of "must-destructure" types (a offshoot of "undroppable" types) after reading boat's Asynchronous clean-up post.
I like the idea that rustc can tell me "you can't just implicitly drop this type here". It nicely eliminates the possibility of accidental not calling custom cleanup fn in some branch of code.
I suggested it over on reddit but didn't get a clear reason on why it was stupid, so perhaps you guys could help provide that .
Example: Async cleanup
async fn example() {
let guard = must_destructure_example::start();
guard.do_something(1).await;
guard.do_something(2).await;
// compiler will err without the next line
// as AsyncGuard must be destructured
guard.cleanup(123).await;
}
mod must_destructure_example {
pub fn start() -> AsyncGuard {
// start something
AsyncGuard {}
}
#[must_destructure] // <-- new thing
pub struct AsyncGuard {
// ...
}
impl AsyncGuard {
/// Do something.
pub async fn do_something(&self, arg: u8) {
// do some operation
}
/// Cleanup using the given `arg`.
pub async fn cleanup(self, arg: u8) {
// destructure and do cleanup asynchronously
}
/// Fallback sync cleanup.
fn sync_cleanup(&mut self) {}
}
impl Drop for AsyncGuard {
fn drop(&mut self) {
self.sync_cleanup();
}
}
}
Note: Without the line guard.cleanup(123).await;
rustc will fail: "must destructure `guard`" and will suggest using the available self
methods, e.g. AsyncGuard::cleanup
.
Limitation: Can't move into generic code
You couldn't move such types into generic code, as that code is allowed to just drop them and that would make them quite easily accidentally dropped. So rustc would have to forbid this.
This is a big limitation. But if it is possible to implement this rule the types still seem quite useful.
It is conceivable that generics could allow must-destructure types as opt-in somehow too, though such an ability would not be an initial requirement.
Otherwise can be moved
Like existing drop-guards, but unlike try-finally or defer, these types can be moved (just not into generic code). E.g. the AsyncGuard
is moved in the example. This makes them more flexible as they would be used a similar way to sync drop-guards and could be composed into other types.
Leaking
As with sync drop-guards these types could benefit from better guarantees that they shouldn't leak (aka unforgettable types). However, I think they would be useful regardless as sync drop-guards already are.
Drop handles edge cases: Panicking & cancelling
The difficulty then are the edge cases: panicking & cancelling (perhaps there are others too?). As you can move types out of a runtime, can cancel any future, can panic etc, it seems a necessity to cover these with a sync fallback.
Why not use Drop
to define this sync fallback? Then rustc will do its best to avoid implicitly dropping, but then actually will use Drop
to handle panics, cancels and perhaps other such edge cases.
Because of this rustc should probably require must-destructure types implement Drop
.
In this way any must-destructure type can define a sync-drop behaviour as a fallback. But rustc can help a lot in ensuring that something like an async cleanup fn is called in almost all regular usage.
This is also why I don't call these types "undroppable".
Composition
Must-destructure types may be used as fields of parent structs. It makes sense that rustc should then also require that parents are also must-destructure types.