4 ms·
I had not seen that particular example, thanks for providing the citation. Perhaps I'm just being stubborn here, but allow me a moment. Isn't the core issue t
by couchand 7y ago
I had not seen that particular example, thanks for providing the citation.
Perhaps I'm just being stubborn here, but allow me a moment. Isn't the core issue there that the executor requires the future to have a lifetime of 'static? Aaron explains it:
> ...because they are tossed onto executors like thread pools and hence not tied to any particular stack frame.
It's surely a huge undertaking, but wouldn't a more nuanced lifetime analysis solve the underlying problem here? I don't think the issue is that any other way to write this example is unsafe, it's that it's running into a limitation of the borrow checker.
What's required of an asynchronous borrow? The same thing required of any borrow: exclusivity. Sure, the async/await lets you write code that "looks synchronous", and gives the lexical cues the compiler needs to enforce the borrow. But does that mean it's the only way? The counterexample in Aaron's article even relies on async/await! That makes me highly suspicious that the need for it was taken as a given and never sufficiently challenged.
The core issue in that example is that the borrow needs to be held until some point in the future that can't truly be determined lexically. That says to me that it's not a borrow in the traditional Rust model. It is absolutely a useful borrow to check for, but why go about it backwards by only allowing such chronological borrows in such narrow circumstances?
Moreover, the example isn't realistic. The model that should be used for reading from a socket is a stream.
> The return type of an `async fn` is an `impl Future`.
Well, it is and it isn't. For instance, it appears that one can't await on a plain function that returns an `impl Future`...
- steveklabnik 7y ago> Isn't the core issue there that the executor requires the future to have a lifetime of 'static? Yes, and no. That is one of the factors. Another, and IMHO, bigger factor is that the lifetime needs to be 'static only as the task starts to execute. This is why Pin, etc, came into existence. This is due to self-referentiality. > It's surely a huge undertaking, but wouldn't a more nuanced lifetime analysis solve the underlying problem here? Yes, we're back in the realm of "open research question." > What's required of an asynchronous borrow? The same thing required of any borrow: exclusivity. Most borrows are not exclusive. > That makes me highly suspicious that the need for it was taken as a given and never sufficiently challenged. With respect, it is just as easy for me to say "This makes me highly suspicious that you have not investigated how all of this works and that's why you think it's easy." > The core issue in that example is that the borrow needs to be held until some point in the future that can't truly be determined lexically. Rust's borrows have not been lexical since last year. That's fine, of course! It's not like getting into this stuff is easy. But the team working on this has a ton of experience; multiple PhDs, etc, etc. This is the most investigated feature of Rust ever. Again, yes, maybe it is possible, but it's not within the realm of a "just". Additionally, none of any of the stuff in this comment this solves the original issues with do notation. > it appears that one can't await on a plain function that returns an `impl Future`... Sure you can: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=d35fb7bf16d85fdf38c8ef2128b8d824 https://play.rust-lang.org/?version=nightly&mode=debug&editi...
- couchand 7y ago> the lifetime needs to be 'static only as the task starts to execute. It would seem the lifetime needs to be coerceable to the lifetime of the executor, but as far as I can tell never really should need to be 'static. > Most borrows are not exclusive. Sorry, I meant, exclusive mutability. > Rust's borrows have not been lexical since last year. As I understand it, the borrows are not lexical in the classic sense of lasting until the end of the function, but they are still determined completely lexically, correct? Here the async/await syntax is provide lexical cues regarding the sequencing of asynchronous code, which is how it helps the borrow checker, right? > But the team working on this has a ton of experience; multiple PhDs, etc, etc. Of course. You've all done a great job producing a solid language! I'm not questioning the technical chops. And I don't think I said "just", I appreciate the scale of the problem. I'm just arguing about a tradeoff that I have not been able to find much discussion of. It's great to improve the ergonomics of the language. We need more people to want to use it! Is it possible that this is going too far? It sounds like it introduces an awful lot of magic.
- steveklabnik 7y ago> I'm not questioning the technical chops. No worries then; a lot of people have made very similar statements in the past, and that's the place they're coming from. > I'm just arguing about a tradeoff that I have not been able to find much discussion of. I am still not 100% sure what exactly it is you're talking about, at this point, to be honest. I think it's "why do we need async/await and not just write manual futures". I've pointed out several things. Is there something else? > It would seem the lifetime needs to be coerceable to the lifetime of the executor, but as far as I can tell never really should need to be 'static. Some executors, like Tokio, are mult-threaded with work-stealing. A task (chain of futures) may start executing on one thread, be waiting, and then get moved to another thread before continuing execution. > the borrows are not lexical in the classic sense of lasting until the end of the function, but they are still determined completely lexically, correct? I am not totally sure what you mean by "completely lexically" here. It is true that NLL means that borrows can exist for only part of a lexical scope, but not more than a lexical scope. Or rather, the "more than" part is already taken care of by lifetimes anyway. > Is it possible that this is going too far? It sounds like it introduces an awful lot of magic. Most programmers don't really mind magic as much as you'd think. I mean, sure, in some sense, it's a lot of magic, but the result is that instead of writing use std::future::Future; use std::task::{Context, Poll}; use std::pin::Pin; struct Five; impl Future for Five { type Output = i32; fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> { Poll::Ready(5) } } fn returns_five() -> impl Future<Output=i32> { Five } I write async fn returns_five() -> i32 { 5 } And, the intuition that "async fn returns an impl Future that I don't have to implement" is simple enough and accurate enough that it feels like even less magic. In practice, the people who have been writing tons of networking code don't see it as too much magic; they see it as a significantly nicer way to write the code they already have to write. The ergonomic difference is so large, that the delays in shipping have caused fairly significant community strife and pressure. It is possibly too hyped. Writing manual futures is so, so, so painful.