Hacker News new | past | comments | ask | show | jobs | submit login

I used to be a huge fan of async/await in the JS world, until I realized that we were tethering a very specific implementation to new syntax, without adding any clarity beyond what a coroutine and generator could do. eg `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

I came in not wanting to see rs go down the same path. The idea of adding something that looks like it should be a simple value (`.await`) seemed odd to me, and I was already finding I liked raw Futures in rust better than async/await in JS already, especially because you can into a `Result` into a `Future`, which made async code already surprisingly similar to sync code.

I will say, the big thing in rust that has me liking async/await now is how it works with `?`. Rust has done a great job avoiding adding so much sugar it becomes sickly-sweet, but I've felt `?` is one of those things that has such clean semantics and simplifies an extremely common case that it's practically a necessity in my code today. `.await?` is downright beautiful.




I've written a lot of JS/TS over the last couple of years, and I've found that even though it may just be syntactic sugar, async/await is a major win in both how easy it is to now write async code, and how readable the code is.

Generators got us close to that, and it is a more generic feature. But over all the code I've written, I've only had one solid use case needing generators for something other than what's covered by async/await. Having a specific syntax that covers 95% of usages is very worth it in my opinion.


Looking at the example I gave, how is `async function` any more clear than saying `co(function`? It covers the exact same use cases with equivalent clarity.

They could've shipped the `Promise` object with a `coroutine` fn, like bluebird did, and had the same semantics without needing to reserve words and add syntax to the language. All it opens up is combining async and generators, which can be handy for iterating things like prefetched results, but that's a rare enough case not to need to be baked into the language.

Separating the coroutine would let us work beyond just promises too. Wrap around old code using error-first callbacks, or new code using observables (yield to get the next, yield* to complete). I just wrote a coroutine to give quasi-concurrency to Google apps scripts, so it can queue up the routines running `fetch` methods and instead do them in one parallel-request-making `fetchAll`. It's a much better approach for the long-term of a language. Imho 90% of the junk we deal with in old languages is stuff for a convenient implementation at the time that we don't use anymore.


>Looking at the example I gave, how is `async function` any more clear than saying `co(function`? It covers the exact same use cases with equivalent clarity.

It skips the generator part and extra wrapper. That's a simpler syntax (and thus more clarity) for what people do 99% of the time.

Clarity is not necessarily "I can see the underlying mechanism".

Generators hide their underlying implementation (in C++) too, after all.


> Generators hide their underlying implementation (in C++) too, after all.

that has been contentious to say the least.


In my opinion, the main reason to have `async` is because it doesn't require knowledge of generators. I've worked with developers who thought that `async`/`await` was too much trouble to learn and unnecessarily complicated. Promises allow us to act like nothing changed and functions return values. The trick, to me, is how do you meet that need without sacrificing these more powerful operations.


But it does require knowledge of generators. You're suspending a function during execution; that's what a generator is. We could've just named a global `async` that declares a coroutine FN on promises, and named `yield` as `await`, and the whole thing would look identical to now except for an extra paren and asterisk. You wouldn't need to understand their inner workings any more than you do with async/await now.


>But it does require knowledge of generators. You're suspending a function during execution; that's what a generator is.

That's like saying "Ordering from Amazon does require knowledge of driving a van. That's how the stuff comes to your home!".


No it's not. If the syntax is that similar, your metaphor makes no sense.


> I used to be a huge fan of async/await in the JS world, until I realized that we were tethering a very specific implementation to new syntax, without adding any clarity beyond what a coroutine and generator could do. eg `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

FWIW Python originally used generators to implement async / coroutines, and then went back to add a dedicated syntax (PEP 492), because:

* the confusion of generators and async functions was unhelpful from both technical and documentation standpoints, it's great from a theoretical standpoint (that is async being sugar for generators can be OK, though care must be taken to not cross-pollute the protocols) but it sucks from a practical one

* async yield points make sense in places where generator yield points don't (or are inconvenient) e.g. asynchronous iterators (async for) or asynchronous context managers (async with). Hand-rolling it may not be realistic e.g.

    for a in b:
        a = await a
is not actually correct, because the async operation is likely the one which defines the end of iteration, so you'd actually have to replace a straightforward iteration with a complete desugaring of the for loop. Plus it means the initialisation of the iterator becomes very weird as you have an iterator yielding an iterator (the outer iterator representing the async operation and the inner representing the actual iterator):

    # this yield from drives the coroutine not the iteration
    it = yield from iter(b)
    while True:
        try:
            # see above
            a = yield from next(it)
        except StopIteration: # should handle both synchronous and asynchronous EOI signals, maybe?
            break
        else:
            # process element
versus

    async for a in b:
        # process element
TBF "blue functions" async remain way more convenient, but they're not always an easy option[0], or one without side-effects (depending on the language core, see gevent for python which does exactly that but does so by monkey patching built-in IO).

[0] assuming async ~ coroutines / userland threads / "green" threads


I do wonder if I'd feel differently if async + yield was a more common pattern. It can be extremely powerful (especially when combined with an event emitter). Maybe my problem is more with how async/await still isn't really being used for anything generators couldn't already do, but that's more about the current approach to using them than a limit of the language.


The reason we have await in JS is particularly because it is _less_ powerful than generators and you can make more assumptions about it and how one uses it as well as write better tooling.

This is why we have await in Python and C# as well, eventhough we know how to write async/await with coroutines for 10 years now :] Tooling and debugging is super important.


This is an important argument. Less powerful constructs are easier to combine and understand. For example, call/cc is very powerful, but I would prefer to use various less powerful constructs like try/except/finally which are clearer.


> `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

I'd rather pick a standard solution over a library. I never used CO, because I didn't see the benefits of pulling this library in over plain ol' promises. The cognitive overload of generators, yielding, thunks and changing api-s is just too much IMHO, I like simpler things that work just as well.

With the await keyword baked into the language I can simply think of "if anything returns a promise, I can wait for the response in a simple assignment".


> I'd rather pick a standard solution over a library.

This. Rust can be complex enough for engineers new to the language. This sort of feature really requires a standard approach.


It's as standard a solution as any, it just doesn't require changing the language. If you can Promise.all, you can Promise.coroutine. Putting async in front of a function definition requires learning a new syntax, but wrapping a coroutine FN around it builds on knowledge of existing behaviour.


I spent quite a lot of time writing Typescript using Fluture and a few other libraries a while ago. Coming from a functional background, combinators + futures actually feels quite natural to me, so I looked at the examples of the "bad" code here and thought I actually quite liked it. But I certainly don't mind the async syntax too much either. Will see how it works out some time!


It's not just about the combinators vs specialized syntax. I've had to write a library with 0.1 futures and you have to implement a lot of Futures manually. i.e. you need to implement the Future trait for your types pretty often. In those cases you cant use the combinators, you need to directly drive the state machine of your future with poll() and try_ready macro etc. With async/await I don't have to implement any futures manually anymore, the diff of my library updating to async/await is basically deleting 75% of my code.


Yeah and this is huge. Not something I realised I needed coming from my experience in js, but it's a big deal in rust. It's not that different from needing to manually branch match arms and `into` your errors back for `Result`s, when those could all be communicated just as clearly with the far more concise `?`. The `.await` removes so much re-implementing the Future trait. It's been especially bothersome for me, because often I'm trying to figure out how this particular library implemented their Future. Now I feel like if I get the gist of how a Result is returned, I can apply identical reasoning to how `.await` is returning my Future, except that it's suspending the function and saving the state. Obviously that's what a generator does, but the `.await` syntax is far more expressive than `await` in js, because passing back that `Result` is a much more difficult thing to manage by hand.


All that redundant noise is one of the main reasons why I avoid Rust if I can write something in Haskell. Rust has it everywhere, what is expected for a low level language, but as long as they can remove it, they indeed should.


Coming from mostly Js/ts, I've had the opposite experience. I'm finding what I write on rust may be more explicit, but often less noisy. Using an error wrapping lib and ? everywhere made the biggest difference. May not be as efficient as the most optimized rust code, but far faster than the equivalent JS for the same thing. I also pray every day for macros to make there way into ts, since I'm constantly dealing with friction on the boundaries of runtime vs static code that rust handles nicely with a macro.


Oh the combinators in rust can be great. I think making the code read more imperatively is often a case of confusing the familiar with the simple. TS makes combinators way better too. That's why it's the `.await?` that sells me on the whole concept. Writing an error-path separately can be a headache, and as a result I still have daemons that have some dependency with an `unwrap` that sits there, poised to crash everything without warning. The `?` has cut the length of some of my modules in half. I've been waiting for a good way to do the same in async code for a while.


> `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.

Except in the latter you have to bring your own `co(...)` function. Baking it into the syntax means you don't need that as a library.


Not really - you have your own promise implementation as `Promise`. Bluebird made `Promise.coroutine`, and that could've gone into the main Promise implementation.


in my experience, "async" allows you to write the code linearly, but now the code is executed out of order, which is actually much more confusing (for me) to debug and deal with. however, I'm interested to try the rust version to see whether the more explicit types, etc. make this easier to deal with than e.g. `async def` in python, which I find hideously complicated compared to normal event loops.


Double as confusing if you're used to taking a more fp approach, too. I definitely went overboard with await when it first came to JS, but nowadays I've found a new respect for .then. Especially in TypeScript where it's easier to chain the functions and worry less you're using them wrong. Many of the functions I'll write for a .then will find their way into an Array#map or an observable too.


> in my experience, "async" allows you to write the code linearly, but now the code is executed out of order

Nope, the code is executed in order. That's what `await` accomplishes. Wherever you use `await` you have a guarantee that anything that comes after it will only execute when the promise that was awaited, has resolved.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: