8 ms·
What color is your function? (2015)
- fastforwardius 5mo agoHaving functions literally color coded based on type would be nice improvement.
- frankfrank13 5mo agoMy 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"
- gordonhart 5mo agoFor 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.
- graemep 5mo agoI 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).
- seabrookmx 5mo agoYou 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.
- bigstrat2003 5mo agoPerformance aside (which I would argue is premature optimization, as most programs will not feel the theoretical overhead of threads), async is a bad approach for developer ergonomics. Threads are so much easier to work with and reason about than async. There are reasons to use async (like if you're in the rare case when thread overhead is noticeable), but developer ergonomics are absolutely not a reason.
- seabrookmx 5mo agoSay I need the results from two expensive REST API calls, so I want to run them concurrently. Managing a thread pool you find a _better_ experience than one, two = await asyncio.gather(callOne(), callTwo()) ?
- wtetzner 5mo agoDoesn't Python support futures? with ThreadPoolExecutor() as executor: one, two = executor.map(lambda f: f(), [callOne, callTwo]) I'm sure you could write a nicer helper function that's more similar to gather as well.
- seabrookmx 5mo agoSure, in a web server context you'd probably want to instantiate that executor globally and re-use it, Then you could write helper functions around that. But it's still considerably more code and legwork than the async alternative, while also being slower and using more memory if you care about those things.
- ian_j_butler 5mo ago> something like Celery with all it's underlying infrastructure. Unpopular opinion, but combining this with the other "no thanks" sentiments in this subthread is the right answer. Your app is so complicated you need async? Then it's complicated enough that you can benefit from infrastructure. I don't want to watch coworkers try to badly rebuild message queue or scheduling semantics in an application code base. Just use infrastructure that's made by people who know what they are doing. That was problematic in 2015, but in 2026 it's a bit of docker, and it's not just about web/microservices. Very easy for sufficiently complex apps to simply leverage a local sandbox of celery, redis, graphdb's and whatever. Stand-alone is overrated since we don't have to do it anymore.. app devs should get more comfortable working with ensembles like this so they have access to best-in-class solutions. You don't like infrastructure AND have such a need for performance AND don't want threads or multiprocess? Consider using another language. Async is mostly a solution in search of a problem, and the enduring popularity of TFA goes to show this has been the right conclusion for ~10 years.
- nickcw 5mo agoGo 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 :-)
- vq 5mo agoAnd 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.
- jerf 5mo agoThis is a subtle point that I've seen missed repeatedly, but: The reason that "color" is important is that if you have a function ten layers down in your stack that is the wrong "color", you now have to change that top-level function. There is no other option. Propagating errors up the stack is not the same, because the top-level function is not developing an error return because of the 10-level-nested function. It is developing one because the function it called has one, and apparently, it needs to return it to its local caller. It's a local consideration. It is true that it may be a recursive local consideration where this was true 10 times, but the reason it is different is that it doesn't have to be that way. It could have been the case that the function 7 layers down handled the error somehow and it stopped propagating up the stack. But at each point, the consideration was local, and as such, amenable to local solutions other than just tossing the error up. If you choose to "correctly" plumb the error through all your functions, well, good on you for apparently being willing to apply good software engineering practices even when it's annoying, but this is just normal day-to-day function activity stuff. By contrast, in a function coloring situation, if the color is wrong 10 layers down, you must change the calling function. It's a non-local consideration. You don't get to decide not to change it. You can't encapsulate it. You don't get a choice. It pollutes the entire stack, forcibly. Another way to look at it is, if the function 10 levels down developed what you think is a color, but there is a way for the function 9 levels down to hide the color from the rest of the stack, even via a hack like simply dropping an error you really need or hackily constructing an object of some type to pass in, then it is by definition not a color. A color change can't be stopped by any way of writing an intermediate function. It must be propagated all the way up the stack. If you don't have this, you don't have "color". Like, some people will say that in their language that maybe there is some way to encapsulate "async". If you can, then you don't have an async color. Although I will say that if your "encapsulation" is basically to run it in a non-concurrent environment, that's really not encapsulation. It isn't really "encapsulation" if you're giving up an entire major feature of the language, because that is something very visible to the rest of the program. Go's context.Context is similarly not a color. You can always just create a context.Background() and pass that down. If you didn't have any context already in hand, which means you must not care about any of the features context offers, then that is usually a fine thing to do. Context is trivially bypassed if you don't want it. It can be encapsulated within a portion of the stack without "polluting" the rest of the stack like any other function parameter. The key aspect of color is that it is not optional. It isn't something that you can just decide to ignore and stop passing up, or trivially create a value for passing down to other functions. You have to change the "color". Async is a color in many environments. There aren't really that many colors in programming languages because they are very, very quickly inconvenient and we tend to squeeze them out. (Haskell really sticks out here as a language that is not only capable of creating arbitrary colors, but where this is an explicit tool used by the community rather than a limitation, and they even have ways of combining colors together deliberately.) Statement versus expression distinctions are another one, where a "statement" may not be usable in an "expression", and you'll note how languages have in general erased that one over time because it's really just a cost without much benefit.
- preommr 5mo ago> 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.
- assbuttbuttass 5mo ago> Your entire codebase is now a surface area that is at risk of being blocked The point of goroutines is that they can freely block when needed. It's not like async where you have to be paranoid at every moment about writing blocking code
- stymaar 5mo agoYou can only freely block the goroutines that you designed that way, there's plenty of ways of shooting yourself in the foot with goroutines without even touching “blocking” code (because everything is blocking).
- sheept 5mo agoWhy would there be paranoia when writing blocking code with async? The downside of goroutines is that you have no control when the goroutine context switches, so naively accessing a global value can lead to race conditions (which the language has no warnings for despite being such a concurrent language), while the same code works fine in JavaScript because context switches don't happen in synchronous code.
- assbuttbuttass 5mo ago> Why would there be paranoia when writing blocking code with async? In languages like JavaScript, you have to be careful to avoid blocking the event loop, and use something like worker threads for CPU-intensive tasks. Otherwise you will end up with long tail latencies. In Go, the runtime automatically manages this and can suspend and resume long-running goroutines. > naively accessing a global value can lead to race conditions Fair point that the language doesn't automatically catch this, but that's what a mutex is for. In return you get actual parallelism that can use all your CPU cores
- Tzt 5mo ago[flagged]
- satvikpendem 5mo agoWe 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. Related, one of the former React maintainers wrote a primer on algebraic effects that's a good read: https://overreacted.io/algebraic-effects-for-the-rest-of-us/ https://overreacted.io/algebraic-effects-for-the-rest-of-us/
- zokier 5mo agoI don't think effects alone solve function coloring problem, in worst case they make it worse because every library can have its own colors
- wtetzner 5mo agoEffects solve it in the sense that the caller can handle the effects to work however they like, making the functions that use the effects more reusable. E.g., if you install IO handlers that are async and call a function that does IO, it's now an async function.
- bre1010 5mo agoI 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
- axus 5mo agoLike the & at the end of a shell command?
- enragedcacti 5mo ago> Like, why can't my sync function await something asynchronous? The answer, at least for Python, is that it is an intentional limitation because the alternatives introduce some quite bad trade-offs. Option 1: your awaited promise goes into the main async event loop. This is bad because it means that your single-threaded sync function now needs to be thread-safe, and so does any sync code that calls your sync function despite it not even knowing that you're doing anything async. This is essentially unworkable without throwing away the option of writing non-thread-safe code. Option 2: Your awaited promise goes into its own new event loop that only contains sibling and child promises. There's nothing technically stopping someone from doing this[1], but now you've lost a ton of the value of async because you will inevitably end up with a ton of siloed event loops that leave the process idle despite other async tasks existing that could run. Effective async code needs to share an event loop at as high of a level as possible, which means tainting as many methods with async as possible. At that point, you might as well enforce it at the language level and avoid the inevitable pain and fragmentation that comes from other devs across the ecosystem mixing sync and async code. [1] https://pypi.org/project/nest-asyncio/ https://pypi.org/project/nest-asyncio/ As explained by Guido: https://github.com/python/cpython/issues/66435#issuecomment-1526848310 https://github.com/python/cpython/issues/66435#issuecomment-...
- zokier 5mo ago
- gnarlouse 5mo ago> Spidermouth the Night Clown Thanks for my next horror shortfilm plot. Twist: he's the protagonist
- qihqi 5mo agoColored functions are good. It reflects the language design on signaling what is important, and what are the properties it want the writer to pay attention. Other examples of colored functions: * Haskell: pure function and non-pure (IO monads) looks different. * Rust: unsafe functions (or block) requires special markers.
- clintonc 5mo agoMy mind went to Java's checked exceptions -- not sure if anyone today believes that coloring is still a good idea.
- zokier 5mo agoThe problem with checked exceptions afaik was far more in the execution than in the idea itself. And also late 90s-early 00s was different time in general.
- vips7L 5mo agoJava just makes them hard to use. They're not fully apart of the type system and they're hard to escape when you actually want to panic. Everyone around here praises Rust's result, checked exceptions are the same idea: fn someFn() -> Result<T, E> T someFn() throws E fun someFn(): T | E // Kotlin's proposed error unions Checked exceptions actually compose a little better when you have a function that can throw multiple types: T someFn() throws E, F, G This is like a union type of E | F | G. I don't know about Rust, but most languages won't let you do that over generic types like Result<T, E | F | G>. The main problem for Java's checked exceptions is just how boilerplatey they are, especially when you can't handle something. In Java if you need to become "unchecked" or panic you need to: try { someFn(); } catch (SomeException ex) { throw new RuntimeException(ex); // dunno panic } Ideally that would just be: someFn()!!!!; // shut up compile panic if this happens
- steveklabnik 5mo agoRust makes you define an enum of E, F, and G, but also provides a conversion API so you can pass any of the three and it feels like it does, at least at the site of returning the error. It also provides an error interface so sometimes you don’t need the enum, if all the types return that interface.
- rendaw 5mo agoI really don't like this article. It has a catchy, profound-sounding title that people bandy about to argue against stuff they don't like. All functions, even non-async functions, are colored. In any large system codebase you'll have functions that can only be called in certain situations, with the right setup, whatever, and if you're lucky this is communicated by types but regardless those restrictions can't be avoided. It's easy to call low-restriction functions from high-restriction ones and not the other way around. Furthermore, it's not like the alternative to explicit await doesn't have issues too (that the article doesn't mention). There is inherent complexity, it's a tradeoff, you can't just syntax it away.
- kubb 5mo agoEvery time this is posted, it’s worth reminding: async functions in JavaScript are the correct design, and the people who did it deserve praise.
- lmm 5mo agoAsync functions are an ad hoc, informally-specified, bug-ridden, slow implementation of half of monad support.
- moomin 5mo agoMaking await flatten promises was kind of questionable. But that’s my main beef.
- tikotus 5mo agoC# (and F# before it) got it (mostly) right. JS did a shallow copy of it, messing up some details, making it harder to use for certain things.
- hmry 5mo agoYep. It's not an async vs not async thing. The way some people talk about it, you'd think the async keyword was at fault. It's all about whether a function is callable in some context. Passing in the context as an argument or making it a global variable or returning a monad doesn't do anything to uncolor the function. What's the difference between `async function f()` and `function f(eventloop, callback)`? Only syntax. Not to mention there's lots of colors unrelated to async, that most languages don't type at all. And if you use the wrong one, your program just doesn't work correctly at runtime. Thread-safe vs thread-unsafe. Blocking vs non-blocking. May throw/panic vs won't throw/panic. May fail/return null vs infallible.
- wesselbindt 5mo agoI feel like this argument always boils down to explicit vs implicit. It tastes the same as static vs dynamic typing. Personally, I fall well into the explicit camp. I like when I can know stuff about a function without having to read its body, and the bodies of the functions it calls, and the bodies of the functions they call, and so on. And so, I like when I can see from the function signature that it returns an integer, or when I can see from the function signature that it might do IO. This comes at a cost, namely that of reading five extra characters in a function signature, and I could kind of imagine (truly!) how that gets in the way for some people. There is a cost of writing the five characters as well (and like the author mentions, in a poorly designed codebase, this may have to go down the call stack), but code is read more often than written, so in a sense this is negligible. Like the dynamic vs static typing debate, I feel like this ultimately boils down to context and personal taste, and some amount of intelligence as well. I'm impressed by the amount of stuff the dynamic typing / non-async crowd is able to keep in their working or long term memory while coding. I don't have that kind of mental bandwidth, sadly. Having said all that, this argument is disingenuous in that it completely ignores the fact that the async keyword tells you something useful (rather than some made up nonsense like color), and most of the argument basically boils down to "if you ignore the benefits, this syntax has no benefits", and I really don't respect that as an argument.
- marcosdumay 5mo agoNo. The argument boils down into the fact that if you are not making something like Rust, coloring your functions for fine-grained performance issues is bad; if you are not making something like Haskell, coloring your functions for fine details of correctness is bad; if you are not doing something like a DBMS, coloring your objects for what code is reading it is bad; and so on.
- wesselbindt 5mo agoThose are not arguments, those are assertions.
- mgoetzke 5mo agoFor a lot of architectures its also irrelevant. I use sync code and I use async code in my architecture. But more like independent subsystems. They are sync internally and only few places have connections to outside systems or IO. Those can be async. I do understand though that it can be annoying for library authors, especially those that need to interact with the FS/Network etc.
- rcq 5mo ago[flagged]
- viktorcode 5mo agoIf I understand correctly, Go language praised in the article still has red and blue functions, only now they the colours are handled implicitly, and you as a programmer reading the code will have harder time guessing which is which on the call site.
- Jtsummers 5mo agoThere are no function colors in Go in the way being discussed. Every function can be spawned as a go routine, every function can spawn go routines.
- slopinthebag 5mo agoThe functions are still coloured, just implicitly. IYKYK to spawn a goroutine or not ts.
- Jtsummers 5mo ago> The functions are still coloured, just implicitly. IYKYK to spawn a goroutine or not ts. In Go, you can choose to either block on a function call or to execute it as a go routine. The function has no "color" in the sense of the article. If you want to print asynchronously, you can with a `go fmt.Println("Hello")`, or you can block on that print and remove the `go `. There is no color to any function. And the function containing that, it also has no color. It can be called synchronously or spawned as a go routine, Go makes no distinction between the kinds of functions that can be used each way.
- slopinthebag 5mo agoSay you have a function like validate_user(). In Go, should you block the main thread on this call, or fork and join? What if it makes a DB request and now your UI is blocked for 2 seconds or something? You need to know implicitly you need to call validate_user() in a goroutine, and then deal with forking and joining manually. If it's explicitly coloured as async, you know. In most async/await languages you can run async functions as sync, eg. Tokio's block_on method or C#'s .Result.
- wasmperson 5mo agoIMO the function coloring problem was solved with async/await. This article was posted before Javascript's async/await syntax cleaned up that ecosystem, so the author is only guessing when they say it doesn't fix the issue. It did fix the issue, and now function coloring isn't really a problem. If async/await doesn't solve the coloring problem, then neither do threads. Why would you ever need to start a thread to invoke a function when you could just invoke the function directly? Because the function is a red function.
- cogman10 5mo agoNo, I think you don't understand the problem. This article is about async/await. The function coloring problem arises when you have async functions. Regular functions can't call async functions. You have to hoist them into async functions in order to do that. Threads do solve this problem because they are just regular functions being called by other regular functions. They don't require the entire function stack to be `async` in order to work.
- wasmperson 5mo ago> This article is about async/await. It is not. It tries to address async/await part-way through, but it does so without the context of 10 years of successful async/await usage in javascript, the language it's criticizing. > Threads do solve this problem because they are just regular functions being called by other regular functions. They don't require the entire function stack to be `async` in order to work. This is fixating on syntax: it would be trivial for all functions to simply be `async` by default and for all calls to an `async` function to automatically `await`. This might "fix" the coloring problem as you describe it but I argue wouldn't meaningfully change anything.
- slopinthebag 5mo agoI've exclusively used async/await style languages for my entire life and have not once ran into this supposed problem of function colouring. Basically all IO/async work you do requires a context, does it matter if that context is a parameter or a keyword? I don't think so. The author is inventing a problem to rant about.
- notnullorvoid 5mo agoThe one time I did run into coloring being an issue was when working with gevent/greenlets (green threads) in Python nearly a decade ago. Implicit management of async operations is something I hope I never have to deal with again.
- triMichael 5mo agoThe author is definitely not inventing a problem. I ran into this issue at work this month. We were using WPF, which is C# combined with XAML. I needed to call some async code. First, I tried to call it from synchronous code. That compiled correctly and seemed okay, but then I got vague crashes, and after doing research, I found everyone was like "don't ever call an async function from a sync function" for that exact reason. So instead, I had to change whatever called that to be async, and then whatever called that to be async, and so on. The solution I ended up having to go with was literally changing the code in over a hundred places. This is legacy code that I touch as little as possible, and instead of the one line fix that it felt like it should have been, async turned it into over a hundred small changes throughout the entire project. I'm not saying there isn't a better approach. I'm pretty new to async/await code as I've been doing asynchronous code through threads my whole programming career. I don't think WPF is a good technology, so maybe some newer tech has solved it better. But what I can say is that this problem was not invented, it is a real problem and it caused a simple change to become a complicated one.
- deleted 5mo ago[deleted]
- overgard 5mo agoI think a few things are simultaneously true here: 1. async/await is a huge improvement over callbacks. 2. doing asynchronous programming through callbacks has always been a messy hack, primarily coming from languages that couldn't/wouldn't do real concurrency in their runtimes and async/await just papers over it without fixing the fundamental problems 3. threads are a lot more elegant from a language design standpoint and a lot more powerful, but.. 4. .. I would much rather fuss with what color my function needs to be than deal with the average concurrency bug that results from threads.
- gpderetta 5mo agodefinitely point 1. Also: 5. Async/await is a necessarily evil that allows for extremely high concurrency (10s of millions of concurrent tasks) where stackful coroutines and threads wouldn't practically scale.
- darepublic 5mo agoI believe I remember reading this back at the time of release. Incredible how far we've come from there
- Jach 5mo agoHave we? JS has its async/await syntax, and you can even handle exceptions with less pain (just uh try not to put multiple awaits in a try block). But there are still a gazillion async libraries in npm indicating things missing from the language or older libraries, and then the rest of the JS/TS ecosystem that from my perspective has only doubled down on some of its worst aspects. It looks more like regression when developing similar to how the ClojureScript guys have enjoyed things since at least 2014 seems further away now than it did then, or how it can be controversial when people like dhh push a "no build" approach.
- Yokohiii 5mo agoApparently this comment section...has color.
- deleted 5mo ago[deleted]
- capl 5mo agowill always upvote this post when I see it virtual threads (stackful coroutines) in Java is most of the reason why I’m defaulting to it nowadays instead of C#, Rust, etc
- lukaslalinsky 5mo agoI've spent the last year working on an async runtime for Zig and I really grew fond of stackful coroutines. Your just program your code as if everything was blocking. The main benefit is that you can use whatever library, it doesn't have to be async aware. Heck, I could even use C libraries, and they would work correctly with my coroutines. I really don't understand why GC-based languages decided to go with stackless coroutines, given that they could do what Go is doing.
- sufficientsoup 5mo agoWhat are your thoughts on the Io situation? Has your work been with the Io plans in mind from the beginning?
- lukaslalinsky 5mo agoMy work was kind of similar to the `std.Io` plans, so when I learned about it, I started aligning the project to that interface. Now it's a full fledged implementation of the interface. It's actually the first async implementation of the interface, the one in stdlib is very far from finished. There are many shortcomings in the API, especially missing timeouts everywhere. It was clearly designed by people working on the compiler and other local tools, and not much network services, but I think it's a great improvement in the Zig ecosystem and hopefully people can now write reusable libraries.
- matheusmoreira 5mo ago> I really don't understand why GC-based languages decided to go with stackless coroutines From what I've read it's due to a general idea that stacks use a lot of memory, which limits how many of them can be spawned. It's only good if it scales to a million concurrent users. A million 16 KiB stacks is over 16 GiB.
- lukaslalinsky 5mo agoBut if you have stack growth, the way Go does it, then the stack you allocate is actual memory you use. You are just trading heap allocations for stack growth. I started to see the stack as a super fast allocator that is always available.
- ramon156 5mo agoEh not a fan of this writing style. It's too push and pull.
- Timwi 5mo agoEdited because I didn't realize how old the article is. Almost everything the article says about C# is wrong: > Sync functions are just called, async ones need an await. No they don't. An async method can be called like any other method and you'll just get the Task<T> object. > You can’t unwrap it unless you make your function async and await it. False. You can just access .Result and it'll execute synchronously. > Before they added async-await and all of the Task<T> stuff, you just used regular sync API calls. You can still call the sync APIs, and I'm not getting any impression that those APIs are unmaintained or that they're avoiding adding new ones where it makes sense, even if in many cases it's literally just a matter of calling the async method and accessing .Result! When they describe how the compiler transforms async code into closures, they seem to get this right, but for some reason they treat it as a problem and I can't figure out why. The compiler transformation is exactly the right tool for the job. They even mention the great benefit of it not needing special runtime support. I'm very confused as to the direction of this argument. By the way, while I was reading the first part, before the “big reveal”, my two top guesses for what it's about were C++ const correctness, and Java checked exceptions. I'd argue that more of their arguments apply to those than to async/await.
- vorticalbox 5mo agoasync/await didn't hit javascript until 2017. in 2015 you picked between function foo(cb) { cb(42, null) } or function foo() { return new Promise((resolve, reject) =>{ resolve(42) }) } the second allowed for .then and .catch foo.then((value) => console.log(value)).catch((error) => // handler error) which isn't much better but did get us chaining foo().then((v) => bar(v)).then((v) => baz(v)).catch()
- benrutter 5mo agoI'm almost certainly missing something, but I'd love to be able to call await within a sync function. If I'm waiting on the async function, it feels like there's no need for it to "colour" my caller function like it does.