This article shows how it's hard to design and implement a novel and powerful language like Rust. You have to understand "Haskell concepts" like GADTs and then try and find a way to hide them from the users of the language.
> The only real drawback here is that there is some performance hit from boxing the futures – but I suspect it is negligible in almost all applications. I don’t think this would be true if we boxed the results of all async fns; there are many cases where async fns are used to create small combinators, and there the boxing costs might start to add up. But only boxing async fns that go through trait boundaries is very different.
you'll end up with boxes of boxes of boxes. It kinds of makes the feature something for the outermost abstraction layer or you end up with boxes of boxes of boxes.
> And of course it’s worth highlighting that most languages box all their futures, all of the time. =)
It's also worth highlighting that most of those languages (1) have a GC that makes boxing a much cheaper operation than in Rust, and (2) those languages aren't advertised as "low-level" languages and that justifies implicit boxing as a trade-off.
---
I feel that async shouldn't really be special but rather just build on other features that stand on its own. GATs and impl Trait in Traits seem reasonable. But being able to do dynamic dispatch on trait methods that return a type that isn't `Sized` seems like a tough problem to solve, and the blog post didn't managed to convince me that implicit boxing is the right approach here. AFAICT, we need either some kind of boxing, or better support for unsized rvalues or similar. I think I would be more comfortable with "sugar" for boxing if the caller of the trait method were in control of where exactly the result is allocated (not necessarily a Box), but that calls for some kind of placement syntax.
> most of those languages (1) have a GC that makes boxing a much cheaper operation than in Rust
Why is that? I would intuitively think it is the other way. (Is a malloc/free pair not cheaper than an allocation on the GC heap + collecting its garbages?)
In that particular example you won't get boxes of boxes- `baz` gets instantiated for each impl of `Baz`, so the call to `self.foo()` is resolved statically and the return type is fully known.
Kotlin coroutines are just async/await with different syntactic defaults. That is, when you call a `suspend fun` it is awaited by default, and if you want to get a promise/future/etc from it instead that's when you write an extra annotation.
Rust went the way it did syntactically because annotating the await points lets you see them when reading a program, and possibly out of familiarity.
Rust went the way it did implementation-wise (stackless rather than stackful) because it's more efficient.
Rust doesn't use M:N threading (coroutines) because it was found to be slower than 1:1 threading in practice. Async/await avoids allocation of stacks entirely and can therefore be faster than both 1:1 threading and M:N threading.
In this context the hazard is that you can break the clients of your API by changing only the implementation (the function signature stays the same) because the compiler automatically infers whether the resulting future is Send or not based on the implementation.
It's just a fancy way to say "risk of breaking backwards compatibility". Semver is Semantic Versioning, a versioning scheme that all Rust crates are expected to use.
In that context itmeans a situation where a developer of a library could accidentally break semantic versioning guarantees (no breaking change except between major versions) without noticing it. Or something that would require a major version update for some small change that shouldn't need one.
I grew up when OOP was the shit. Everyone wanted to add OOP to every language. C++, ObjC then Java.
I absolutely hated, not because OOP is bad, but because they never really manage to blend it just right with static typing. But I was the odd one. Everybody else seemed to love it and use it for everything.
I was lucky enough to outlive this OOP craziness.
Now I am seeing the same craziness with “a sync” and continuations and wonder if I will be lucky enough to outlive it to.
It's not really all that crazy. Sure, if you consider languages like C#, it's much more verbose than those, but that's because Rust tries to deal with async without garbage collection. This isn't a typical code you would write in Rust, in fact, in this case, this type was generated by a library.
Dealing with this "ridiculous" type step by step:
Future<Output = User>
This is a future. It returns an user.
dyn
However, because the exact `Future` implementation can vary, dynamic dispatch is necessary. Use of `dyn` keyword explicitly says that a vtable is necessary.
+ Send
Rust is supposed to make programs free from data races. It's very much possible to have objects which aren't allowed to change the thread they are in. For instance, `Rc<T>` (single-thread reference-counting pointer) cannot be sent to other threads due to it using non-atomic reference count.
In this case, using `+ Send` says that whatever future this method returns must be possible to send to other threads. This prevents use of objects like `Rc` within the futures, and allows the future to be used in multi-threaded executors.
+ '_
The method borrows `self` (as seen by the use of `&self`). Saying `+ '_` means that the future should borrow `self`. This is necessary due to lifetimes.
Box<...>
Because dynamic dispatch types (note `dyn` before) can have varying sizes (one future can be smaller, another one can be bigger), it's necessary to wrap them somehow so that they could be represented in memory. In this case, the simplest heap-allocation type (`unique_ptr` in C++) is being used. There are other options, like `Arc<...>`, but in this case, `Box` is sufficient.
Pin<...>
Futures can store pointers to its local fields. This is going to cause issues if the allocation could be moved somewhere else in the RAM - the future would be moved to the new location, but the pointers would still refer to RAM in the old position. Using `Pin` prevents the user from moving the contents of `Box` to another place in the RAM.
This is a silly argument, of course de-sugaring language leads to a less nice type signature. C functions compiles down to assembly and C++ or Java hide vtables and such when you do method calls.
If you go lower in the abstraction level you get more complicated mechanics, and that says nothing over the validity of the high level semantics.
This example from the article was to demonstrate a type that was more complex than would be acceptable in day-to-day use, so they agree with you here, it would be crazy to make that required to use the feature.
Java has always been an object oriented programming language. A class is a basic construct of the Java language. It wasn't added into Java. People still use classes. All the major new programming language have OOP features, you can create instances in Swift, Kotlin, Go and Rust for example.
async language features are just inline co-routines. Inline co-routines are a useful concept. It's a single tool in a big toolbox of other tools and not every problem requires the same tool, just like OOP features. It's just more stuff you can use if you want. If you don't want it, you don't have to use it, you can just write Haskell or OCaml without using objects and not worry about it and you won't have to worry about how long you live.
> Now I am seeing the same craziness with “a sync” and continuations and wonder if I will be lucky enough to outlive it to.
Maybe you will. It's an attempt to address shortcomings of shared memory, just like borrow checker. Once languages start moving away from shared memory to actors, there will be no need for any of it. But it could take a long time, the industry dug itself way too deep into the whole shared memory world.
> Everyone wanted to add OOP to every language. C++, ObjC then Java.
By mentioning the languages after that comment, was it your intention to imply that OOP were added to them? Because to my knowledge, all three were OOP based from the very beginning.
Please comment with a source if you feel I’m wrong on saying any of those were OOP from the beginning. I (as well as others I’m sure) would enjoy the learning opportunity.
[+] [-] tuukkah|6 years ago|reply
[+] [-] pjmlp|6 years ago|reply
"Confessions Of A Used Programming Language Salesman, Getting The Masses Hooked On Haskell"
https://www.researchgate.net/publication/237445028_Confessio...
[+] [-] fluffything|6 years ago|reply
In a situation like this:
you'll end up with boxes of boxes of boxes. It kinds of makes the feature something for the outermost abstraction layer or you end up with boxes of boxes of boxes.> And of course it’s worth highlighting that most languages box all their futures, all of the time. =)
It's also worth highlighting that most of those languages (1) have a GC that makes boxing a much cheaper operation than in Rust, and (2) those languages aren't advertised as "low-level" languages and that justifies implicit boxing as a trade-off.
---
I feel that async shouldn't really be special but rather just build on other features that stand on its own. GATs and impl Trait in Traits seem reasonable. But being able to do dynamic dispatch on trait methods that return a type that isn't `Sized` seems like a tough problem to solve, and the blog post didn't managed to convince me that implicit boxing is the right approach here. AFAICT, we need either some kind of boxing, or better support for unsized rvalues or similar. I think I would be more comfortable with "sugar" for boxing if the caller of the trait method were in control of where exactly the result is allocated (not necessarily a Box), but that calls for some kind of placement syntax.
[+] [-] likeliv|6 years ago|reply
Why is that? I would intuitively think it is the other way. (Is a malloc/free pair not cheaper than an allocation on the GC heap + collecting its garbages?)
[+] [-] Rusky|6 years ago|reply
[+] [-] The_rationalist|6 years ago|reply
[+] [-] steveklabnik|6 years ago|reply
There's also https://www.reddit.com/r/rust/comments/6zy8hl/kotlins_corout...
One tough part about digging through history here is that designs change over time...
[+] [-] AlexanderDhoore|6 years ago|reply
RustLatam 2019 - Without Boats: Zero-Cost Async IO
https://www.youtube.com/watch?v=skos4B5x7qE
[+] [-] Rusky|6 years ago|reply
Rust went the way it did syntactically because annotating the await points lets you see them when reading a program, and possibly out of familiarity.
Rust went the way it did implementation-wise (stackless rather than stackful) because it's more efficient.
[+] [-] pcwalton|6 years ago|reply
[+] [-] skybrian|6 years ago|reply
[+] [-] dunkelheit|6 years ago|reply
[+] [-] comex|6 years ago|reply
[+] [-] littlestymaar|6 years ago|reply
[+] [-] noncoml|6 years ago|reply
I absolutely hated, not because OOP is bad, but because they never really manage to blend it just right with static typing. But I was the odd one. Everybody else seemed to love it and use it for everything.
I was lucky enough to outlive this OOP craziness.
Now I am seeing the same craziness with “a sync” and continuations and wonder if I will be lucky enough to outlive it to.
This is plain craziness.[+] [-] GlitchMr|6 years ago|reply
Dealing with this "ridiculous" type step by step:
This is a future. It returns an user. However, because the exact `Future` implementation can vary, dynamic dispatch is necessary. Use of `dyn` keyword explicitly says that a vtable is necessary. Rust is supposed to make programs free from data races. It's very much possible to have objects which aren't allowed to change the thread they are in. For instance, `Rc<T>` (single-thread reference-counting pointer) cannot be sent to other threads due to it using non-atomic reference count.In this case, using `+ Send` says that whatever future this method returns must be possible to send to other threads. This prevents use of objects like `Rc` within the futures, and allows the future to be used in multi-threaded executors.
The method borrows `self` (as seen by the use of `&self`). Saying `+ '_` means that the future should borrow `self`. This is necessary due to lifetimes. Because dynamic dispatch types (note `dyn` before) can have varying sizes (one future can be smaller, another one can be bigger), it's necessary to wrap them somehow so that they could be represented in memory. In this case, the simplest heap-allocation type (`unique_ptr` in C++) is being used. There are other options, like `Arc<...>`, but in this case, `Box` is sufficient. Futures can store pointers to its local fields. This is going to cause issues if the allocation could be moved somewhere else in the RAM - the future would be moved to the new location, but the pointers would still refer to RAM in the old position. Using `Pin` prevents the user from moving the contents of `Box` to another place in the RAM.[+] [-] dtech|6 years ago|reply
If you go lower in the abstraction level you get more complicated mechanics, and that says nothing over the validity of the high level semantics.
[+] [-] DougBTX|6 years ago|reply
This example from the article was to demonstrate a type that was more complex than would be acceptable in day-to-day use, so they agree with you here, it would be crazy to make that required to use the feature.
[+] [-] rosybox|6 years ago|reply
async language features are just inline co-routines. Inline co-routines are a useful concept. It's a single tool in a big toolbox of other tools and not every problem requires the same tool, just like OOP features. It's just more stuff you can use if you want. If you don't want it, you don't have to use it, you can just write Haskell or OCaml without using objects and not worry about it and you won't have to worry about how long you live.
[+] [-] continuational|6 years ago|reply
[+] [-] ht85|6 years ago|reply
[+] [-] jdub|6 years ago|reply
[+] [-] Rusky|6 years ago|reply
[+] [-] zzzcpan|6 years ago|reply
Maybe you will. It's an attempt to address shortcomings of shared memory, just like borrow checker. Once languages start moving away from shared memory to actors, there will be no need for any of it. But it could take a long time, the industry dug itself way too deep into the whole shared memory world.
[+] [-] jsjohnst|6 years ago|reply
By mentioning the languages after that comment, was it your intention to imply that OOP were added to them? Because to my knowledge, all three were OOP based from the very beginning.
Please comment with a source if you feel I’m wrong on saying any of those were OOP from the beginning. I (as well as others I’m sure) would enjoy the learning opportunity.