-
Notifications
You must be signed in to change notification settings - Fork 641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type-safe futures and streams #458
Comments
Note: Just found that #462 would be probably avoided if the |
FWIW I feel "type safe" is the wrong name for this. That typically invokes thinking along the lines of "if you're not type-safe then you can store a I personally feel the cons outweigh the pros, and have yet to be convinced of this change. |
Maybe I have a different understanding of what "type safe" means. From what I understand, it also means "you can't store Well, I'm not convinced about "one perfect way of doing it" either. And I certainly don't want to throw away all the existing code. I believe both approaches could co-exist with their pros and cons. What I want is to explore possibilities of statically checked code. So to address your concerns, you mentioned three:
Could you, please, describe whether you see some problems with my suggestions for solutions? I'd love to solve it. :) |
Sure yeah, I'll dig into some more details. So it sounds like you've got object safety taken care of, but this definitely comes at a cost. There's an extra trait and more concepts to understand (just to be clear), but at a technical level it's solved. For ergonomics much of what I'm thinking is somewhat anectodal. We prototyped a system exactly like this (without object safety) where futures were all moved by value, and that's only one incarnation of the system! I find it difficult to articulate as to why it was so unergonomic, but in general Rust, while moving by default, typically favors borrowing in terms of ergonomics. Almost all idioms of the standard library heavily rely on borrowing to make methods, helpers, etc work out nicely. Tons of code in the ecosystem relies on borrowing in very colorful ways as well. In general it means that idioms are typically not geared towards lots of movement of ownership and ergonomics tend to suffer once movement is forced on everyone. Movement has also been explored a lot in other systems like rotor. I did not personally use rotor, however, so I can't comment myself. In general I'm all for type systems upholding invariants where possible, but I personally at least favor ergonomics over the "type system getting in my way". For example the For performance I'd recommend profiling pieces of code here and there written both ways. IIRC Hyper received a double-digit percent boost in performance moving from rotor to futures. I don't know whether that was movement related or runtime related, but it's at least one data point. |
Sorry, I was unable to reply sooner, I had a lot to do. Thank you for deep explanation! You made very good points. I'm still dreaming about solving everything and having a perfect code. :) But it seems like it makes sense to prefer borrowing at least for now. I've been thinking about other ways to solve this but they seem ergonomically worse. That being said, I'd very much like to improve implementation of Another thing that might be useful for some cases is equivalent of Of course for performance I'd definitely use profiling. I'm not concerned with performance too much, because it is important but not critical for me yet. But I can understand other people caring about it a lot. I think I'm gonna skip worrying about this too much right now but I'd like to revisit this in the future (no pun intended), when I'll have more time. Edit, one more thing: I feel a little bit sad every time I see (panicking) checks in code just to prevent memory bugs if programmer screwed up. So that's also one of the reasons I was exploring this. That is probably just my perfectionism... |
This reminds me a bit of what @dtolnay did with Serde. I believe it was in 0.9 when the serialization system changed to move a sentinel value to indicate when serialization was done. Personally, I really like the idea of the type system being used to ensure panic-free code. "In practice, this rarely happens" is the kind of thing people would say back in the dynamic programming world, and it usually ended up meaning you'd get screwed eventually. Of course if the performance cost of that approach is too great, maybe it's not feasible. |
FWIW I'd be very curious to hear about scenarios where this actually caused problems. I don't think in practice I've ever seen panics or behavior due to poll-after-future-is-done except maybe once or twice. I'm probably not the best data point though because I'm familiar with all the internals! An interesting data point is the proposed coroutine language feature, which uses |
It hasn't caused "problems" for me. Receiving This isn't a particularly big deal for me though. The more common mistake I make is to accidentally not call the inner Future / Stream's |
I had an idea how to get performance of |
Here's a scenario I recently encountered where linear typing would have helped: Implementing a handshake protocol. This is what I wanted to implement: There's a The actual handshake is performed via How should the corresponding async API look like? Instead of synchronously returning a So in my current implementation, the Say there is a read/write wrapper type When I saw As for the performance cost: And if "need to rewrite stuff" is an actual blocker, I'd happily contribute some of my time. |
I'm personally pretty tempted to close this issue. I think we've got lots of data which points in favor of where "in the small" a by-self API can provide an ergonomic and statically checked win. Once futures start scaling up, however, this becomes much less clear and I think by-value @AljoschaMeyer it's true that |
That function can accept The reverse (using owned |
That's not really the point, though. The |
Well, I personally can't imagine either of those - that implementations of |
I'm going to share my idea, so it won't be lost, although I doubt someone will want to actually implement it (but who knows?). So the idea was to take trait Slot {
type Item;
fn get_mut(&mut self) -> &mut Self::Item;
fn take(self) -> Self::Item;
}
struct OptionSlot<'a, T: 'a> {
// This always point at Option containing Some
option: &'a mut Option<T>
}
impl<'a, T: 'a> OptionSlot<'a, T> {
fn new(option: &mut Option) -> Self {
match *option {
Some(_) => OptionSlot { option },
None => None,
}
}
} The Then the caller is responsible for checking invalid states using The obvious downside would be seemingly complicated API and lack of object safety (It may be possible to restore it somehow, but I didn't figure it out yet.) I'd be very happy to see someone figure it out. I don't have much time for this. :( |
This inspired me to do a valuable_futures crate (I think the name is funny, no?). What it does is it has another There is also a wrapper that passes both mutable pointer and by-value state. I have a lot of code should be simpler if designed this way. (If you have some suggestion for naming this thing, please open an issue in that repository) I'll try it on some real futures soon, but here are examples:
While the code is approximately of the same length and complexity, there are few tiny but imporant things:
(you might question why It's just quick prototype to try and gather some comments. Docs and implementation for Stream will be done later. |
@tailhook Do I understand correctly that you reinvented the wheel or am I missing something? |
Didn't see that. Have you given a link? (also not at crates.io) As I see now:
(well, also usage examples would be useful) |
In the top post in this thread 🤣. It's PoC code.
|
I added most of the existing
|
I've spent some time writing |
I think something like linear types rust-lang/rfcs#814 would be helpful. |
@omni-viral |
@Kixunil exactly the same 😄 |
I'm going to go ahead and close this out, given that we've recently completed the 0.2 revamp and it's pretty clear that this change is not in the cards. |
As discussed in #206, current poll API of futures and streams takes them by mutable pointer. That would suggest those are always valid after call to
poll()
, which is not the case, sincepoll()
shouldn't be called after future resolves.Consuming self and returning it optionally would make more sense from type-safety perspective. It would also simplify implementations, especially when
Future::Item
is notClone
and must be stored in the future itself before resolving. (The current solution is to have it insideOption
and unwrap it each time.)I've made a demonstration example of some concepts.
Pros of type-safe futures/streams
poll()
is called on resolved future.Stream::Error = SomeError
. Stream that doesn't fail fatally is represented withStream::Error = !
.Cons of type-safe futures/streams
UnsafeFuture
trait as demonstrated in my example. Someone might consider that solution ugly.poll()
decreases if futures are big and compiler didn't inline, since it involves copying due to pass-by-value. This might be solved with enough#[inline]
s. Also, I've invented a hack to get pass-by reference, yet type-safe behaviour. I didn't implement it yet and didn't publish anything about it anywhere. I probably will some day. It doesn't look very nice/intuitive though. :(Motivation
I believe the type-safe futures might be useful in cases where safety is more valued. E.g. financial applications, mission-critical software, etc.
As demonstrated in my example, the type safe and type unsafe futures/streams can co-exist using
Glue
wrapper. So maybe the best solution is to have both version and use the best tool for the job. I'd like to explore this topic and know what do you think about it.The text was updated successfully, but these errors were encountered: