wesselbindt 8 months ago

I've always found the criticism leveled by the colored functions blog post a bit contrived. Yes, when you replace the words async/await with meaningless concepts I do not care about such as color, it's very annoying to have to arbitrarily mark a function as blue or red. But when you're honest with yourself and interpret the word "async" as "expensive", or as "does network calls", it becomes clear that "async/await" makes important features of your function explicit, rather than implicit. Seeing "await" gives me more information about the function, without having to read the body of the function (and the ones it calls, and the ones they call, etc). That's a good thing.

There are serious drawbacks to async/await, and the red/blue blog post manages to list none of them.

I've wrote a blog in response to OP containing a more detailed version of the above:

https://wpbindt.github.io/async/opinions/programming/2024/01...

  • Calavar 8 months ago

    I think you read the article as saying that juggling function color is trivial nonsense and a waste of programmers' time, and your rebuttal is "await" isn't trivial - it conveys important information about the function.

    My reading of the article is very different. I think the main thrust is function color is infectious, and that's an issue because adding "async" to a single function can have cascading effects through your codebase, which makes refactoring difficult and slows down development speed. This is true even if "async" conveys important information.

    • wesselbindt 8 months ago

      > I think the main thrust is function color is infectious

      My response to this would be that this infectiousness is not a feature of async/await but an intrinsic property of the function. A function calling an expensive function is itself expensive. This is not a bug, it's just reality, and async forces you to be upfront with that.

      Another part of my response would be that it doesn't need to infect your entire code base, and that it actually pushes you in the direction of keeping your business logic (which can be sync) separated from your infra logic (which is almost guaranteed to be async). This too I see as a good thing, mixing business logic with infra logic tends to push you in the direction of death by a thousand cuts bad performance. You can achieve this separation as follows:

        async def your_endpoint():
         business_object = await db.get()
         business_object.do_business()
         await db.save(business_object)
      
      The blog post I linked to goes into more detail.
      • Calavar 8 months ago

        > My response to this would be that this infectiousness is not a feature of async/await but an intrinsic property of the function. A function calling an expensive function is itself expensive. This is not a bug, it's just reality, and async forces you to be upfront with that.

        You're right, the async keyword describes a reality about a function, and any attempt to syntax sugar your way out of that reality will leave you with a leaky abstraction. Async is async whether you have colored functions or colorless functions.

        On the other hand, should changing a single function from synchronous to async require you to touch up your code in 30 different places across 10 different files? If you know what you're doing and you're okay with the async call, why do you have redeclare your intentions over and over again like you're punching in nuclear launch codes?

        It's an opinionated question. You can see a similar design tradeoff in Zig's approach to generics (implicit type constraints) versus Rust's approach to generics (explicit type constraints) Rust's explicitness removes potential footguns, but it's also an absolute PITA to refactor your type system when you have to.

        • wesselbindt 8 months ago

          I do understand that the point the blog is making is the infectiousness. My point is when you drop the meaninglessness of blue/red and replace it with the more meaningful async/sync, this infectiousness goes from being arbitrary and annoying to completely natural, and even desirable. If all of a sudden you find yourself wanting to make some function async on top of a stack of 10 sync function calls, you're probably doing something ill-advised[1], and async/await making this difficult is valuable feedback on your design. Is this feedback useful in every system? No, just like static typing would be overkill for a five line shell script. But writing a paradigm off just because it makes it more difficult to set off certain footguns is, in my view, a mistake, and that's what irks me about this blog post.

          [1] keeping business logic separate from infra logic is typically a good idea, see hexagonal architecture, or imperative shell functional core. Async/await is a convenient mechanism to guarantee maintaining this architectural choice, even for developers less familiar with this architecture.

    • eyelidlessness 8 months ago

      This. Plus, the cascading effect isn’t necessarily just difficult to account for, sometimes it makes integration effectively impossible.

      As an example, a project I work on provides functionality implementing arbitrary computation graphs, with an explicit guarantee that all computations are synchronous, consistent and complete on return from every state change call. This guarantee is a conscious design decision that characterizes both the project itself (and its API boundaries) as well as many of its internals predicated on the same principles. It does explicitly account for asynchronous behavior at the entry point, and at very specific exit points. But introducing async anywhere else in between would mean building a fundamentally different project, and probably revisiting business goals in the process.

      The function color problem isn’t just annoying or tedious, it can be fundamental to whether certain projects are even viable.

      • daotoad 8 months ago

        So, this cascading issue is hitting me right now.

        Being real vague here, in order to support a new capability, we need to insert an asynchronous operation in the middle of what has been a synchronous operation. The synchronous operations is done implicitly and explicitly in many, many places in the app (yes I know this sucks, big old codebases are what they are, and you can only refactor so much at one time while continuing to deliver new features).

        We have a hard ordering requirement where the new async behavior must be completed before the work can continue.

        And it's killing us. What would be a simple case of "call this new function" is spiraling into weeks of work and thrown our planning into question.

        Most of the time it's pretty easy to accommodate the contagion. It's a little more work than is ideal, but it is fine in practice because you hit a natural boundary that stops the spread.

        But in this case, due partly to tech debt and partly due to deep architectural decisions, it's blocking our ability to move forward in a reasonable amount of time.

        We will certainly find a solution, but it's going to be much more expensive to get there than it needs to be.

      • fwip 8 months ago

        To me, this actually reads as a reason for function coloring. Your project makes guarantees that neatly match up to the division between async and sync code, and it would be fundamentally incorrect to ever call a function marked as async from most points of your program flow. So in this case, it would be good that the language enforces this for you, right?

        • eyelidlessness 8 months ago

          Well, yes and no.

          Yes: because the language has distinct semantics for asynchrony which fundamentally* cannot be made synchronous, it is good that the language also makes that fundamental inability explicit, and inviolable.

          No: the above reasoning is inherently tautological! It’s effectively arguing that it’s good that the language doesn’t have a massive inconsistency between two deeply coupled aspects of the same semantics. That is good! But it’s the kind of good you kind of have to expect, or treat as a tragic design flaw in its absence.

          * There are some exceptions, but they’re generally esoteric or niche or have other important caveats.

        • assbuttbuttass 8 months ago

          > it would be fundamentally incorrect to ever call a function marked as async from most points of your program flow

          For a language with async/await yes, you need to bifurcate your program into synchronous and asynchronous pieces, and be very careful to maintain that split.

          For a language with a synchronous thread abstraction like go, you simply don't need to create this artificial bifurcation in the first place

          • fwip 8 months ago

            Yes. The person I replied to described a situation that, to me, is made easier by this bifurcation.

            • eyelidlessness 8 months ago

              Is it though? In a hypothetical JS with the possibility of, say, “decoloring” async functions—e.g. by optionally blocking on their resolved return value—the situation that function coloring helps… ceases to exist as a situation in need of helping in the first place.

              That would be broadly unappealing where single threaded, cooperative concurrency is a baseline assumption! But it’s more fair to say it’s a trade off, and it’s also fair to recognize the things made impossible by a trade off.

  • klabb3 8 months ago

    > But when you're honest with yourself and interpret the word "async" as "expensive", or as "does network calls", it becomes clear that "async/await" makes important features of your function explicit, rather than implicit.

    The main issue here is that whether or not a function is expensive is known upfront. In practice, this bites you when (a) you refactor (the impl changes), and (b) when you’re writing interfaces (the impl is unknown). This causes cumbersome cascading refactors and even complete loss of interop (in case where a 3p interface is sync and your impl is not).

    In order to argue against that point, you have to argue that async is upfront-predictable and obviously part of the interface. And that it should be considered a breaking change - or semantically different if you will.

    • shadowgovt 8 months ago

      Key to note here is that asynchronicity impacts interface because it's an entirely different kind of function in a way that "regular" functions are not.

      Regular functions always return (or crash the program).

      Regular functions run to completion before returning control to the caller.

      These are two key properties async functions lack, so the caller has to care. It's analogous to the notion that checked exceptions are challenging because they also become part of the interface and so the caller is impacted by implementation details.

      (This does raise the question of whether there's a form of async hiding just out of sight of current language designs that's analogous to "unchecked exceptions": can one construct a model of function calling that makes one not have to care about async vs. sync functions at the cost of something else? I suspect, without exploring too deeply, that Orc (https://orc.csres.utexas.edu/) has gone that road).

      • dragonwriter 8 months ago

        > Regular functions always return (or crash the program).

        No, you can't say that; claiming they do is equivalent to claiming the halting problem is trivially solvable since any program it applies to will definitively halt.

        Trivial counterexample:

          def forever():
            while True:
              pass
        
        > Regular functions run to completion before returning control to the caller.

        This is true, but it need not be an inherent distinction of the function, but could be one made between the function, caller, and context: that is, the function can have points at which it can yield control, if it is called in the context of an event loop, it will yield at those points, and the caller can choose either to directly call the function and get sync behavior relative to itself or call it in the context of the same event loop as the caller is running, getting async behavior if the function yields early and the call returning an awaitable value (whether or not there is an early yield.)

        For the way many “scripting” languages are implemented, where there is already a kind of yield to the runtime at IO, this would be a pretty natural implementation and lets a caller treat any function as async or not without caring if it was specifically implemented as async or not, though you’ll grt no real concurrency in a single-threaded runtime without the function having yield points. (This is, AIUI, basically what the async implementation in Ruby does, which is why while there are libraries specifically designed for async use, existing Ruby async-unaware code that does IO can be called as async and works concurrently out of the box.)

        • shadowgovt 8 months ago

          I was speaking loosely and lumped "run forever with no progress" and "crash the program" into the same space; my apologies for the lack of clarity.

          A more precise way to say it is "regular functions either return or we're in a bad place (or we're in a very special circumstance, like 'this program burns cycles on purpose because the scheduler requires something to be running')." For async functions, failure to progress to a returnable state is expected behavior, so the whole system needs to be built around allowing computational progress while waiting for the state to change to where the return can occur.

      • enragedcacti 8 months ago

        > can one construct a model of function calling that makes one not have to care about async vs. sync functions at the cost of something else?

        If I understand it correctly this is technologically possible in Python and there are two main tradeoffs depending on how the event loop is modified. given the following:

            x = 0
            y = 0
            async def set_x():
                global x
                x = await request_x()  # returns 5
            async def set_y()
                global y
                y = await request_y()  # returns 10
            
            def business_logic():
                if x == 0:
                    set_y()            # magic synchronous call to async func
                print(x + y)
            
            async def do_business():
                business_logic()
            
            async def main():
                await asyncio.gather(
                    set_x(),
                    do_business())
                    
        What is the value that's printed? If all coroutines share the same event loop then we don't know because set_y() could yield execution and give it to set_x(). The author of business_logic() may not even know they are writing async code because it just works, and the author of main() assumes that do_business() is concurrency safe because it's async.

        The alternative is nested event loops, so everything in business_logic() gets its own event loop that must fully complete before anything else in the parent event loop can run. This solves the concurrency bug but we've introduced blocking into code that was obviously intended to be async. This approach works (see nest-asyncio package) but the core devs have opted against it because they feel it makes it too easy to break the value of async on accident or out of laziness.

        IMO something that could work is a `sync def` declaration where the function is written in async style but gets its own event loop and can be called synchronously. It solves the tainting issue while still being explicit, easy to convert to full async, and easy to lint for in projects where it would be a net negative.

      • klabb3 8 months ago

        > it's an entirely different kind of function

        Absolutely. Async is sugar for state machines. There is nothing wrong with that per se.

        > Regular functions run to completion before returning control to the caller.

        Yep. However it's important to note that it’s very possible to cancel regular functions IO from a language perspective, using a cooperative signal. (I always squirm a little every time someone says cancelation is easier in async - you shouldn’t imo cancel by dropping a state machine on the floor).

        The best example of this is context.Context in Golang. But even Tokio has CancellationToken.

        That said, the real benefit of the state machine is known size. This is the main (and I would say only) meaningful advantage compared to a regular stack. In Golang, you have dynamic per-goroutine (green thread) stack, which is wasting memory compared to rust async.

  • enragedcacti 8 months ago

    > Yes, when you replace the words async/await with meaningless concepts I do not care about such as color

    My issue is that in some contexts the cost of the function is effectively meaningless. A lot of python scripting falls into this category, where performance is not a concern or there is nothing useful to accomplish in the process while waiting for I/O. At that point any async up the stack is just forcing the user to deal with implementation details they shouldn't need to care about.

    I can appreciate that it enforces good design decisions in some contexts, but marking that up as a pro feels like tasking the interpreter with doing the job of a linter.

    • pxeger1 8 months ago

      Another key thing is that in many languages, you can (and will often enough in practice for it to be a concern) do I/O outside of an async function, using the equivalent blocking API. So in those languages, async does not act as a useful “this function is expensive/impure” marker.

      • colejohnson66 8 months ago

        That's because many languages start without async/await. So, sync overloads stay for eternity for backwards compatibility.

        If I could design a language from scratch, I'd argue for async/await OOTB. For example, the Microsoft Midori team did "async everything" in their C# derivative.[0]

        [0]: https://joeduffyblog.com/2015/11/19/asynchronous-everything/

  • spinningslate 8 months ago

    this is a controversial topic - right up there with static vs dynamic typing.

    I found your post helpful and well written, but in the end I still find myself supporting Bob's blue/red delineation as a helpful articulation of the key limitations with async/await. Why?

    Because the only thing that the async keyword objectively says is "someone, somewhere down the call stack, decided to make their function async". You mention I/O in your post and it might indeed be because of that. You also mention cheap vs expensive and, again, it could be that. But those are heuristics: not objective or definitive. The cheap vs expensive point is particularly problematic (a) because there's no delineation between the two, and (b) because it's different for different use cases.

    Is reading a file expensive? It might be. If I need to read a 1GB file just to pick out 10 characters, then the read is expensive. But if I'm reading that same file to run a stochastic simulation, then the time to read the file might well be insignificant in comparison to the computation. And, besides, I can't run the computation until I've read the input data anyway - so it's not helpful to me that someone else decided that reading files is "expensive".

    The other argument I've seen for marking IO as async is partial failure: IO can fail in ways that local computation doesn't (modulo hardware failures - which can occur, though are much less common). That would be legitimate - except, per above, "async" doesn't definitely mean "does I/O". It only means someone, somewhere in the chain, decided to make their function async. And now I'm constrained to make my functions async too because of its transitive nature.

    I'm not against cooperative multi-tasking per se. I do prefer the developer experience of runtime-supported fine threading such as Erlang processes or goroutines. That's a preference though, and others will feel differently - ref comment on static vs dynamic typing.

    • jayd16 8 months ago

      > It only means someone, somewhere in the chain, decided to make their function async

      Well it specifically means that a function can yield its execution context. That property is taken on when one function calls an async function.

      It has clear implications for very common threading patterns, I think it's right to argue that using blue/red obfuscates that.

      • shadowgovt 8 months ago

        One utility of blue/red is that it applies to other contexts that would cause similar "poison" effects as well.

        Some type systems allow for validated vs. unvalidated data. And then all of a sudden you have a creeping concern that half your functions talk about char* and half your functions talk about String (or, perhaps, a heterogeneous collection of strings: userid and telephone number and the like). Suddenly the code on the outside of the validation boundary can't use the code on the inside without addressing the question of whether their data is valid.

        • jayd16 8 months ago

          Sounds like a feature to me. Same with const functions in c++. I'm not really sure why the articles only ever focus on async await.

          • gpderetta 8 months ago

            You can abstract over const in C++.

  • gpderetta 8 months ago

    There are two issues with async/await annotations in most languages:

    - The annotation doesn't guarantee any particularly useful property: in many languages async functions can still block or take arbitrary time, while on the other hand non-async functions can still lead to reentrancy issues.

    - Most languages do not allow abstracting over async functions: you end up having to write the same higher order function twice even if it would be agnostic over the effectfulness of its parameter.

    Some languages, for example some of those with first class effects and no implicit side effects, you can both enforce the property and abstract over the effectfulness.

    But for most languages the real reason the async annotations exists is because it is a way to syntactically enforce that only a single stack frame can be suspended as opposed to a whole stack, which is tradeoff between usability, and performance and space usage. The supposed benefit is a post-hoc rationalization.

  • jayd16 8 months ago

    Replace "expensive" with "this function can yield" and your argument becomes much stronger.

    The peer replies here are dinging you on the vagueness of "expensive" but it's not really about that. The explicit information given by async is whether a function will yield its execution context or retain control of it.

    If you're leveraging "has control of the thread" as a form of concurrency (eg the UI thread) then knowing a function is sync or async is extremely important.

littlestymaar 8 months ago

Ah my pet peeve reaching the front page again…

The “coloring problem” has nothing to do with async at all, it's just about making effects explicit or implicit in the code.

In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack. Same for checked exception in Java. Unchecked exceptions are like blocking functions, it's invisible so you don't have to manually annotate your functions but as a result any function can now fail without the programmer knowing it.

JavaScript having async/await but unchecked exception may sound bit paradoxical in that regard, but every language has a combinations of both explicit and implicit effects: Rust has both async/await and explicit errors, but it also has panics (implicit errors) and implicit allocation for instance.

I personally live explicit effects, but at the same time when you have too many of them (for example I'd like to have effects like “pure”, “allocating” and “panics”) they start to cause combinatorial explosion of cases in libraries accepting closures as parameter.

Algebraic effects supposedly solve this particular problem but I'm not sure the additional conceptual learning effort is sustainable for a mainstream language, so in the meantime I think every language must keep its amount of explicit effects under a certain threshold.

  • paldepind2 8 months ago

    I think there's something important and correct in this take.

    As another example to your point, whether a function returns a list of foo's or a single foo is a also a color. You have to do completely different things when calling a function that returns a list compared to one that just returns a single value. And that's not just empty pedantry. Haskell let's you treat "list" as an effect (since it is a monad). Array languages like APL, J or R are colorblind with respect to "a list"/"not a list" as the difference is erased (to a large extend). So there really is a whole rainbow of function colors (basically types).

    On the other hand, this point of view doesn't really invalidate the original coloring argument at all. The effect encapsulated by "async" really is one that can be annoying and one that can actually be made implicit with a result that many would prefer.

    • feoren 8 months ago

      This is exactly why I like all of this being handled in the return type. Return a Promise<T> or a Task<T> or a Stream<T> or an IEnumerable<T> as appropriate. Every function always returns immediately, but it might not do the important work immediately -- instead, it plans out the important work, which you can plug together as you see fit with other work and execute if you want, or simply return a composite plan for someone else to execute. All of its various "colors" are explicit in the return type.

      Combinatorial explosions are wrapped up in the Ts themselves. Sure, you might sometimes get a Stream<Array<Stream<T>>>, but if that's simply the appropriate type to describe what your function does, then who's complaining? As long as you have various ways to manipulate those types, like "Combine" (Array<Stream<T>> -> Stream<Array<T>>) and "Flatten" (Stream<Stream<T>> -> Stream<T>) then you can handle everything. And you'll find that getting your types correct increasingly implies that your entire code is correct, so the compiler is helping you out a lot.

  • randomdata 8 months ago

    > In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack.

    Not unless you're doing it wrong. Errors need to be handled. That may, in some cases, mean handling it with another error, but that isn't adding any "color". The new error is independent of any previous error state.

    • littlestymaar 8 months ago

      You're misunderstanding: the fact that the new error is different from the original one doesn't change the fact that the function returns an error. Exactly like how async functions awaiting on a promise to create new promises doesn't change the fact that the function is an async one.

      Sure sometimes you can avoid propagating the error upward, but that's not the most common case and it also happens with async/await (if you don't need the result of the promise in the caller function you don't need to await and to make it a async function).

      In Rust the similarity is very flagrant because there are one postfix operators for both (? and .await).

      • assbuttbuttass 8 months ago

        > if you don't need the result of the promise in the caller function you don't need to await and to make it a async function

        Why call it at all, then? In Rust, futures don't run unless you poll them, so just calling an async function without polling it doesn't do anything.

        In contrast, for error returns it's very common to log an error but continue with some default value, or just skip processing one item and continue with the rest. There are a lot of cases where errors can be handled immediately, rather than always blindly bubbled up the call stack

        • littlestymaar 8 months ago

          > Why call it at all, then? In Rust, futures don't run unless you poll them, so just calling an async function without polling it doesn't do anything.

          Rust is a bit special as the futures are lazy and you need to call your future with `task::spawn` if you want this to work, but it still does. And in other languages it's going to work directly without doing anything in addition to calling the async function.

          > In contrast, for error returns it's very common to log an error but continue with some default value, or just skip processing one item and continue with the rest. There are a lot of cases where errors can be handled immediately, rather than always blindly bubbled up the call stackm

          It really is use-case dependent but I don't think it's more common than letting the request run in the background without waiting for its result. In fact your log example is a funny one because as soon as you use remote logging you're going to let the request run in the background and never await it (otherwise you'd get tons of gratuitous latency).

      • randomdata 8 months ago

        You're misunderstanding. async/await creates a dependence. errors do not – at least no more than any other type. Would you say your perspective on "coloring" is equally applicable to email addresses?

        I might buy into your idea the "?" operator colors functions. But that's something else entirely and obviously isn't what you were talking about originally as it isn't found in all of the languages originally mentioned.

        • littlestymaar 8 months ago

          > async/await creates a dependence. errors do not.

          It does! In both case you need to "unwrap" the value, and in most case it means making the caller function async or returning the error. (As an anecdote I've done way more refactoring involving replacing T with Result<T> in entire call stacks in Rust that I've done it for async).

          > I might buy into your idea the "?" operator colors functions. But that's something else entirely and obviously isn't what you were talking about originally as it isn't found in all of the languages originally mentioned

          It's exactly the same with Java's checked exception and Go errors, as soon as you start calling a function that returns an error, you're going to write something that looks like

              if err != nil {
                return 0, err
              }
          
          And tada! your functions now need to add the error to it's return parameter.
          • randomdata 8 months ago

            > It does! In both case you need to "unwrap" the value

            Just as you do with any value, such as an email address, returned by a function. It is curious that you avoided my question as it would help us better understand where you are trying to come from. If that is the stance you are trying to take you're not exactly wrong about there still being some sort of dependence – there is an inherit dependency created when calling a function, always - but there is not a different color of dependence found within that. You're missing the intent of "coloring" if that is what you are trying to say.

            async/await introduces a different type of dependence than a function would normally present, where the dependence is no longer direct. I may even agree that the "?" operator introduces a similar indirect dependence. But a function returning an email address (or error; the type of the value is immaterial) is the "natural color" of a function. If that is all you have, there isn't a secondary color.

            > as soon as you start calling a function that returns an error, you're going to write something that looks like

            Only for internet memes. You would never write production code like that for so many reasons.

            • littlestymaar 8 months ago

              > you do with any value, such as an email address, returned by a function. It is curious that you avoided my question as it would help us better understand where you are trying to come from

              I don't understand what you mean with your email address example. If you ask a function for an email address, and you get an email address you don't have to unwrap anything.

              > You're missing the intent of "coloring" if that is what you are trying to say.

              I'm not, really. The key thing is that as long as you have some effect (like "an error occurred" or "the execution is suspended while running some IO operation", “this function is allocating”, “this function is impure”, etc.) happening somewhere in the call stack then it infects all the call stack no matter what the language does. (A previously pure function calling an impure one becomes impure, a non-allocating function calling an allocating one becomes allocating itself, etc.)

              In some language, this effect is visible to the caller by default (but can generally be opted-out) and you have this kind of red/blue split for any effect. In other languages this effect is just hidden (but it still exists on reality), that means less effort annotating your code but also less information when reading other people's code. It's exactly the same tradeoff as typing.

              > Only for internet memes

              Not literally like that, but the gist of it still stands, you end up adding an error return value to your functions the vast majority of the time.

              • randomdata 8 months ago

                > I don't understand what you mean with your email address example.

                But you didn't think to ask any followup questions to help with your understanding?

                > The key thing is that as long as you have some effect [...] happening somewhere in the call stack then it infects all the call stack no matter what the language does.

                In the same way email addresses do, I suppose. But that is the "natural color" of a function. Consider:

                    fn getEmail() {
                        return "joe@example.com"
                    }
                
                    fn getPerson() {
                        email = getEmail()
                        return "Joe Blow", email
                    }
                
                    fn getUser() {
                        name, email = getPerson()
                        return "joeblow123", name, email
                    }
                
                    fn main() {
                        username, name, email = getUser()
                        print(username, name, email)
                    }
                
                Yes, you could say in a sense `getEmail` "infected" `getUser` in its need to get the email address to the top of the stack, but the dependence at each step remains direct. getUser returns an email address because an email address is relevant to the result of `getUser`, not just because it happened to call `getEmail`.

                That is different to async/await, where `main` now has to become aware of what `getEmail` is doing because `getUser` happened to call it transitively. The dependence is indirect.

                    async fn getEmail() {
                        return background { return "joe@example.com" }
                    }
                
                    async fn getPerson() {
                        email = await getEmail()
                        return "Joe Blow", email
                    }
                
                    async fn getUser() {
                        name, email = await getPerson()
                        return "joeblow123", name, email
                    }
                
                    async fn main() {
                        username, name, email = await getUser()
                        print(username, name, email)
                    }
                
                I may be inclined to agree that the "?" operator can introduce a similar indirect dependence:

                    fn producesError() {
                        return ProductionError("an error occurred")
                    }
                
                    fn doesSomething() {
                        value = producesError()?
                        return true   
                    }
                
                    fn main() {
                        result = doesSomething()
                        match result {
                            ProductionError:
                                print("Error", result)
                            OK:
                                print("OK", ok)
                        }
                    }
                
                Like async/await, `main` now has to be aware of what `producesError` is doing without calling it directly. But, then again, much like as we discussed in your Go example, you'd never actually write production code like that. There are so many things wrong with it. So, I'm not sure how significant that coloring actually is. And, regardless, is not related to the original discussion.
                • littlestymaar 8 months ago

                  Your email address example is backwards: you can indeed get function coloring with just regular functions but its not the return value that causes it, but the input parameters. Consider this:

                      function foo(name){
                          console.log(`Hello ${name}`);
                      }
                  
                      function bar(name){
                        foo(name)
                      }
                  
                      function top(user){
                        bar(user.name)
                      }
                  
                  If the `foo` function at the bottom of the stack needs some additional input (say the email address), then you'll need to pass it down and modify the signature of every function above:

                      function foo(name, email){
                          sendMail("Hello ${name}", email);
                      }
                  
                      function bar(name, email){
                        foo(email)
                      }
                  
                      function top(user){
                        bar(user.name, user.email)
                      }
                  
                  As you can see, "My function needs some particular data" is also an effect that will transitively contaminate the call stack above (in "indirect" manner to reuse your wording).

                  In fact this is already apparent in the original blog post since the first two third are about callback-based concurrency, in which the coloring comes as a function parameter.

                  > But, then again, much like as we discussed in your Go example, you'd never actually write production code like that. There are so many things wrong with it.

                  No matter how you end up writing your code, there is only two alternative: you either address the problem and then you don't have to propagate the error (this is the minority case by a large margin) or you need to propagate it upwards (which happen at least 90% of the time).

                  > So, I'm not sure how significant that coloring actually is. And, regardless, is not related to the original discussion.

                  It is exactly as significant as async coloring. That doesn't mean the problem it causes is big though, the "async coloring problem" is vastly overblown and everybody deals with strictly equivalent "effect problems" all the time without even thinking about it, which was my original argument at the very top of this thread.

                  By the way:

                  > But you didn't think to ask any followup questions to help with your understanding?

                  When you use this kind of passive-agressive tone, don't be surprised if a charitable reader simply discards it, the less charitable thing to do instead would be escalating in aggressiveness.

                  • randomdata 8 months ago

                    > Your email address example is backwards: you can indeed get function coloring with just regular functions but its not the return value that causes it, but the input parameters.

                    Uh, what? Convention in Go and Rust sees errors be returned. Earlier, when you defined the topic of conversation, which hasn't changed, you even said "In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack.", so you seemingly knew this at one time. Where did you get the idea that they are taken as input parameters now?

                    > or you need to propagate it upwards

                    Perhaps like you need to propagate email addresses. But that's the "normal color" of a function. If that is all you are doing, there is no secondary color. That's just "normal" use of a function.

                    async/await, and perhaps even "?", are not "normal" as they introduce to the caller dependencies on functions the caller didn't even call. This breaks the "black box" nature of a function, seeing implementation details leak, thereby no longer guaranteeing that a function is "black" and thus establishing functions that are of other "colors".

                    > When you use this kind of passive-agressive tone, don't be surprised if a charitable reader simply discards it

                    Oh, I wasn't surprised. It was clear from your first comment that your emotions aren't letting you think straight.

                    • littlestymaar 8 months ago

                      > Uh, what? Convention in Go and Rust sees errors be returned

                      Yes, but that's irrelevant. "Regular" return values don't have this coloring effect, but input have. It's a covariant / contravariant kind of thing. You should really re-read my previous comment if it went over your head.

                      > Perhaps like you need to propagate email addresses

                      No, again you get this backwards, you don't need to propagate the email address, errors suppose a specific kind of control flow that is different than non-error values. Again the correct analogy is the input type, not the return type. Understanding this is a prerequisite of understanding the whole concept so you should really take some time thinking about it.

                      > Oh, I wasn't surprised. It was clear from your first comment that your emotions aren't letting you think straight.

                      You're projecting your own mindset on me, I've been neither emotional nor aggressive in this entire thread, despite repeated attempt on your side to heat things up.

                      • randomdata 8 months ago

                        > Yes, but that's irrelevant. "Regular" return values don't have this coloring effect

                        It is certainly not irrelevant as that is the very topic of conversation. The topic of conversation was clearly and specifically defined by "In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack." But you're right to finally come around to realizing that there is no coloring effect created by that. Glad you were able to learn something today!

                        > errors suppose a specific kind of control flow that is different than non-error values

                        They might want a different kind of control flow in some circumstances, but it is not supposed. Email addresses might also want a different control flow in some circumstances (e.g. a "local" address).

                        > Again the correct analogy is the input type

                        It is quite possible to introduce an indirect dependence through inputs as much as it is possible through outputs, if that is what you are trying to say. I'm not sure your example successfully demonstrates that, though. There is nothing to suggest that email isn't relevant to `bar`, even if the actual "black box" implementation happens to call upon `foo`. I see no evidence of implementation details leaking, which can be demonstrated by removing `foo` and refactoring `bar` to:

                            function bar(name, email){
                              sendMail("Hello ${name}", email);
                            }
                        
                        > You're projecting your own mindset on me

                        No, but I read what you wrote. Perhaps I have misinterpreted it? What did you mean by "tone" if not something that pertains to your emotions?

                        > despite repeated attempt on your side to heat things up.

                        Admittedly, in context, this too reads like you are referring to your emotions. But as you also seem to be trying to assert that they are not weighing on you, what are you actually trying to say here? You have not made yourself clear, I'm afraid.

                        • littlestymaar 8 months ago

                          > The topic of conversation was clearly and specifically defined by "In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack."

                          Returning an error has this property, yes. And errors Alps have this property when they are propagated separately from the return value (like unchecked exception in Java). Returned values do not have this property.

                          > . Glad you were able to learn something today!

                          I've tried with much patience and good faith in spite of your aggressiveness to explain this things in ways you can understand them so you could actually be learning something, but it looks like your ego is too fragile for that.

                          That's really unfortunate for you as it's the recipe for perpetual mediocrity. Good day.

                          • randomdata 8 months ago

                            > Returning an error has this property, yes.

                            Only in the same way that returning an email address does. That does not change the color, though. It remains "black". This is just "normal" use of a function.

                            Yes, you can go out of your way to leak implementation details making the function no longer "black". async/await, use of the "?" operator, "return err", even checked exceptions all introduce leakage. That can result in a colorful rainbow of functions, but "unless you are doing it wrong" was caveated at the very beginning. The very first sentence I wrote, in fact.

                            Is that the source of your confusion, perhaps? That you somehow forgot to read the very first sentence? async/await is the only one that isn't yet generally considered a bad practice, and only because it is quite a bit newer than all the others, as we always learn in the end that leaking implementation details leads to pain.

                            > I've tried with much patience and good faith in spite of your aggressiveness

                            I really don't understand what you are trying to say here. Aggressiveness? Patience? Good faith? These have no applicability to the conversation, so I am, again, not sure what you are trying to say. Further, these are words that are usually used around emotion, which we both seemingly agreed does not shape our reading of the discussion, so I have no idea what else they could be referring to. What do you mean?

  • pistoleer 8 months ago

    > In Go or Rust, returning an error is also a “color” that spreads out to the top of the call stack.

    That's wrong... Suppose A calls B and B returns a Result<T> (so it's colored as you say). A can match both the Some() and the Err() variants of B and return something other than the error in the latter case. So A can shed the color. There is no way to "shed" async-ness like that in javascript.

    • littlestymaar 8 months ago

      > There is no way to "shed" async-ness like that in javascript.

      In rust you can simply call `block_on` and call it a day. In JS there isn't such a construct by default because the blocking the thread would freeze the app, but it exists in every other language with async/await that I'm aware of.

mattxxx 8 months ago

I've spend a lot of time writing things using async in rust, python, and typescript, and I still find it un-intuitive / conceptually incorrect.

Once you're in an async function the rules then fundamentally change for how everything operates; it's almost like programming within a dialect of the same language. In particular, I'm referring to everything from function calling, managing concurrency, waiting on results, sleeping threads.

Comparatively, when you're in a go-block in go, you're still writing within the same dialect as outside of it.

  • mattgreenrocks 8 months ago

    It ultimately amounts to a dialect. It seems confusing because it is a well-supported dialect in each language, which makes you think it is first-class.

    The JVM's virtual threads approach is the right way. The runtime should be able to do everything needed to deal with async, and I, the developer, can write simple blocking-style code. Currently there are still issues around pinning when using the older concurrency APIs, but I'm hopeful they can push through them.

omgbear 8 months ago

I've thought about this a lot in relation to typescript over the years and had various opinions -- For some time I thought it'd be better if there was an implicit `await` on every line and require `void` or some other keyword to break execution like `go` in Golang.

But, eventually I realized the difference in pre-emption between languages -- Go can (now) preempt your code in many places, so locks and thread-safety are very important.

The javascript runtime only preempts at certain places, `await` being one. This means I can know no other code can be running without explicit locks around all critical sections.

Finally understanding the trade-offs, I no longer am as frustrated when recoloring a bunch of functions. Instead, I can appreciate the areas where I'm not required to lock certain operations that I would in other languages.

  • sakex 8 months ago

    There is no preemption in Javascript. It is based on cooperative multitasking[1] (your await statements and non-blocking callback) which is the opposite of preemption.

    [1]https://en.wikipedia.org/wiki/Cooperative_multitasking#:~:te....

    • ndndjdueej 8 months ago

      As anyone who has done the old setTimeout(fn, 0) trick in the browser probably knows.

      • Feathercrown 8 months ago

        They even created setImmediate(fn) for this purpose

    • orlp 8 months ago

      If every line had an implicit await then it is indistinguishable from pre-emption, which I think is the point the person you're replying to is trying to make.

recursivedoubts 8 months ago

My web scripting language, https://hyperscript.org, tries to hide the difference between sync and async by resolving promises in the runtime. My theory is that this is something that web script writers should not be concerned with (this theory makes more sense when you consider hyperscript is a companion to htmx, and favors a particular approach to scripting[1].)

  on click
    fetch /whatever as json
    put the result's data into #some-div
    wait 2s
    put '' into #some-div
You don't have to mark the script as sync or async, and the runtime will resolve everything for you, making everything feel like synchronous scripting.

This obviously has limitations and foot guns, but it works reasonably well for light scripting. More info here:

https://hyperscript.org/docs/#async

And the implementation of it in the runtime here:

https://github.com/bigskysoftware/_hyperscript/blob/c81b07ce...

https://github.com/bigskysoftware/_hyperscript/blob/c81b07ce...

--

[1] - https://htmx.org/essays/hypermedia-friendly-scripting/

  • paldepind2 8 months ago

    I'm curious how you would you do something like the following? Is that just not supported?

        const promise1 = job1();
        const promise2 = job2();
        const [result1, result2] = Promise.all(promise1, promise2);
    • recursivedoubts 8 months ago

      If you want job1 and job2 to be executed in parallel, you could do this:

        set result to [job1(), job2()]
        set result1 to the first result
        set result2 to the last result
      
      The array expression will wait until all promise values resolve.

      This isn't really what hyperscript is designed for, however: its async behavior is designed instead to take the async/sync distinction (and callback hell) out of "normal" DOM scripting. If you have sophisticated async needs then JavaScript is probably a better option. The good news is that hyperscript can call JavaScript functionality in the normal way, so that is an easy option if hyperscripts behavior isn't sufficient for your needs.

      • paldepind2 8 months ago

        Thanks for the reply. I can understand that the purpose here isn't to be as powerful as JavaScript, but rather to be simpler and easier. That being said, is there a way to achieve the below or is that beyond the limits?

            const result = Promise.race(job1(), job2());
        • recursivedoubts 8 months ago

          You can't achieve that with the intended semantics directly in hyperscript, because it will resolve all argument promises before invoking Promise.race()

          You could move that out to JavaScript and then call that function from hyperscript:

            set result to jsPromiseRace()
          
          it could also be done as inline js:

            js
              return Promise.race(job1(), job2());
            end
            log the result
          
          but I think if you are writing this style of code it makes more sense to just kick out to JavaScript for it.
    • gpderetta 8 months ago

      You could imagine an hypothetical language with implicit awaits but explicit forks:

          const promise1 = async job1();
          const promise2 = async job2();
          const [result1, result2] = Promise.all(promise1, promise2);
vitiral 8 months ago

This is one of the things I love most about Lua. You can coroutine.yield inside of any function, it's up the the caller to handle it correctly.

This means I can write my tech stack to run in either mode, then swap out async/sync functions in the application. This is exactly what I do in https://Lua.civboot.org#Package_lap

fleabitdev 8 months ago

I've spent some time thinking about language design recently. Coloured functions have a surprising number of downsides:

- They're sometimes much too explicit. When writing a complicated generator, I don't necessarily want to annotate every call to a sub-generator with `yield*`, especially if I need to drill that annotation through wrapper functions which aren't really generators themselves.

- Colours show up everywhere. If you have several functions with a `context: GodObject` parameter, then that's a function colour. Most third-party code will be unable to forward a `context` argument to a callback, so you'll have to manually smuggle it in using closures instead.

- Different "colour channels" don't compose nicely with one another. Even though JavaScript ES2017 provided both async functions and generators, ES2018 had to add multiple new pieces of syntax to permit async generators.

- It's normally impossible to write code which is generic over a function's colours. For example, if you have `function callTwice(f)` in JavaScript, you'd need a separate `async function callTwiceAsync(f)`, and a `function* iterateTwice(iterable)`, and an `async function* iterateTwiceAsync(iterable)`, despite the fact that all of those functions are doing the same thing.

Several small languages are experimenting with algebraic effect systems [1], which would make all functions colourless. If JavaScript had this feature, the syntax for defining and calling functions would be the same, no matter whether you're dealing with a normal function, a generator, or an async function. No more `async`, `await`, `function*`, `yield`, `yield*`, `for-of`, `for await`...

This can make everything too implicit and vague. The state of the art is for algebraic effect systems to be implemented in a statically-typed language, which then uses type inference to quietly tag all function types with their colours. This means that the compiler can enforce requirements like "you can only call generators within a scope which is prepared to collect their results", but that rule wouldn't prevent you from wrapping a generator in `callTwice`.

[1]: https://github.com/ocaml-multicore/ocaml-effects-tutorial

  • magicalhippo 8 months ago

    > Most third-party code will be unable to forward a `context` argument to a callback

    Having a context argument which is passed to the callback has been par for the course in all cases I've seen, but I've mostly been in C land and related areas.

    Though, if you have closures, should you still prefer a context param?

    > If JavaScript had this feature, the syntax for defining and calling functions would be the same, no matter whether you're dealing with a normal function, a generator, or an async function.

    How would one reason about performance and possibly concurrency in such languages?

    • fleabitdev 8 months ago

      In a language with algebraic effects, questions like "how should I make this `CancellationToken` available to these forty small functions?" simply go away :-)

      It's ambient state, just like a `catch` block, and so you can make it ambiently available to a whole tree of function calls. The outcome is the same as passing around a context argument by hand, and it can have equally strong type-checking, but the compiler handles all of the boilerplate for you.

      To answer your second question: I agree that, in a dynamic language like JavaScript, making all of these language features implicit could be too costly. I'm more optimistic about effect systems in statically-typed languages.

      • magicalhippo 8 months ago

        > ambiently available to a whole tree of function calls

        So a "localized global variable" of sorts? Sounds interesting, got any links to examples I could look at?

        • fleabitdev 8 months ago

          The term is "dynamic scope": https://en.wikipedia.org/wiki/Scope_(computer_science)#Lexic...

          It isn't a very common language feature, but you could take a look at React's `Context` or Common Lisp's `defparameter`.

          • magicalhippo 8 months ago

            Ah, like Python objects then. Which is unfortunate, because that's one of my main dislikes with languages like Python.

            I was imagining more along the lines of (on-the-spot pseudo-code)

                using [x := foo(...)]
                {
                  bar();
                  output();
                }
            
                func bar()
                {
                  x = x + 1
                }
            
            where bar() and output(), or something they call in turn, can reference x and it would be a compile-time error to use bar() or output() in cases where they couldn't resolve x. Or something more along those lines.

            Anyway, cheers, always fun to ponder these things.

            • fleabitdev 8 months ago

              Sorry, the Wikipedia article isn't very helpful. Your example is the correct one.

              The killer feature is that you can shadow a dynamic variable, changing its value for the duration of a dynamic scope, in the same way that an inner `catch` handler overrides an outer one.

              For example, you could have a dynamic variable which tells the `print` function where to send its text output. The variable's value would probably default to stdout. By temporarily assigning a string-stream to that variable, you could collect an arbitrary function's text output into a string instead. Regardless of where the text is going, any function which produces text could still use the convenient global `print` function.

              • magicalhippo 8 months ago

                Right. The closest thing I'm familiar with is the dynamic context used in Serilog[1], where you add log properties to the current scope, which gets added to each log line generated in that scope, and removed once the scope exits.

                Of course, no compile-time checking there, so the combination sounds indeed quite useful.

                https://github.com/serilog/serilog/wiki/Enrichment#the-logco...

throwaway313373 8 months ago

A slightly separate but still related question that bothers me every time I, a Python programmer, read about function coloring and different approaches to concurrency:

Does anyone understand why asyncio-style approach to async won over gevent-style in Python? Why was the former approach accepted into to stdlib, got special syntax and is preferred by the community while the latter is a fringe niche thing?

  • 0cf8612b2e1e 8 months ago

    I hope there that was the result of some back room deals made in cigar filled rooms. gevent feels more pythonic in the amount of things that effortlessly worked.

  • zahlman 8 months ago

    The async keyword takes over from the asyncio standard library module; the relevant PEP for the former is https://peps.python.org/pep-0492/ and for the latter is https://peps.python.org/pep-3156/. That might give some hints.

    • throwaway313373 8 months ago

      Unfortunately, 3156 doesn't really have an explanation why did Guido chose this particular implementation. It just says "yeah, that's what we gonna do". And 492 was written when tulip/asyncio was already chosen and included into stdlib.

bazoom42 8 months ago

It is similar to IO in Haskell which also force you to explicitly declare it in the signature (and in turn force callers to be IO), where most other languages (even functional) allow you to do IO anywhere.

immibis 8 months ago

There is also coloured data: stack vs heap, threadsafe vs not, disk vs memory vs SQL, mutable vs immutable, etc ...

Another comment: Python's async/await is precisely syntactic sugar for generators. It just changes the keywords and checks that you only use await in a function labeled async.

Edit: my rate limit appears to have been increased.

jpc0 8 months ago

I think the main issue with async/await specifically is that it is abstracted away from most developers.

I may have a sync program running a main loop and I want to have a subset of that program using something like the reactor pattern. In any multithreading aware programming language I can do that by just sticking that in another thread and syncing however I want, even using coroutines I can do that, it doesn't need to follow the reactor pattern.

However libraries (tokio) and languages (js) don't make that the obvious default. In something like JS it isn't even possible, the entire language is built on top of coroutines. For tokio the default is "put tokio main here and voila async" and you need to actually understand what it's doing under the hood to know that that isn't the behaviour you want in all cases.

This is more bottom-up vs top-down learning. Most people learn and teach top-down because it gets you to productive really quickly. Bottom-up is much harder and has the chance of getting lost in the weeds but makes what is happening very obvious.

Does tokio use reactor or coroutines? It depends...

withinboredom 8 months ago

This is how we got "fibers" in PHP and they are absolutely worthless. With async/await, I can choose or choose not to wait for the result. Or I can trigger the work asynchronously and wait on it later, in an entirely different function. With Fibers, you cannot choose. You must wait, and you must wait now.

ks2048 8 months ago

Do any relatively-popular languages avoid “two colors” by essentially making everything async?

  • rererereferred 8 months ago

    Zig was[0] going to make functions "colorblind" by not needing the function to be declared differently. Just a configuration in your root file[1]

    [0] I say was because I don't know the current status of async in zig or if it will come back in the future. [1] https://kristoff.it/blog/zig-colorblind-async-await/

pornel 8 months ago

This article gets referenced a lot, but it does a poor job of defining what color actually is.

The biggest issue the article describes — inability for sync code to wait for an async result — is limited mostly to JavaScript, and doesn't exist in most other languages that have async and a blocking wait (they can call red functions from blue functions).

If this architectural hurdle is meant to be the color, then lots of languages with async functions don't have it. This reduces the coloring problem down to a minor issue of whether async needs to be used with a special syntax or not, and that's not such a big deal, and some may prefer async to be explicit anyway.

  • enragedcacti 8 months ago

    Python also has it. You can call async code with `asyncio.run` but it is extremely limited by the fact that nested event loops are prohibited. Any function that uses it becomes extremely brittle because it can't be called from async functions unlike all other synchronous code.

    This is for the most part an intentional design decision to "make sure that when the user is already in async code they don't call the sync form out of laziness." [1]

    [1] https://github.com/python/cpython/issues/66435#issuecomment-...

dang 8 months ago

Related. Others?

Ruby methods are colorless - https://news.ycombinator.com/item?id=41001951 - July 2024 (235 comments)

On 'function coloring' (2018) - https://news.ycombinator.com/item?id=36199499 - June 2023 (21 comments)

I Believe Zig Has Function Colors - https://news.ycombinator.com/item?id=30965805 - April 2022 (158 comments)

In Defense of Async: Function Colors Are Rusty - https://news.ycombinator.com/item?id=29793428 - Jan 2022 (101 comments)

The Function Colour Myth, Or: async/await is not what you think it is - https://news.ycombinator.com/item?id=28904863 - Oct 2021 (7 comments)

What color is your function? (2015) - https://news.ycombinator.com/item?id=28657358 - Sept 2021 (58 comments)

What Color Is Your Function? (2015) - https://news.ycombinator.com/item?id=23218782 - May 2020 (85 comments)

What Color is Your Function? (2015) - https://news.ycombinator.com/item?id=16732948 - April 2018 (45 comments)

The Function Colour Myth (or async/await is not what you think it is) - https://news.ycombinator.com/item?id=12300441 - Aug 2016 (4 comments)

What Color Is Your Function? - https://news.ycombinator.com/item?id=8984648 - Feb 2015 (146 comments)

from-nibly 8 months ago

Wouldn't adding a way to block on an async function make javascript colorless?

No one would want that though right? Like in what context would you even have a consequenceless block? A CLI maybe?

  • shadowgovt 8 months ago

    You got the meat of the situation in these four sentences.

    Yes, mitigating the color issue in JavaScript is as "trivial" as allowing the `await` keyword inside non-`async` functions. And what that would mean more-or-less makes sense: the runtime sleeps the active execution thread and resumes it if and when something else in the runtime satisfies the await.

    But then you can't know, as the caller, whether any function has "sometimes this function just never returns" semantics without inspecting the whole call chain of every function you call (an arguably impossible task), and that's worse. Especially because the dominant use-case for JavaScript is browsers, where just blocking the UI thread forever is extremely bad.

    The fact `await` can only show up in a function declared `async` is a feature, not a bug---it saves the developer from a type of hell where they can't know, in the general case, if a function never returns.

    • dragonwriter 8 months ago

      > But then you can't know, as the caller, whether any function has "sometimes this function just never returns" semantics without inspecting the whole call chain of every function you call (an arguably impossible task), and that's worse.

      You have exactly as much guarantee of this with async functions as with sync ones, though, but if there is a reason to worry about an async function where it is being called from a sync function and you need to provide a better guarantee, Promise.race() it with a timeout and...problem solved.

dmvdoug 8 months ago

Maybe Chomsky was on to something…

Colorless green functions async/await furiously.

Rapzid 8 months ago

I still haven't seen a good "solution" to this "problem" for imperative style programming. Why the quotes? Because the functions have different signatures, thus different functions(colors).

And IT MATTERS. That's why you have structured concurrency hot on the heels of Loom. And once you are using structured concurrency constructs... Ehh, it's not the magic "solution" we were sold anymore is it?

  • HiJon89 8 months ago

    Not sure I follow. The function coloring problem is still solved in that scenario. You can apply structured concurrency to any function (there’s no colors), and any function can contain structured concurrency within it (ie, using structured concurrency doesn’t color your function)

    • Rapzid 8 months ago

      You don't follow. Nothing against you in particular, but this is why we've had 10+ years of "color problem" articles and no real solutions. The problem isn't the colors, it's the ergonomics.

      The naive solution doesn't work because it turns out the difference between functions that return a value and functions that return a promise matters..

      The other approaches destroy ergonomics.

  • immibis 8 months ago

    The article is not about functions having different type signatures but being somehow fundamentally different. It occurs every time we try to qualify and enforce restrictions on functions, whether it's async, pure, nonblocking, or whatever. Although they aren't all harder to call, they always cause problems with higher order functions. An idea of "effect generics" tries to improve upon this - you write something like

    template<effect T> T_function List filter(List list, T_function pred)

    (element type committed for brevity) and then filter is async if the predicate is async, etc - it's pure if the predicate is, nonblocking if the predicate is. If you continue in this direction, your language gets more complex than C++. The other options are to just accept that you need a separate filter function for async and non async predicates, or to do away with function flags (doesn't work for CPS style async of course).

    • Rapzid 8 months ago

      This article is about exactly what I'm addressing. I stopped reading after your false premise.