kotlinx.coroutines
kotlinx.coroutines copied to clipboard
Consider deprecation cycle for `CoroutineDispatcher.invoke`
CoroutineDispatcher.invoke, a function used as such:
Dispatchers.IO {
stuff
}
is a shorter version of withContext that only works with dispatchers.
Does this function need to exist? It seems to predate structured concurrency and is so terse that it offers no indication as to its underlying behavior. Does it launch a new coroutine? Does it move the current coroutine? Does the supplied block honor structured concurrency?
I personally have a hard time finding a reason for this function to continue to exist.
For context, it was originally added in #428 as an experiment. As far as I know, it is very rarely used (if at all) in the ecosystem.
Very few usages found: https://grep.app/search?q=.IO%20%7B&case=true&filter[lang][0]=Kotlin https://grep.app/search?q=Dispatchers.Default%20%7B&case=true&filter[lang][0]=Kotlin https://grep.app/search?q=Dispatchers.Main%20%7B&case=true&words=true&filter[lang][0]=Kotlin
I personally find this to be non-idiomatic modern Kotlin. Thing { } looks to me like a builder of some sort. So, I'm for this.
We can try and proceed with it in RC2 along with other deprecations
I'm not sure what I expected, but the deprecation doesn't look pretty in the IDE!
Given how rare this is, I don't think this should stop us, though.
Finding some good IntelliJ bugs/feature requests at least!
You can say I'm biased, but I like it because it's concise and clear enough once you know what a coroutine dispatcher is.
withContext is a lower-level construct, that can also be misused since it accepts the CoroutineContext type, which can notoriously(?) contain a Job, breaking structured concurrency.
For the most common case of switching dispatchers to perform blocking I/O, or ensure work is done off the main thread, or on the main thread, wrapping some code in a dispatcher matches quite intuitively to Dispatchers.Whatever { someCode() }.
I'd say that all that is missing is adding the contract for the block lambda (now that Kotlin supports it for operator functions, new in 2.2.20), and actually advertise it, now that it can have the same set of features as withContext for the case of dispatchers.
If you don't like it, you can just not use it, instead of breaking the codebases that use it. And keep in mind that not all code is open source, and by far.
Last but not least, the operator fun invoke in question does NOT predate structured concurrency, please don't bring false assumptions to the discussion, and what it does is quite simply explained by 2 things: the KDoc, which can be improved if deemed helpful, and the very straightforward implementation one can jump in by ctrl/cmd/force clicking on the opening brace.
The reason to count usages in open-source codebases is to compare the usage count to the equivalent pattern using withContext:
- https://grep.app/search?f.lang=Kotlin&q=withContext%28Dispatchers.Default%29 1,754
- https://grep.app/search?f.lang=Kotlin&q=withContext%28Dispatchers.IO%29 8,679
- https://grep.app/search?f.lang=Kotlin&q=withContext%28Dispatchers.Main%29 3,472
12 usages vs 13905 usages show that in open-source projects, less than 0.1% of dispatcher switching happens using CoroutineDispatcher.invoke, and I don't see why the distribution in closed-source projects would be different. There's value in having only one idiomatic way to do things, and we know what the idiomatic way here is. withContext is the clear winner.
Maybe invoke would have won if it supported contracts from day one or was advertised better, but I doubt that. I know I wouldn't use Dispatchers.Something { } in my own code.
- Its missing name strongly suggests that the only thing this function does is transfer work to another dispatcher, but in fact, it creates a nested coroutine scope, with all the structured concurrency implications.
- The
Dispatchers.Something { }form looks visually similar to constructing an object, likeList(10) { it * 3 },Runnable { println("OK") }, orJson { pretty = true }. It sends the wrong message when skimming code. We are not constructingDispatchers.Something, we are using it.- I've polled our team, and
withContextwon overinvokewith the score 7:2 as the more Kotlin-idiomatic form.
- I've polled our team, and
- It's visually distinct from every other coroutine builder:
withContext,runBlocking,launch,async,coroutineScope,supervisorScope... Something likerunOn(Dispatchers.IO) { }would fit in better.
Also, to the point about the risk of passing a Job to withContext: launch is by far the biggest offender in that regard, withContext is not even close, so we need to deal with the Job problem some other way than hiding the CoroutineContext arguments.
Given all that, the benefits of invoke are at least not clear enough for us to push for a kotlinx.coroutines ecosystem fragmentation (however minor it may be) by reviving this experiment.
Some data:
At the time of writing this, there are 364 public usages of CoroutineDispatcher.invoke on the public side of GitHub.
https://github.com/search?q=%22import+kotlinx.coroutines.invoke%22&type=code
Grep.app is only finding 23 results, for some reason: https://grep.app/search?f.lang=Kotlin&q=import+kotlinx.coroutines.invoke
It's interestingly more popular than this anti-pattern of replacing the Job (146 public usages, vs 364):
https://github.com/search?q=%22withContext%28Job%22+language%3AKotlin&type=code&p=2&l=Kotlin