One way to interpret "color" is just another term for effect [1]. An effect is a specific attribute a function may have which propagates to its callers. Async and throws
are effects, but so are "does IO", non-determinism, and even potential non-termination (if a function has the potential to infinitely recurse, it can't be used to prove things via the Curry–Howard correspondence). A function which calls an async function must be async and a function which calls a throws
function must be throws
, similarly a function which calls an IO-capable function is itself IO-capable, etc. [2].
I'd argue that every language has functions with multiple colors. It's just that in some languages like C and JavaScript the colors are implicit, in some languages like Haskell the colors are explicit in the function signature (monads like IO
, Future
, State
), and some languages like C++ and Java some colors (async
, const
member function, throws
) are explicit and the rest are implicit. Even in Go which don't have async
, some functions are entirely single-threaded and others spawn goroutines, you just can't tell by the signature; similarly in JavaScript some functions will never throw exceptions and others may, and in Java some methods won't mutate this
and others will, but you can't tell by the function signatures. *Go is fundamentally different from C# in that both support asynchronous functions, but Go's functions are defined and called no differently than synchronous ones, while C#'s asynchronous functions must be prefixed async
and called with await
(or explicitly handled with something like .GetAwaiter().GetResult()
).
You can argue that the syntax of languages without explicit colors/effects is less polluted. In Go, you don't have to denote all of your async functions with extra syntax, and in JavaScript, you don't have to put throws IOException
after every function which invokes IO (or spam try { ... } catch (e) { throw new RuntimeException(e); }
). In fact, this is probably a big reason why languages where every effect must be explicit haven't caught on, because it would be a big pain to write signatures like async nondeterministic nonterminating ioperforming FooBar(arg: int, arg2: string) throws IOException, TimeoutException, BazException
(and there are plenty of reasonable, small functions which would have many types of effects).
However, languages "without" colored functions (without explicit effects) are no less "polluted" than those with them, because every language has colored functions, those languages are just hiding the pollution :). In fact, languages without explicit syntax usually have more effects, because programmers can't hide unreasonable side-effects from their API, and class signatures may disallow unreasonable effects. For example, Java's URL.equals
performs DNS resolution, and the result can be different depending on whether or not you are connected to the internet or if a server's IP address changed. If Java was forced to denote that Object.equals
could potentially access the internet, more people may be dissuaded from using Java; or more likely, the developers of Java may have figured out a more reasonable solution (like, not using DNS resolution to compare URLs).
[1] "What color is your function", which I believe is the origin of "color", defines colors a different way: pure functions are blue, functions which perform an effect are red. Here, pure functions are colorless or grayscale, and different effects correspond to different colors.
The terminology "color" is used because it helps to show effects by highlighting functions with each effect in their specific color, like the article does. If you have multiple effects, each may correspond to a different color (green, orange, blue), then a function with multiple effects would have to have multiple colors.
[2] Unless the function uses an effect handler, which effectively consumes the effect so it's no longer observable to the outside world. Ex: calling an async
function with .GetAwaiter().GetResult()
to simply block until it returns, calling a throws
function within a try
block to catch the exception, calling an IO-capable function on a virtual filesystem, calling a non-deterministic function with a seeded random, or calling a potentially non-terminating function but only running a finite number of steps and then "giving up" if it doesn't terminate.