Actually my proposal is quite different. Most importantly, my proposal split a single mutable reference binding into multiple bindings, in the same way a typical SSA transform does. Every binding would still have "continuous" lifetime and not assuming any “two phase borrowing".
I have some trial on this idea. But I think this is not currently workable:
revive_onlysupposed to support everything that isCopyor like&mut T. Your example is not even close to it.- The trait bound "
Copyor&mut T" is something not able to express in Rust thanks to the orphan rule. Even the current specialization RFC cannot resolve this issue (which is strange -Copyand&mut Tare non-intercepting type sets anyways). - Even with your example, it will require generic associate type feature to work - the
Targettype can have any life time shorter thanTitself (generic associate type is not even started yet
)
So, in addition to the consume_revive_when_possible requirement, I would put another thing to the list:
- Making trait bound on "
Copyor&mut T" possible. Without Specialization, this can be done by enforcing another magical traitRevivewhich automatically implement for those two type sets.
The usage of this is obvious: they can both work on contexts like consume(v);consume(v) so you can write generic code assuming only it. Without this, you will have to write two seperate functions (and name it seperately) that simply does the same thing.
Is the above answers this question?
@earthengine, Lifetime analysis is very important and you have barely considered it here.
With &T types (assuming T has no embedded lifetimes), it only matters that the referred value is still alive — hence you can freely copy the reference whenever you like, and the lifetime describes only the region where the reference is valid.
With &mut T types, there is an additional requirement: that there is only ever a single active reference. As said above, this currently works by locking the reference “passed”, and passing a “reborrow” of this value but with shorter lifetime constrained to that of the function called.
Your idea of reviving a &mut value assumes that the passed mutable reference does not still exist anywhere — which it is allowed to do if it maintains the original lifetime restriction. In other words, conceptually, either the input needs to be restricted to a shorter lifetime (aka a “reborrow”) or the input reference needs to get returned from the function (this doesn’t require copying the value out, but only restricting its lifetime — in other words, your T: Revive restriction really means that T has lifetime restricted to that of the function).
In other words, it seems to me that the only way Revive could work is by being implemented using the existing lifetime machinery and a mechanism very close to reborrowing — hence trying to call this “revive” seems misguided.
For the benefit of everyone else, I’ll link to the RFC I wrote (which was closed because it appears that it needs more work on higher-kinded types first): https://github.com/rust-lang/rfcs/pull/2364
Pretty much every single suggestion I’ve seen in this area has too little plumbing to be sound wrt lifetime inference and borrow-checking.
However, the “revival” idea is promising in another form, which is that Option<&'a mut T> ought to be copyable under some circumstances, but it can’t be Copy because that’d produce two seemingly-unrelated mutable references.
But if we had a way to change the type of the copy, to, say, Option<&'b mut T> (where 'b is shorter than 'a), then it would be a perfectly valid copy, which means:
// TODO: better names
trait CloneShrink<'a, T> {
// Could this work without `&'a mut self`?
fn clone_shrink(&'a mut self) -> T;
}
impl<'a, T: Clone> CloneShrink<'a, T> for T {
fn clone_shrink(&mut self) -> T {
self.clone()
}
}
// #[lang = "copy_shrink"]
// The compiler would need to enforce that `T: CopyShrink<U>`
// cannot exist unless `T` and `U` only differ by lifetimes.
// e.g. `T = U` and `T = Foo<'a>, U = Foo<'b>` are both valid,
// but `T = Foo<'a>, U = &'b Bar` isn't.
// GATs would make this easier by requiring that you have
// `T = Self::GAT<'a>, U = Self::GAT<'b>`.
trait CopyShrink<'a, T>: CloneShrink<'a, T> {}
impl<'a, T: Copy> CopyShrink<'a, T> for T {}
impl<'a: 'b, 'b, T: ?Sized> CloneShrink<'b, &'b mut T> for &'a mut T {
fn clone_shrink(&'b mut self) -> &'b mut T {
*self
}
}
fn main() {
let mut x = &mut 0;
*x.clone_shrink() += 1;
println!("{}", x)
}
That exact code errors with this funny note:
= note: downstream crates may implement trait `std::clone::Clone` for type `&mut _`
But if you remove the blanket impls, it does work, which means the compiler should be able to integrate CopyShrink in its is-copyable checks, without a lot of design and implementation work.
EDIT: I realize now, after reaching something that works (or at least can be tested for the “Clone” equivalent), that I likely ended up the same point @dhardy started from, with the main difference being associated type vs type parameter (we can do type parameter today, with a small check on every impl).
Hmm. If I’m not mistaken, this is essentially encoding into the trait system the fact that T is covariant on the lifetime parameter.
I guess it’s a combination of covariance and “can make a copy with a slightly different types (from lifetime covariance) if I have exclusive (mutable) access to the source” - it couldn’t be applied to anything that needs dropping, just like Copy can’t.
You mean because it would be reborrowing? A copy would still not be valid, but if you "block" the original until 'b is over then it works.
Another scheme that this might work is the following:
-
The original value at some point split into two parts: a “soul” part that have a different type, and a “body” part that is the same as the original value so can be sent to somewhere that expecting the same type, but both have the exactly the same lifetime.
-
As soon as the “soul” part ends its lifetime, the original variable bind to a “revived” value from the soul again. This is a new valid “copy” of the original value, but still have exclusive access as the original value is guaranteed to gone.
-
For types that have
Dropclues, theDropclue is moved to its “soul” part and so when the “body” part is gone, theDropclue didn’t run. This have to be done manually likeClone, not likeCopy. So I would call it “resurrect”.
I realized right now, to make sure the idea of Revive work we have to be able to say 'a after 'b, means 'a and 'b are independent lifetimes, and 'a starts only after 'b ends.
This seems too big a change before NLL. Maybe NLL can check such constraints?
I believe, the model I have shown here, especially “Revive”, would be a good fit for async/await semantics. Today we can see a few works in tokio that change the Read::read signature from
//well, not quite; this is a trait method. But you know what I mean.
fn read(&mut impl Read, &mut [u8]) -> Result<usize,io::Error>;
to
fn read(&mut impl AsyncRead, Vec<u8>) -> Future<Item=(Vec<u8>,usize),Error=io::Error>;
The semantic of the later seems like “Revive”: it forces the read function to consume the data, and then “Revive” it after use. So the main idea of Revive is to make it more ergonomic.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.