futures::sink::Sink, there are
start_send fns. The documentation specifies that a successful
poll_ready call is required before
start_send can be called.
So, with rust, we can enforce that at the type system level, by not adding the
start_send to the trait itself, but instead returning a handle that implements
poll_ready. This would guarantee the proper order - which is much better compared to giving users the possibility to make a mistake.
My question is - why wasn't it done? My suspicion is the runtime overhead of having a handle value was deemed to be too high. I also have other guesses, but I'd love to hear from the people that participated in the design loop of that interface!
Largely because you aren't supposed to use
futures::sink::Sink directly, and to make it easier on the implementor (they only need to implement a single trait with 1 type). Your supposed to use these types,
all of which are futures that you could await that wrap the
Sink trait and call into it.
This should probably go in the Sink docs?
Thanks for the response!
I feel like from an implementor point of view, that design seems a lot better too. I'm actually coming from an implementor end here.
My current issue is, I think, that I hit the design limitations.
While I can understand the idea of having a trait that's not supposed to be used directly, but, in that sense, what we have here is more of a hack. The reason I think so is if there were any performance gains from designing the trait itself without the handler values, the resulting architecture requires even more overhead with the additional features.
To do this, you would probably need to create an associated type on the sink trait that represents the type you call
start_send on, but since we do not have GAT this would mean that the returned type can't borrow from the sink, so now you need reference counting inside the sink... And now you need a Mutex if that type should be able to modify something inside the sink...
Ah, yeah, we hit the GAT again there. This must be the reason. Or at least, it's the reason it can't be currently implemented as I described. Going through
Mutex is obviously far worse than what we now...
I think the expected interface here would be:
fn poll_send( self: Pin<&mut Self>, cx: &mut Context, item: Item )
-> Poll<Result<(), Self::Error>>
But now what happens if the buffer of the
Sink is full and the underlying connection is giving back pressure? Drop the
Often enough the error type will be
std::io::Error, but that has no way to hold the item to give it back to you, and async API's are not supposed to return
WouldBlock. They are supposed to return
Pending, but if there is no place to store
Item, we run into trouble.
Thus a method
poll_ready is provided which can return
Pending without having consumed an
This is just my imagination. I didn't invent this API, nor looked up discussion about why it was made this way, but I can see this being the reason.
If you really want to safeguard here, you could return a
Poll::Ready< Result<SendNonce, Self::Error> > from
and require the client code to pass the
SendNonce back into
start_send. But probably that seems like overkill since indeed more convenient API's are provided for daily use. You only need to use this API if you are using a Sink from within another
I was thinking about something like:
self: Pin<&mut Self>,
cx: &mut Context
) -> Poll<Result<FnOnce(item: Item) -> Result<(), Self::Error>, Self::Error>>;
But again, see In futures::sink::Sink why is order of calls not enforced at type system level?
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.