> There is nothing wrong with hiding the nature of the async under the hood of a sync function as long as the abstraction is clean, which in most cases it quite easily can be.
This is the question and I would disagree. Hiding the asynchronous nature of any given function is a dangerous illusion. Languages that promote it through stackful coroutines ultimately lead to programs that are difficult to reason about and often don't perform well because developers lack any real control over concurrency and are completely at the mercy of an opaque scheduler. (And frankly, even languages like Go which get this 'right' ... don't. Eventually even people who like Go realize Go channels are bad[1].)
The answer here I think is going to be higher kinded types a la Rust. Asynchronous is a "fundamental property of time and space" -- ignore it at your own risk. But if the type system can elegantly capture the difference between present-values and values-to-come then you can realize the best of both words: code that reads like a single-thread-of-control but is actually heavily asynchronous. This is why 'await' style stackless coroutines and Futures prove to be so popular.
Though I agree more research is needed here. I was disappointed to see the Rust developers (who apparently convinced people to pay them to research this stuff) converge so quickly on stackless coroutines.
Nobody forces you to use channels in Go, I use them rather rarely, in 9 out of 10 cases it's to pass around a stop handler for some activity loop. That's where they are quite useful.
I think of myself of an older generation so my approach to concurrency is to use locks and fancy data structures and fancy architecture to avoid race conditions.
I would suggest looking into how easy it is to write an HTTP handler in Go. That involves plenty of go routines. Each connection is a go routine. Each HTTP request also gets it's own (in HTTP/2 the former and later may not be the same).
I don't have to think about the blocking nature of an operation to, for a recent example, fetch and parse a remote webpage, I simply do it. The Go runtime will take care of scheduling the goroutines such that if a new request comes in while I'm a tight loop the HTTP server can continue to handle queries.
In JS land however, I can't write tight for loops without potentially blocking up the entire server.
You mentioned the solution yourself, hide the async nature of the code. My HTTP code doesn't read like async code until you hit global resources. That's what I essentially want.
I do not want to think about async until I need it and when I need it I should be able to pretend it's sync without cognitive overhead; that enables me to efficiently reason about the steps a routine takes before every resource a request uses is released again.
As the author of the linked post I would love to correct a misconception here.
Go is fantastic about asynchronous programming and the fact that it hides it is one of Go's strengths. Channels are/were overused when I wrote that post but that's completely independent of Go's excellent async support.
My all time favorite blog post on the matter is http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y... which is required reading about why a language hiding a program's async nature is actually the best thing it can do, and Rust's decision to not implement green threads at the language level was wrong, in my opinion.
> This is the question and I would disagree. Hiding the asynchronous nature of any given function is a dangerous illusion.
I understand where you are coming from. However, in my experience, it's "turtles all the way down". Pre-emptive multi-tasking, NUMA, multiple CPUs, CISC, all add their level of asynchronicity to your program execution whether you are aware of it or not.
The right answer in this case is "hiding the nature of async under the hood". Yet, we've seen in a few cases recently, e.g. the recent CPU bugs, where this isn't entirely possible.
> ultimately lead to programs that are difficult to reason about and often don't perform well because developers lack any real control over concurrency
My experience has been the complete opposite. It's hard to make complex parallel programs and reason about execution, but concurrency primitives like fibers and reactors are a negative overhead abstraction which makes code simpler and easier to understand and therefore allows us to do more complex things with the same cognitive potential.
This is the question and I would disagree. Hiding the asynchronous nature of any given function is a dangerous illusion. Languages that promote it through stackful coroutines ultimately lead to programs that are difficult to reason about and often don't perform well because developers lack any real control over concurrency and are completely at the mercy of an opaque scheduler. (And frankly, even languages like Go which get this 'right' ... don't. Eventually even people who like Go realize Go channels are bad[1].)
The answer here I think is going to be higher kinded types a la Rust. Asynchronous is a "fundamental property of time and space" -- ignore it at your own risk. But if the type system can elegantly capture the difference between present-values and values-to-come then you can realize the best of both words: code that reads like a single-thread-of-control but is actually heavily asynchronous. This is why 'await' style stackless coroutines and Futures prove to be so popular.
Though I agree more research is needed here. I was disappointed to see the Rust developers (who apparently convinced people to pay them to research this stuff) converge so quickly on stackless coroutines.
[1] https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s...