Firefly is a new general purpose programming language that tries to achieve convenience and safety at the same time, by using pervasive dependency injection. There is no global access to the file system, the network, other processes or devices. Instead, you access these through a system object that is passed to the main function, which in turn can pass this object to other methods. The idea is to give the programmer fine grained control over which parts of the code can access what (Log4Shell anybody?), without introducing monads or other explicit effect tracking.
As it turns out, this mechanism enables colorless async/await.
Because all I/O is accessed via dependency injection, a top level function call can only be asynchronous if it’s called with an asynchronous argument. This leads to the first rule of inferring async/await in Firefly:
Rule 1: Top level function calls can only be asynchronous if they’re called with asynchronous arguments. Every top level function gets a sync version and an async version. If one of the arguments is async, the async version is called; otherwise the sync version is called.
Inner functions may also capture async objects and call them. However, that can only come, directly or indirectly, from the arguments of the top level function. This leads to the second rule.
Rule 2: Inner functions are async if they call an async function. Such functions always come, directly or indirectly, from the arguments of the top level method.
When you call a function with multiple arguments, the function may update one argument with the the capabilities of the other though mutation. This also applies to the return value, which may be e.g. a lambda functions that updates one of the captured arguments when it’s later called, possibly based on the concrete arguments given when calling the lambda function.
Rule 3: All arguments and the return values are potentially async if any of the arguments are async. The return value may later cause the arguments to become async.
Using an effect type inference based on these rules, Firefly automatically infers which calls are definitely synchronous, and which calls are possibly asynchronous. It’s colorless: Programmers never have to annotate functions as async, and never have to manually
await. In fact, Firefly has no syntax for specifying such things.
Let’s take a final look at the output of the static analysis:
The calls that may be async are automatically identified. This means you can write straightforward code that looks like it’s blocking, and still get all the benefits of asynchronous I/O.
In part 2, we infer async/await on a concrete code example and see how it works under the hood: