.await and auto-deref

#1

I think this post by @HeroicKatora deserves its own thread, since the thread it was originally posted in is mostly filled with prefix-vs-postfix debate:

See also, from this GitHub issue:

I’m not sure about the interactions either, but it seems compelling that something with dot syntax should do auto-deref; I think this needs to be considered before stabilization.

12 Likes
#2

There’s one problem with the mechanism of auto-deref. Maybe a very good linting/fix-suggestions are also possible options instead of forcing the issue. So, awaiting takes its argument by-value. If we imagine it working similar to fn(impl Future) the auto-deref would, when applied, yield a &mut impl Future at some point, which gets dereferenced further into a future that we cannot move from since we only borrowed the place during dereferencing (keep in mind this will only happen when the initial value did not implement Future).

As noted, &mut impl (Future + Unpin) would allow the mutably borrowed value to serve as the future but is not selected as it is not the maximally dereferenced possibility. This means that most likely actual auto-deref is unfit for simplifying most uses of .await. Instead, a really good error message could be more helpful:

When trying to apply .await to something that is no future, Instead of:

error: only futures can be awaited

We could try to find out if this looks like it could be solved via auto-deref and if so issue this instead:

error: await can only be applied to a value that implements Future directly
  | let mut lock = mutex.lock().unwrap();
  | lock.await;
  |     ^^^^^^ trying to apply await to value of type MutexLock<_>
lint: try applying it to the reborrowed value instead
  | (&mut *lock).await
2 Likes
#3

It seems to me that adding in auto-deref after stabilization is backwards compatible, it will just allow more things to compile that previously didn’t.

To expand on this a little more, because Future::poll takes Pin<&mut Self> the auto-deref that happens here needs to be a pinning deref (presumably some trait DerefPinMut which doesn’t currently exist). In the example of a MutexGuard even with this auto-deref wouldn’t help because Mutex cannot provide the guarantees required of pinning. It might be possible to have a subset of the Mutex API that can support it, but that requires some design work.

It would be possible to instead have the auto-deref work for just DerefMut<Target: Future + Unpin>, but that seems to be just as much of a weird limitation. It would mean that taking the result of an async fn and sticking it in a Mutex still wouldn’t work, you’d need to Box the future inside that Mutex anyway to make it Unpin.

Autoderef also doesn’t help in other common scenarios like join! or select!, these wrap the passed future in some internal state which requires the type to directly implement Future.

With all those open design questions I agree with @cramertj that trying to have .await support auto-deref from the start shouldn’t be a blocker. It might be nice to expand it in the future as part of improving the ergonomics of pinning, but that needs someone to do the design work.

2 Likes
#4

The Pin<&mut Self> special casing is required for cases where Self: !Unpin, which should work regardless for poll but do (obiviously) not provide the safe construction and DerefMut implementation that is otherwise present. Also, note Pin::new is completely safe as long as the pointer target implements Unpin. That is we can trivially pin and thus implement Future for &'_ mut impl (Future + Unpin) which is precisely the case I tried to insinuated above with MutexLock.

The Deref would not have to guarantee it itself, the best option would be that the implementation of Future on the mutable reference along the way makes it possible to await (thus implicitely relying on Unpin instead). But consistentcy before new compiler weirdness, and as you said it should be forward compatible.

#5

We should look in general if we want auto deref for .keyword if that is something we want to do in the future. For example with .match we would probably don’t want auto dereferencing and use the normal match ergonomics.

1 Like
#6

I’m not sure that is technically accurate. Isn’t possible for someone to write an object that has both Deref and Future traits and the derefed value is a future?

My (possibly incorrect) understanding of deref rules, are that if .await doesn’t deref it will await the original future but if it gets added it will await the derefed one.

Of course, that doesn’t seem like something that is likely to come up in practice.

#7

If a type both implements a trait and derefs to another type that implements that trait the implementation of the outer type is chosen for method-auto-deref (playground). That means that if .await were to have consistent auto-deref added later then the behaviour wouldn’t change for such a type.

5 Likes
#8

Another thing to consider is autoref (the other direction) which is used for methods on the dot operator. I don’t think there’s any useful case where &?mut T would be impl Future where T isn’t, but it’s worth considering as another thing dot does.

2 Likes