use std::thread;
let value = 0;
thread::scope(|scope_1| {
thread::scope(|scope_2| {
for scope in [scope_1, scope_2] {
scope.spawn(|| &value);
}
});
});
Can we update the scoped thread API so that the code above compiles? For example, as described in the title, would a new set of API in the following code be correct?
pub struct Scope<'scope, 'env: 'scope> {
// Other fields are omitted.
// `'scope` and `'env` are both contravariant.
variance: PhantomData<fn(&'scope (), &'env ())>,
}
impl<'scope, 'env> Scope<'scope, 'env> {
// Lifetime in `&self` is also relaxed from `'scope`.
pub fn spawn<F, T>(&self, f: F) -> ScopedJoinHandle<'_, T>
where
F: FnOnce() -> T + Send + 'scope,
T: Send + 'scope,
{
...
}
}
The "scope" (the closure) can borrow from the environment, and can do so mutably, so &'scope mut &'env makes sense to represent that. That said, the only way the environment is actually accessible through the Scope and not the closure is effectively fn(&'env _), so you could also make an argument for by-example contravariance that way.
'scope is a tricker argument; the description claims that 'scopeis the lifetime of the Scope, including that 'scope ends as the Scope drops. This formulation directly encodes that 'scope is the lifetime of the Scope given to you, but discards the bit about the lifetime "end" with the observation that the end timing doesn't actually matter, just that the 'env: 'scope bound remains true.
So I conclude that 'scope can be contravariant easily, but 'env is more questionable. Perhaps the proper by-example variance is fn(&'scope mut &'env _), even. (Could that end up contravariant? I'm not sure.)
But big picture, what's the benefit of relaxing Scope? I do think it'd be more "correct" for Scope::spawn to take &'_ self instead of &'scope self, but the APIs work as is and invariance, while conservative, isn't causing any meaningful usability issues that I can see.
Note: I'm actually in favor of correctly applying contravariance to types instead of just throwing invariance at it when covariance is the wrong thing. But it still bears asking why this is a more correct model of the borrow/loan lifetimes going on.
The most obvious benefit of relaxing Scope is to allow more valid programs to be compiled successfully. Although I don’t currently have an actual use case for this flexibility.
This problem came out when I found out that I needed a similar API like the scoped thread API to ensure certain values outlive a certain scope. So the scoped API might be somewhat a common pattern in Rust. I want a use the standard library scoped thread API as a reference design, then I was wondering whether the standard library API can be considered canonical.
Now there are two problems:
Is an API with relaxed variances still sound?
If the relaxed API is still sound, do we want the standard library to have this flexibility?