Hacker News new | ask | show | jobs
by simiones 2227 days ago
> I’m pretty sure you could implement futures and async/await using Go channels too if you wanted to.

Futures, maybe (though without generics you'll be either very dynamic or write a new future for each struct, of course).

But async/await is a syntactic feature and can't be implemented in a language without macros and/or continuations. Basically `await` is a keyword which returns a future that will execute the rest of the function as written. Something like this is relatively easy to implement:

    async Task<int> Foo() { 
       int i := await Bar();
       return i + 1;
    }
You could re-write it to something like this in Go:

    func Foo() func()(int){
         var reply chan int
         go func() {
             reply <- Bar()
         }
         return func()(int) {
             i := <- reply
             return i 
         }
    }
Or something similar. Maybe you could even reduce the boilerplate, though it's already much worse than the C# verison. But this is much more difficult to re-write in Go:

    async Task<int> Foo() {
        auto sum = 0;
        foreach (auto x in someEnumerable) {
               switch(x) {
                     case 1:
                          sum += await Bar(x);
                          goto case 2;
                     case 2:
                          sum /= await Bar(x);
                          break;
                   }
        }
        return sum;
    }
Assuming you want to keep the asynchronicity, this gets much uglier to implement in terms of channels (not that this is very common code).
1 comments

I don’t really want to implement async/await in terms of channels. I would rather just use channels directly. Notice that even in your Go code it’s explicit which part executes as a separate coroutine and where the blocking communication between coroutines takes place. Async/await is more “magical”, or maybe I’m just dumb.
Hmm, I think it only looks magical because I chose a relatively unfair comparison, I realize now.

To show a more realistic comparison, let's asume Bar() is not "async ready" in either Go or C#. Then, the C# code would look something like this:

    async Task<int> Foo() { 
       var task = Task.Run(() => Bar());
       int i = await task;
       return i + 1;
    }
This compares more clearly to the Go version with the same assumption:

    func Foo(ret chan int) {
        task := make(chan int, 1)
        go func() {
             task <- Bar()
        }
        i := <-task
        ret <- i + 1
    }
So the readability / cleanliness is pretty similar for simple examples. However, once you start doing more complex things, like I showed in my second example, the difference become much more pronounced. Overall, a `Task<T>` offers much more information to callers than a `chan T`, so it can usually be composed in more interesting ways, but it can also sometimes be more cumbersome to use.

And again, in both languages, there are clear demarcations between async functions and blocking functions. In C#, async functions return `Task<T>` and are usually marked `async`, while in Go async functions have one or more `chan T` arguments and don't normally return values. If you want to call an async function from a non-async function or vice-versa, you need to use special constructs (e.g. Task.Run, Task.Wait or Task.Result for C#, and `go func () {...}` and blocking channel reads/writes in Go).