Hacker News new | ask | show | jobs
by devxpy 2217 days ago
I've been thinking why exactly async-await was chosen at the function level, and not the caller level.

I mean for languages that have event loops at their core, why isn't every function `async` by default? Let the caller decide how it wants to use the function.

`async` functions don't wait for a result of a `Future` unless `await` is used.

Instead of putting all that effort into putting `await` everywhere, why not invert that logic and make them `await` by default, and instead have the async-await syntax used by callers?

Like if one to were to write a transpiler to do this in dart, it might compile the following -

    void main() {
        Future<int> xFuture = async doSomething()
        int x = doSomething()
         
        assert(await xFuture == x);
    }

    int doSomething {
        ...
    }
into -

    Future<void> main() async {
        Future<int> xFuture = doSomething()
        int x = await doSomething()

        assert(await xFuture == x);
    }

    Future<int> doSomething async {
        ...
    }

This might be stupid, but my solution to the "what color is your function problem" is to make every function async.

I looks an awful lot like what go does with its `go` syntax, but because you have an event loop with Future and Async Streams, you don't need to worry about dealing with CSP.

2 comments

I suspect this is for two reasons:

* Many popular languages today predate modern asynchronous computing. This makes async-as-default impossible because it's an afterthought

* Async computing is harder to learn and confusing for those just picking up the language. There are some pretty major ergonomics issues that you have to solve if you want anything more than a DSL to achieve noteworthy adoption levels.

> Many popular languages today predate modern asynchronous computing. This makes async-as-default impossible because it's an afterthought

Here's a rough sketch of how this can be achieved for a language that already has event loops -

Transpile the following -

    void main() {
        Future<int> xFuture = async doSomething()
        int x = doSomething()
         
        assert(await xFuture == x);
    }
Into -

    Future<void> main() async {
        Future<int> xFuture = (() async => await doSomething())();
        int x = await doSomething();
      
        assert(await xFuture == x);
    }
Now people can keep using a pre-existing `doSomething()` (that's either async or sync) and its compatible with our new caller based async statement.

BTW that transpiler output is runnable dart code actually works.

I know this has tons of edge cases that this simple example doesn't capture, but it's definitely "possible".

I have thinking something similar but with generators:

    //A async candidate
    fun read_lines(file):
        for line in file:
            yield line  }

    let lines = read_lines("hello.txt").await //turn async 

I wonder why something like this is not used. Exist a bad interaction that could arise from this? Maybe about how nest stuff?

    fun uppercase_lines(file):
        for line in read_lines(file):
            yield line

    let lines = uppercase_lines("hello.txt").await //turn async, but also recursivelly read_lines?
How do you plan on making this concurrent? Generators can defer computation for later, but that doesn't magically make them async.
The assumption is that exist a desugaring step in the compiler/interpreted to async/await/futures.