Function coloring options

I’m working on a toy implementation of structured concurrency (as a library) for the Lua programming language.

There’s the design decision about async/await and function coloring, though here I’m limited because I’ll not add new language syntax. Lua has only coroutines, and no built-in scheduler or notion of concurrency. (In contrast with Python, nice to start from a clean slate.)

So I reread https://lukasa.co.uk/2016/07/The_Function_Colour_Myth/, and it’s clear that using coroutines without marking async definitions or calls is an option, though there is utility to marking. I’m familiar with Python’s choices as well as Kotlin’s.

Summary of function coloring options:

  1. symmetric async / await - i.e. markers for both definitions and calls, as Python does. Typically part of the language syntax, though decorators could be used for definitions. Unfortunately for Python, even though async / await is part of the language, omitting await is not caught at compile time.
  2. asymmetric async / await - marker on only definitions or usages, e.g. Kotlin only marks definitions. The argument by Kotlin’s structured concurrency author is that the IDE can flag awaitable expressions in the code. However, I think the reasoning is flawed because we aren’t always viewing code through an IDE (code reviews, code snippets in the wild, etc.). Especially for single-OS-thread scenarios where there is no implicit context switch, it’s essential to know the points of explicit context switch (and corresponding atomicity between those points) to reason about the code.
  3. naming convention only - for Lua, I’m leaning toward prefixing all async definitions with async_. Obviously the marking is equally visible at usage sites. This naming applies to the “trio” library as well-- so async_sleep(), async_open_nursery(), etc. Perhaps static or runtime checks could be built upon the naming convention.
  4. nothing - e.g. golang? Like (2), but worse.

Let me chime in on the second (asymmetric) option here. The argument that IDE can flag calls is not the reason Kotlin went with this design. The chief reason was about separation of concerns. When I read/review the code, the most important thing I want to understand first is the logic of the code. I want to see the substance of the code without any obscure ceremony. That’s the centerpiece of Kotlin’s philosophy of language design and that’s why this choice was natural for Kotlin. The asynchronous or synchronous nature of the call is a secondary concern, and it should not stand in the way of understanding what code actually does. That was not an easy design decision to make, though. We had all those concerns about how people would understand snippets of the code without IDE, etc. However, tons of code is being written without any coloring at all (golang is a great example) and people don’t usually complain that it is hard to understand without seeing explicit “await” calls or “async_xxx” preffixes in the code. That was the key insight for us. We are really thankful to Go language for paving this road for us.

Hi Roman, thank you for clarifying.

At least in Python, where I spend my time coding these days, explicit context switch is the rule and it’s very helpful to have visible markers. Our async code is very often taking advantage of atomicity between context switches, and it’s part of reasoning about the code. Lua would be a similar situation, though OS threading is an option since it doesn’t have a GIL.

For Go, if I understand correctly that implicit context switches are the rule, it makes sense to eschew the markers. Kotlin’s structured concurrency library offers a single-thread option, though I imagine it’s used relatively less, so the lack of markers may not be missed. But if someone decided to have a codebase exclusively in single-thread mode and take advantage of that to avoid shared mutable state paranoia, I suspect they’d want the markers.

The great thing about the Kotlin design in the language is that it is flexible. If somebody works in a domain where their code is exclusively single-threaded and they rely on atomicity and explicit context switch points, then they can define their own await function and consistently use it everywhere in their code together with async { ... } builder and/or choose to follow some kind of naming convention. They can effectively emulate any other design. It becomes a free, domain-specific choice, not something that is just forced by the underlying language or runtime.

1 Like