Well it also sucks if unpure code has to add a bunch of boilerplate annotations to every single function and call.
> I recently tried Go to speed up some Cloud Functions that use Firestore, it's very difficult to know which of the chained method calls of the Google-provided SDK is the one to actually "blocking" the green thread, perhaps they are all are unnecessarily?
If you have to mark enough of your functions as "possibly blocking" then you can get just as lost about which one is actually blocking in a particular situation.
Since this could be read in a very abstract, purist way, I'd like to add that this is also very much an architecture concern for many pieces of code.
At work, I regularly encounter issues with a piece of code that is written in one of the worst imaginable spaghetti styles. And, since it changes things in a database (not even in proper transactions, btw), and is written to pretty much prevent being tested, no one wants to touch it.
Yet, what the code does would really match an "FP" approach:
- Read in all inputs. You might even read during computation if you're simulating FP through transaction isolation
- Compute the outputs and the new desired state
- Pass that to someone who writes it back to the database and commits the transaction
Outputs in our case are messages that get stored in the database, too (in order to allow recovering after transmission issues), so "output" in effect also just means "state". You don't always have to read "FP" in the strictest sense -- there's a spectrum to this.
Now, if that code was written in such a way it would be easily testable and thus refactorable. In fact, most ideas in the clean/hexagonal architectures or "functional core, imperative shell" are to achieve exactly this. But since the author of said code never had any pushback, they just kept making an already grim situation worse with every change. For years.
I see the same issue with the "ice nine" argument for async. If async infects all your business logic, chances are your abstractions suck.
Edit: Also note that goroutines do effectively the same thing as synchronous code in a larger asynchronous function call: You group the code you can sensibly do in one go into a single goroutine. Then you pass the result to someone else to perform the next step.
So, it's very often just another way to express the same process. CSP will invariably be a better abstraction for some problems, and async/await for others.
Then you keep pushing stuff to other threads "just to be sure", which may hurt performance but the worst part becomes reading that code.
Now you will want to blow your brains out because you will have to refactor all of your function calls down the chain.
It could get even worse if at some point, you are passing a function to a library/framework and for whatever reason it refuses to tolerate a function of your color (assuming you can't lambda-ize the color). Now you have to rewrite the library. Or use a library that does tolerate it. Which might lose other features or ergonomics.
I recently tried Go to speed up some Cloud Functions that use Firestore, it's very difficult to know which of the chained method calls of the Google-provided SDK is the one to actually "blocking" the green thread, perhaps they are all are unnecessarily? Without studying the source code, it's hard to know. I prefer explicitness over absolute simplicity. It also makes it easier to optimise code in my opinion as it's easier to identify sequential async calls that could actually happen concurrently, either by merging SQL queries or by using Promise.all() or the equivilent.
JavaScript does a good job with the await keyword in my opinion, you know exactly when something could take a significant delay, when it's doing something more behind the scenes. Rust does one better by making ".await" a suffix, allowing for nifty call chains. It communicates that this is an explicit yield point and could take a while.