Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upRedefine Stream/Sink/AsyncRead/AsyncWrite/etc on top of Future #1365
Comments
Matthias247
referenced this issue
Dec 8, 2018
Merged
Move the descriptions of LocalWaker and Waker and the primary focus. #15
This comment has been minimized.
This comment has been minimized.
|
Having the core traits return futures directly are probably blocked on GATs (or all the way to trait Stream {
type Output;
type Next<'a>: Future<Item = Option<Self::Output>> + 'a;
fn next(self: Pin<&mut self>) -> Self::Next<'_>;
}I reintroduced |
This comment has been minimized.
This comment has been minimized.
I was afraid of that. I don't know yet what GATs exactly are but will try read up on them. Can you elaborate how a signature for a stream would need to look like in order to make it implementable by future composition as outlined above? I would say as long as that is not possible, we would maybe face the general issue that it's not possible to define abstractions around types that are implemented by async functions. And e.g if I built an async powered HTTP client, I can't built an interface/trait around it that I use for unit-testing an dependency-injection in other code.
You are right, most streams will only support producing a single item at a time. Supporting more would require internal queuing (which can easily be implemented through I think that aspect could be most cleanly solved by the type system, if reading from the stream produces a
I can see that some |
This comment has been minimized.
This comment has been minimized.
|
GATs are generic associated types, that's the feature that allows parameterizing the
This is where having GATs is useful, rather than consuming the stream and receiving it back once complete you can borrow the stream, and have that borrow last until the future is complete/dropped.
My use case was reading a stream of values from a radio peripheral in a microcontroller. By using pinning the stream can have an internal buffer that is written to by the radio using DMA, without pinning this buffer would need to be externally allocated and passed in as a reference (or heap allocated, but this was for a |
This comment has been minimized.
This comment has been minimized.
Ah, that makes sense. So the following wouldn't work, because without GAT we can't put the lifetime on the item? trait Stream {
type Output;
type Next: Future<Item = Self::Output>;
fn next(self: &'a mut) -> Self::Next<'a>; // Or is that Self::Next + 'a ?
}After reading GATs I understand it makes lifetimes more flexible, but I wasn't sure whether something is possible without them. It should be OK for this use-case if the lifetime of the returned Future is bound to the stream.
Ah, cool idea to represent it as a stream. I think however your requirement is more the typical pain point of an embedded system: One shouldn't dynamically allocate, and therefore ideally all buffers/etc. should be contained inside the type. However without e.g. const generics that's already painful, and even with I don't find the embedding approach super appealing (there's an endless forwarding of type parameters). |
This comment has been minimized.
This comment has been minimized.
|
I tried things out. I seems to be only possible to define this at the moment: trait Stream<'a> {
type Output;
type Next: Future<Item = Self::Output> + 'a;
fn next(&'a mut self) -> Self::Next;
}which is arguably not nice due to the lifetime which needlessly is on the trait. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
I discussed this with @erickt elsewhere and explained my thoughts. As the ecosystem stands, it's not possible to make this work without making all of these traits non-object-safe, so I'm not interested in pursuing this at the moment. |
This comment has been minimized.
This comment has been minimized.
|
One thing that come to mind with regards to |
This comment has been minimized.
This comment has been minimized.
|
@cramertj: would you mind sharing these publicly at some point? I guess that would be interesting for others too. The implications on the ecosystem are totally understood. Although the current changes from 0.1 already are lots of those smaller renaming things. And this would mostly be another. We can still have the old things around, with adapters to the new variants. This direction is possible to bridge. However it’s not possible to move future based thingies into the current signatures without costs (eg extra allocations) @seanmonstar: I think this should still be possible as all types that support read and write should also support a split method for getting individual halves in order to make them more flexible. Then you can get a future from the reader and one from the writer. |
This comment has been minimized.
This comment has been minimized.
|
@Matthias247 Oh sorry, I don't have a large writeup or anything, I just mean't we had discussed it. If there were a way to return a |
Matthias247 commentedDec 8, 2018
I think in the async/await world it might be preferable to redefine the traits and types like the ones mentioned in the title on top of
Futures, and e.g. move frompoll_something()signatures to methods that returnFutures.E.g.
The important reason for this, that it should be possible to implement those types on top of Futures as primitives, which isn't possible today.
As a motivating example I'm thinking about a MPMC channel, which can be built around some async primitives as shown below:
I'm not yet sure if the current state of async functions and traits allow all this (please educate me!), but I think this should be one of the goals that async programming in Rust should enable.
This kind of implementation is however currently not wrappable in a
StreamorSinkimplementation.We can't stuff this kind of code inside a
poll_xyzmethod of aStream/etc, since it requires temporaryFutures that all carry their own poll state to be instantiated in the task. ThoseFutures must maintain their state between calls topoll(). E.g. polling an asynchronous mutex the first time creates a registration in the waiting list, and dropping it cancels the registration. If we would try to use an asynchronous mutex inside apoll_nextmethod, we must always need to drop it beforepoll_next()returns, which would cancel the registration and never allow use to retrieve the mutex.I think the more general observation might be, that
Futures allow for persisting some additional temporary state per operation, and provide additional lifecycle hooks per operation (Futuregets polled for the first time, andFuturegets dropped) compared to the currentStreamdefinitions.Another side effect is that currently
Streams/etc must all support pinning. In Streams that returnFutures that wouldn't be the case, since theFutures get pinned. They might indirectly get pinned because the respectiveFutures might reference theStreamthrough a reference and a certain lifetime, but that relationship is already directly checked through Rusts normal means, and e.g. doesn't put special requirements regarding pinning on the implementation of the type. And often we wouldStreams being moveable after they have been partly consumed (e.g. a Tcp Socket), which is not allowed if they are pinned. Of course some of them might beUnpin, but this kind of change would allow moves again for allStreams.