We need algebraic effects in more languages, this solves the function coloring problem. OCaml 5 has them and it seems to be doing quite well, combine that with the semantics of the borrow checker in the form of OxCaml and we might just have an ideal language. I'd like to see algebraic effects in Rust as well but sadly it seems their keyword generics initiative is languishing.
I wish the key word was instead dontawait and was used inversely to how await is used. 99% of the time I'm using an async function, despite however slow it is, there's nothing for my code to do but wait for it to finish. But if for some reason I would like the next line of code to run before the current one is done, I'll let you know.
Like, why can't my sync function await something asynchronous? If it has to lock up the whole thread while that function executes, that's fine because that's how it was going to work anyway 99% of the time
Go doesn't have colored functions due to its nice fat runtime hiding all the async magic away for us.
That makes it a pleasure to code concurrent stuff for IMHO.
It does have its own similar problems though - does a function return an error? If so you are going to need to plumb the error return through all the callers. Does a function need a context.Context? Ditto.
> You still can’t call a function that returns a future from synchronous code. (Well, you can, but if you do, the person who later maintains your code will invent a time machine, travel back in time to the moment that you did this and stab you in the face with a #2 pencil.)
Author makes up a lie.
Then lampshades it away with a colorful non sequitur.
---
The alternatives that people praise like golang, have other tradeoffs that are much worse because the async logic is now implicit. Your entire codebase is now a surface area that is at risk of being blocked by waiting on a channel; the the mitigation of this is through responsible use of coroutines, but then you're right back around to extra information about your code that is analogous to colring, except not as explicit as async/await.
My first ever EM showed me this piece ~10 years ago, and I still think about it a lot. One pattern I've adopted is to keep as much code to be synchronous as possible. On larger teams, especially when the slop-cannon is really going, I can at least depend on codeowners to tag me if someone tries to convert something to async (eg. adding a DB call somewhere), because they chain of things that need to be converted to async is so long. Then I can jump in and say "this entire chain of code is sync, if you want a DB call, do it somewhere else"
For Python backends I've seen good success with just making it company policy that everything is synchronous (normal-colored) and bypassing the developer overhead from async/await. Cooperative multitasking is a pain because, well, it requires cooperation. You can go pretty far by just adding more threads, processes, and replicas before it's worth the overhead.
You not only leave performance on the table (which depending on your use case/environment, may not matter if you can just throw more threads at it) but also some developer ergonomics.
asyncio.gather is a lot less code than having to manage a thread pool or something like Celery with all it's underlying infrastructure.
If you're in an ecosystem where a lot of the async boilerplate is free/cheap (ex: FastAPI) then the developer overhead of sprinkling awaits on your I/O bound calls is pretty low IMO.
I just do not want to do async in Python. If you need async its questionable whether Python is a good choice at all, and if you use Python maybe look at another solution if at all possible (even using more processes and throwing hardware at it).
We need algebraic effects in more languages, this solves the function coloring problem. OCaml 5 has them and it seems to be doing quite well, combine that with the semantics of the borrow checker in the form of OxCaml and we might just have an ideal language. I'd like to see algebraic effects in Rust as well but sadly it seems their keyword generics initiative is languishing.
I wish the key word was instead dontawait and was used inversely to how await is used. 99% of the time I'm using an async function, despite however slow it is, there's nothing for my code to do but wait for it to finish. But if for some reason I would like the next line of code to run before the current one is done, I'll let you know.
Like, why can't my sync function await something asynchronous? If it has to lock up the whole thread while that function executes, that's fine because that's how it was going to work anyway 99% of the time
Like the & at the end of a shell command?
Go doesn't have colored functions due to its nice fat runtime hiding all the async magic away for us.
That makes it a pleasure to code concurrent stuff for IMHO.
It does have its own similar problems though - does a function return an error? If so you are going to need to plumb the error return through all the callers. Does a function need a context.Context? Ditto.
I guess you can't win them all :-)
And Haskell is an ensemble of rainbows. It's very fun and pretty to look at.
Type classes can smooth over some of it but it's not unusual to have to do some plumbing.
> You still can’t call a function that returns a future from synchronous code. (Well, you can, but if you do, the person who later maintains your code will invent a time machine, travel back in time to the moment that you did this and stab you in the face with a #2 pencil.)
Author makes up a lie.
Then lampshades it away with a colorful non sequitur.
---
The alternatives that people praise like golang, have other tradeoffs that are much worse because the async logic is now implicit. Your entire codebase is now a surface area that is at risk of being blocked by waiting on a channel; the the mitigation of this is through responsible use of coroutines, but then you're right back around to extra information about your code that is analogous to colring, except not as explicit as async/await.
My first ever EM showed me this piece ~10 years ago, and I still think about it a lot. One pattern I've adopted is to keep as much code to be synchronous as possible. On larger teams, especially when the slop-cannon is really going, I can at least depend on codeowners to tag me if someone tries to convert something to async (eg. adding a DB call somewhere), because they chain of things that need to be converted to async is so long. Then I can jump in and say "this entire chain of code is sync, if you want a DB call, do it somewhere else"
For Python backends I've seen good success with just making it company policy that everything is synchronous (normal-colored) and bypassing the developer overhead from async/await. Cooperative multitasking is a pain because, well, it requires cooperation. You can go pretty far by just adding more threads, processes, and replicas before it's worth the overhead.
You not only leave performance on the table (which depending on your use case/environment, may not matter if you can just throw more threads at it) but also some developer ergonomics.
asyncio.gather is a lot less code than having to manage a thread pool or something like Celery with all it's underlying infrastructure.
If you're in an ecosystem where a lot of the async boilerplate is free/cheap (ex: FastAPI) then the developer overhead of sprinkling awaits on your I/O bound calls is pretty low IMO.
I just do not want to do async in Python. If you need async its questionable whether Python is a good choice at all, and if you use Python maybe look at another solution if at all possible (even using more processes and throwing hardware at it).
Purple.