kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

Consider deprecation cycle for `CoroutineDispatcher.invoke`

Open JakeWharton opened this issue 1 year ago • 6 comments
trafficstars

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.

JakeWharton avatar Jul 29 '24 14:07 JakeWharton

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.

CLOVIS-AI avatar Jul 29 '24 14:07 CLOVIS-AI

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.

dkhalanskyjb avatar Aug 01 '24 09:08 dkhalanskyjb

We can try and proceed with it in RC2 along with other deprecations

qwwdfsad avatar Aug 01 '24 09:08 qwwdfsad

I'm not sure what I expected, but the deprecation doesn't look pretty in the IDE! Screenshot_20240801_134245 Given how rare this is, I don't think this should stop us, though.

dkhalanskyjb avatar Aug 01 '24 11:08 dkhalanskyjb

Finding some good IntelliJ bugs/feature requests at least!

JakeWharton avatar Aug 01 '24 13:08 JakeWharton

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.

LouisCAD avatar Oct 20 '25 11:10 LouisCAD

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, like List(10) { it * 3 }, Runnable { println("OK") }, or Json { pretty = true }. It sends the wrong message when skimming code. We are not constructing Dispatchers.Something, we are using it.
    • I've polled our team, and withContext won over invoke with the score 7:2 as the more Kotlin-idiomatic form.
  • It's visually distinct from every other coroutine builder: withContext, runBlocking, launch, async, coroutineScope, supervisorScope... Something like runOn(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.

dkhalanskyjb avatar Oct 29 '25 13:10 dkhalanskyjb

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

LouisCAD avatar Nov 12 '25 08:11 LouisCAD

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

LouisCAD avatar Nov 12 '25 08:11 LouisCAD