Function Colors Represent Different Execution Contexts
Posted by Shadowys@reddit | programming | View on Reddit | 18 comments
Posted by Shadowys@reddit | programming | View on Reddit | 18 comments
Guvante@reddit
You can get that for free in most languages using global state. Some languages like Rust require you to do something with the state machine for it to progress others will do that for you.
The pain of async is generally when it overlaps with threading code. E.g. "sleep my thread while I wait", or "block my thread until X happens" which of written normally impact a thread pool member which can result in starvation and other issues.
It is referred to as color because if all of your code is thread based or async based it is easy but if you have access to both it is complicated.
Excellent-Cat7128@reddit
At least in C#, there are async-appropriate methods for doing yields and sleeps that don't block the thread itself. In Javascript, you can use a Promise to do the same and there's probably some 7 line NPM package if you can't be bothered to do it yourself.
Guvante@reddit
The problem with color isn't that "you can't do X" it is instead that how you do it changes.
If you are in an async context you need to await a particular function. If you are in a thread context you need to call a different blocking function.
Excellent-Cat7128@reddit
All true, but it's just not that hard. If you're in an async context, use
Task.Yield()
and if you are not, then useThread.Sleep()
. If you can't figure that out, find another job.Guvante@reddit
Have you looked into the color problem at all or are you discarding the concept everyone is talking about with familiarity as I important?
Because you aren't necessarily calling those functions things you are doing are.
So everyone needs an async function and a non-async function.
Oh and sometimes the two have slightly different implementations because of nuanced differences between the two styles.
Excellent-Cat7128@reddit
Yes, I am aware of it. There are ways to deal with it. The alternatives also have their own downsides. You have to understand how computers work to program sometimes, and no amount of syntactic sugar or leaky abstractions will fix that.
If a function is marked async or returns a
Task<T>
, I expect it to call async-aware things. If it doesn't, that's a bug that either I work around or request to get fixed. If a function isn't marked as async and I have reason to suspect it could be long running or might block, and I'm in an async context, I wrap it in aTask.Run()
call. These are fairly straightforward rules.And again, you have to deal with these same types of problems even if async never came on the scene. People liked async because it was generally a lot easier to deal with than multithreading.
Or you can just provide an async function and non-async contexts can do a blocking wait. But in truth, the async and non-async versions actually are different because they operate differently. The fact that people complain about this shows that they are expecting async, or any other parallelization method, to be more magical than it can possibly be. A non-blocking function is fundamentally different than a blocking one. Fancy syntax can help, but there's a limit.
Yes, and this would be true in any other system as well. A version that spawns a thread to avoid blocking, or uses some sort of asynchronous or callback based system with the OS or runtime will also need to be implemented differently than one that calls a blocking system function. What's your point?
Guvante@reddit
That it is an annoying problem that requires you to color every function in your program...
Like your response of "just do the work" is akin to "it is a Turing complete language of course that is possible". If you squint hard enough it might kind of be true but you aren't actually adding anything to the discussion by ignoring the discussion by discounting it out of hand.
Excellent-Cat7128@reddit
I just fail to see why this is a big problem. We have to annotate return types and parameters to functions too, all the way up the call stack. And if you need a variant of a function that takes different parameters, you have to potentially change it all call sites. Oh the horror! Welcome to the realities of changing and architecting code.
It seems like you read the What Color is Your Function article, and have cargo-culted yourself into what you think is a real argument. And it's just not. First of all, the article is mostly complaining about callback hell. It then goes on to explicitly mention C# and how it massively improves the situation with async/await! Seriously, go read it, right now. It then also goes on to talk about how languages with explicit multithreading are better, which is laughable to anyone who had to deal with the whole host of problems that come with traditional multithreading.
Async functions are colored because they behave differently. They should be colored. And return types should be annotated, and parameters should be annotated, and this should all be enforced by compilers, linters and runtimes. It's explicit and it's well-defined. Getting asynchronicity without the coloring is magic, and magic is fine until it's not and then it's a giant source of bugs. You don't get bugs like that with async, especially if you use a statically typed language.
Guvante@reddit
My point is colored functions have a downside not that they are wrong for being colored.
It is the same thing with exceptions and innumerable other effects that we don't have good enough tools to express.
And I agree with OP that effect systems haven't had much success here. After all annotating isn't the same thing as solving.
The solve for colored functions is easy for a project. Pick a color and you are done.
The problem is mostly a language design or library design one where you want to support both.
Heck C# enumerators had similar weird problems since you generally need to wrap an enumerator in a helper function to check your arguments to make sure you don't accidentally delay failing from bad inputs.
Conscious-Ball8373@reddit
I get where you're coming from but I also think you're minimising a real problem. I wouldn't consider myself an asyncio expert, but I've worked with it for a fair few years now. I still don't know how to properly solve problems like SQLalchemy. You have a big library of synchronous code which has no reason to be asynchronous; all the library itself does is transform some python code into SQL code (and anyway, the library was written before asyncio was a thing). It then hands that SQL code to a database driver which may or may not block and may or may not be asynchronous. The code called in the library may be synchronous or asynchronous.
As far as I can tell, there are three approaches to solving this problem: * Maintain two versions of the library. The actual functionality of the library will be identical in both versions, but one will have all its functions declared async and the other won't. The maintenance burden of this should be obvious. * Maintain a synchronous library which is able to call out to an asynchronous database driver by creating a new event loop for each database call. This requires asynchronous users of the library to wrap every call into the library in a separate thread. This is probably the correct solution but users of the library complain about it so loudly that it wasn't adopted and that should tell you something about the usability of this solution (which I think is the one you're suggesting). * Monkey around with asyncio internals to subvert the function colouring system, allowing layering of asyncio and synchronous functions in the same event loop. This is the solution the developers of SQL alchemy have adopted, regrettably in my view. It means that All the assumptions about thread safety that usually come with asyncio are gone and you have to treat every call into sqlalchemy as a potential context switch when you're reasoning about how co-routines interact.
SwingOutStateMachine@reddit
This article is brilliant. I'm reminded of the tradeoffs that single-source frameworks/languages like SYCL have made to try and distinguish between CPU and GPU contexts. Often, it's just a compiler error, and the user has to be smart enough to know that a GPU context can't call a CPU context.
webstrand@reddit
Solutions to the function coloring problem aren't about hiding transitions across differently able runtimes, they're about reducing the viral refactoring needed when procedural code needs to call into async. When a deeply nested function needs to wait on a Future, every caller in the stack up to the async runtime suddenly needs to become async-aware so that it can efficiently yield the thread of execution to until the Future resolves.
Making transitions implicit help mitigate the refactoring cost of calling an async function. Execution bridges don't solve this problem because threading a bridge through the call stack still requires every function to become bridge-aware, defeating the entire purpose of eliminating function coloring. And making functions bridge-agnostic is exactly what programmers attempting to solve the function coloring problem are pursuing.
Shadowys@reddit (OP)
The refactoring cost is imo high because people treat it as implicit
webstrand@reddit
What do you mean by implicit, then? For instance in javascript every async yield is marked with
await
and every function that can yield is marked withasync function
. To me that's not implicit at all. Sure, it could be more explicit and expose the whole async runtime (allowing for multiple runtimes), but that doesn't actually help with the refactoring cost. If you need to call an async function, every function above it in the call tree must be refactored and marked withasync
and calls must be decorated withawait
everywhere.Whether the async runtime/state machine is passed around via language feature or directly by parameter I don't think matters?
knome@reddit
I agree with what you wrote here. It sparked an idea for me.
Function coloring is kind of a halfassed monad for languages other than haskell.
Async functions are effectively an AsyncIO monad that must be run using some runAsyncIO (AsyncIO a -> IO a) invocation from an IO monad function, and so you can't mix in IO stuff in it because it's the wrong function type.
It's funny because Haskell itself doesn't need to bother with this since it's all greenthreads and so its equivalent of all this is just spawning more threads, like go or erlang, rather than needing to carefully weave them together as languages whose threads are 1-to-1 with system threads must do.
alphaglosined@reddit
I've currently got a proposal for D to add stackless coroutines.
One of the original goals was to make automatic waiting for a stackless coroutine to complete in a synchronous function.
However this has problems with it, such as holding a lock indefinitely, which is why I did not include it in the proposal.
I think you're right in embracing coloring (I figured that out before the proposal), but what I do not think you're right about is the interprocedural offerings in the execution. Synchronous functions simply do not model what it need to model to safely do the awaiting for you.
somebodddy@reddit
Could you elaborate on that approach? I've tried googling it but found nothing relevant since neither macros nor atoms are about async execution.
Shadowys@reddit (OP)
My understanding is that clojure core.async relies on atoms and something similar to CRDT data structures for state synchronisation