Open nikomatsakis opened 3 years ago
Possible example: when you call an async function in JS, it begins executing immediately, and executes concurrently, whereas Rust futures are lazy.
Some other topics that might be worth covering here:
async_hooks
API. Rust on the contrary does not provide a stable tracing API out of the box, and does not allow for monkey-patching. This means the experience is different for both tracing providers and consumers.Hi y'all! ❤️ Coming from JS here are some things that made an impression to me as a beginner.
🔁 I didn't know how to imagine what a runtime until someone said it's like a glorified while loop and that the special macro re-writes my code into something else.
⛔️ Handling errors in Tide was weird, for example if I wanted to upload an audio file, I'd have to do some parsing and writing to the filesystem. Usually I'd slap a question mark at the end of each function that returns a result, but when I wanted to turn those into meaningful errors I got confused. I still don't know how to properly do error handling so I use dyn Error result types everywhere if many errors can happen in a function 😬. Although the compiler catches so many things that I rarely need a debugger so there's that.
These took the longest to figure out. My dad and I have been coding an audio player server for fun every Saturday (🥳) for a while and it tripped us up when we wanted to do ƒancy hot-reloading. It was very rewarding to learn that you can shoo off the borrow-checker by lighting a candle and burning some incence in front of a portrait of Esteban, or just using .clone() everywhere. Oh, and passing closures around is really hard.
Some things that I would like to be improved in Rust, that I find useful in JS:
Combinators
Promise.all
, Promise.join
, Promise.allSettled
use futures::join; join!(a(), b(), c())
, my IDE (IntelliJ) has no idea what is inside of the macro brackets so there is no instant type checking.Pin'ing promises
Async closures
How do promises interact with threads?
Async iterables (for await of
)
A standard runtime that will be supported long into the future.
There's a subtle difference in JS between returning Promises or awaiting them before returning. Suppose createPromise
creates some Promise:
async function returnSomething() {
return createPromise();
}
async function returnSomething() {
return await createPromise();
}
The two snippets lead to different results. This article explains the difference in detail. It can be very confusing even for experienced JS devs. Rust works differently, which can bring even more confusion. For example, the code below panics because I don't .await
return_number()
inside return_call_async
function.
use futures::executor::block_on;
async fn return_async_1() -> usize {
return 1;
}
async fn return_call_async() -> usize {
// return return_number().await;
return return_number();
}
fn main() {
let s = block_on(return_call_async());
print!("{}", &s);
}
The provided error message is very uninformative and could be improved (e.g. explain why I can't do it this way? Is it possible by returning some other type? Should I return impl Future
or something like that? Is returning usize
from return_call_async
correct? ).
error[E0308]: mismatched types
--> src/main.rs:8:12
|
3 | async fn return_number() -> usize {
| ----- the `Output` of this `async fn`'s found opaque type
...
8 | return return_number();
| ^^^^^^^^^^^^^^^ expected `usize`, found opaque type
|
= note: expected type `usize`
found opaque type `impl futures::Future`
help: consider `await`ing on the `Future`
|
8 | return return_number().await;
| ^^^^^^
error: aborting due to previous error
When learning Rust a lot of emphasis is, rightly, placed on ownership/references and mutability. However, there are some marked differences in data types that are easily missed.
The various integer types/sizes is one.
The other is vectors versus arrays. It's all too easy to assume arrays are what you want because of the JS name similarity.
In JavaScript, cancellation is handled with AbortControllers + AbortSignals. In Rust it is handled by not calling poll and the Drop trait. In JavaScript some APIs don't take an abort signal and you end up needing to wrap them: https://gist.github.com/jakearchibald/070c108c65e6db14db43d90d1c3a0305 In Rust, all Futures can be cancelled. In JavaScript cancellation is propagated via exception from bottom to top (by bubbling exception) instead of being propagated from top to bottom (by futures dropping their children) as it is in Rust.
Maybe it would help JavaScript users to have examples that show converting cancellation patterns in JS into Rust.
async function do_the_thing(data, signal) {
const thing = create_a_thing(data);
try {
const res = await use_thing(thing, signal);
return res + 2;
} finally {
thing.cleanup_the_thing();
}
}
I think a lot of people coming from js look for Promise.all/Promise.allSettled/Bluebird.map(…, { concurrency: …})
. It took me quite a while to realize that I needed to make a Vec<Future>
into a stream, and even longer to find FuturesUnordered/BufferUnordered, and even then they're more awkward than I expected. The join/select macros are a confusing distraction for a lot of people, myself included.
Here was my thrashing attempt at understanding how to make outbound http requests with bounded concurrency: https://twitter.com/jacobrothstein/status/1212892911546683394, and critically for this wg, I ended up giving up on async and instead used rayon for this project: https://twitter.com/jacobrothstein/status/1213248527079313414
A little analysis: The futures crate has a lot of good ideas, but also some less-than-useful ones, and very few of them are documented in a didactic/tutorial manner, and the naming could stand to be improved for discoverability. There's no "so you want to iterate over this Vec<Future>
/IntoIter<Item=Future>>
in the order they resolve? Try FuturesUnordered. Want to bound the concurrency of that task? Try BufferUnordered. Want to do some stuff with streams? You'll need to pull in StreamExt. You don't need to do this with Iterator because it's in the std prelude."
Async-std and tokio have different approaches than those in futures, and it's not obvious why/when you need to reach for the futures crate (or, even more confusingly, futures-util / futures-lite, which also have differences). It's especially not obvious that a vec of futures needs to be turned into a stream in order to do async stuff with it, since in javascript there are lots of apis that operate on an array of promises without making it into a special thing. I'm much more familiar with async-std than tokio, but I don't believe async-std currently has any equivalent for FuturesUnordered or BufferUnordered, which leads people to try other solutions that don't work as well, like trying to fold a vec of futures into one giant join
-ed or race
-ed future
Some things that I was struggling to understand after working with JavaScript for a long time:
Mutex
, RwLock
, Arc
etc. required like in async Rust. No need to worry about something not being Send
or Sync
.async
function and not handling the returned future properly doesn't drive the future to completion. Nothing happens. async
code in JavaScript just runs.|| async {}
and async || {}
? Why are async closures unstable, but moving the async
keyword to the right side magically works?Box<dyn Fn() -> Pin<Box<dyn Future<Output = ()>>>>
?! Why is Pin
needed when working with futures? What problem does it solve? Why do I need it, and sometimes I don't?std::sync::Mutex
over, say tokio::sync::Mutex
? Blocking code should be avoided at all costs, so why is it fine to use std's Mutex? This sums it up for me: https://github.com/rhmoller/blobs-and-bullets/blob/master/src/engine/image_future.rs
I just wanted to do the equivalent to this.
const loadImage = src => new Promise((resolve, reject) => {
const img = new Image();
img.onload = resolve;
img.onerror = reject;
img.src = src;
})
It took me forever to figure out. And now that I look at it a year later I can't remember how it works. (I haven't been writing Rust since)
BTW the above is not to dunk on Rust. I overall like the language and would like to pick it up again sometime.
I recently bounced off rust async after thinking about using it for the 4th or 5th time over the years.
I'm somewhere between Alan and Barbara, but I remember before I was Alan and what pain I felt then. When Promises showed up in JS, I largely avoided them, hooked in via my well worn callbacks. This was rooted in a few concerns, some were performance reasons, but mostly it was interoperability with other code I had. In these days, using try/catch
was mostly scoped around the very few things you knew could throw, to keep them from being uncaught and causing crashes, and most error handling happened via so called "error back" callbacks with the first argument being the potential error.
My early encounters with JS promises were learning a lot of interesting rules about how if you .then(thenFn, catchFn)
it was different than .then(thenFn).catch(catchFn)
and there was try/catch
as a way to handle async errors, and now synchronous errors could be thrown and because they happened inside of a promise they would be considered an "uncaught rejection" and not ever bubble up to the user in Node.js until recent versions.
As I dove into that world of using Promises, I found there was a few different gotchas when converting callback code. Subtleties like how return await somePromise;
is redundant in some cases and required to make a try/catch work as expected in others. Mix that in with various issues with where libraries would only work with some promise implementations, etc.
The ecosystem has come a long way in those years since Promises landed in Node.js, I now write Promise first code for 99% of my JS at work. If a library has a promise interaction I'll use it over a callback one for most things.
I've been a rustacean since just before 1.0, was lucky enough to attend RustCamp, and generally been hacking in rust for side projects for years and years now. I've written a lot of CLIs, a few art projects with my own rendering engine, and my own game engines. So I'm not coming at Rust fresh, I remember try!() and how often I used to have to specify lifetimes on everything. I've even taught local workshops and given talks about Rust.
I get vibes of the early JS promise when I start diving into Rust async. Also the amount I have to learn to start modifying my code confidently feels massive. Here's an ordered list, which is akin to some others on here
async-std
vs tokio
vs others and how if I just arbitrarily pick one, how much pain am I setting myself up for? async-std
has some features that talk about tokio
support, libraries say they're for async-std
specifically, does that mean I can't use them with libraries that are built for tokio
or can I? I wanna hook my cart up to the horse that's going to win the race, increasing my chances of finding useful libraries and not having to convert all my code to a different ecosystem in a year because I bet wrong.std::sync::mpsc::channel
for a client of some sort that uses threads internally. How do I take common thread world patterns and convert them? Both in the slapdash equiv of throwing .unwrap()
and hoping as well as the proper wrapper.async
and use the .then().catch()
interfaces mentioned above to keep from "infecting" the rest of the project.My process that leads me to rust async and maybe part of why I bounce off:
structopt
and serde
.std::thread::spawn
and std::sync::mpsc::channel
at any connections I need to manage, such as TCP, serial port, my own custom hardware/protocol, etc.async/await
.async-std
. Note I actually ran into a compile cache issue when I first added it to my deps, causing the linker to throw a bunch of errors until I did a cargo clean
and tried again. Was unable to reproduce it, can't say it is "rust async" related, just a bump that took 20 minutes that could have been used on making this side project move foward.serialport
has two async implementations, one is tokio
based, the other is mio
based which maybe means out the gate I already made a bad choice with async-std
since it isn't either of those. Spend another night reading these async wrappers to try to understand how they're laid out and why.try_recv
based loop, did a little polling, and went on with my life having a working system, a little sad I still don't grok rust async and figure maybe I'll have to make a specific project to learn it sometime in the next few months... but maybe after the ecosystem has settled more.I wrote this as a stream of consciousness so please forgive some transposed words and weird phrasing where my brain jumped about. I would like to have a grasp on rust async, maybe someday I'll have picked up enough tangentially and the ecosystem will come far enough that it does't seem like a huge lift to just try it out in a project.
Edit: Actually, now that I think more on this... I can think of two more things that make it harder for me personally to move to Promises as well:
Yet another story:
I'm coming from a Java background with some familarity with C/C++, but am working with TypeScript/Node.js for >3 years now on a daily basis.
What really bugs me in Node.js is the thing that creating a Promise will automatically add it to the Event-Loop. This means that every time you do that (either explicitly with new Promise
or implicitly with async function
) and do not immediately either try...catch
or .catch
it, you basically created a ~UnhandledPromiseRejection~ landmine.
That's why I absolutely love Rust "explicit polling" approach. Coming from the problem mentioned above in Node.js lead me to always explicitly calling smol::Task::spawn
whenever I want to execute something "in parallel".
Due to this approach I did not understand the recent fuzz about "Drop
+ Future considered harmful" at first:
smol
to call smol::Task::spawn
within smol::Task::spawn
(as opposed to Tokio
AFAIK)spawn
for parallel tasks + channels for output + Drop
" does not lead to the problem in the first placetokio
and async-std
Brief summary
Alan is accustomed to JavaScript promises. He has design patterns in mind that don't work in Rust; he also gets confused by specific things around Rust futures. What are they?
Optional details