Kotlin/kotlinx.coroutines

Add context parameter based `CoroutineScope` APIs

Opened this issue · 3 comments

Use case

Current coroutine scope APIs like CoroutineScope.launch are receiver-based.
It's pervasive, just like function coloring.

According to it's semantic, a CoroutineScope is more fit as a context element if we have context parameter.
In Kotlin, a function can have at most two receiver for now, and this new API will make the receiver more free.

The Shape of the API

Example:

context(scope: CoroutineScope)
public fun launch(context(CoroutineScope) suspend () -> Unit): Job {
    // ...
}

It's pervasive, just like function coloring.

It is indeed pervasive, but so is context(scope: CoroutineScope), which means we are not winning on that front.

According to it's semantic, a CoroutineScope is more fit as a context element if we have context parameter.

I agree.

In Kotlin, a function can have at most two receiver for now, and this new API will make the receiver more free.

Do you mean writing code like this?

class Receiver1 {
    context(scope: CoroutineScope)
    fun Receiver2.createNewJobs() {
        launch { doWork1() }
        launch { doWork2() }
    }
}

This looks quite niche to me. Usually, you see functions either creating new coroutines internally (in their own coroutineScope { }) or using a CoroutineScope stored in a property (so that the lifetime of the coroutines matches the lifetime of some object). Isolating the spawning of some work in the caller's CoroutineScope is already situational, and needing two receivers for this is something I can't imagine seeing in actual code. Do you have any specific examples of such code in mind?

Another, in my opinion more realistic concern about the current API shape was raised internally, namely that coroutineScope { }, launch { }, and the other coroutine builders shadow the expression this, which means that occasionally, you have to write code like fun X.foo() = coroutineScope { something(this@foo) }. If coroutineScope's lambda had a context parameter instead of a receiver, the call would instead be simply something(this). There is indeed some code in the wild that looks like that: https://github.com/search?q=%2Fsuspend+fun+%5Cw*%5C.%2F+language%3AKotlin+%2F%28%3F-i%29coroutineScope%2F+%2Fthis%40%2F&type=code


My personal opinion is that changing the API/adding some new API of this form is not worth it, for two reasons.

  1. There is a use case that benefits from the current API shape: accessing the outer coroutine's scope, like in https://github.com/search?q=%2Fthis%40%5Ba-zA-Z0-9_%5D%2B%5C.launch%2F&type=code . With context parameters, this would require additional syntax.
  2. This change would entail an ecosystem-wide migration. A lot of code is already written with the CoroutineScope receivers in mind. Having to re-learn what the idiomatic usage of kotlinx.coroutines looks like is painful and should bring tangible benefits. The benefits don't seem to be there: we only get small syntactic improvements in a couple of seemingly niche use cases in exchange for a syntactic degradation in another (probably comparably niche) use case.

I'm ready to be proven wrong by someone highlighting common patterns that would benefit significantly from context(scope: CoroutineScope), so let's please keep this issue open and see if someone comes up with clear use cases.

Thank you for considering this so carefully and your open mind!

@dkhalanskyjb According to discussion in KEEP and KEEP/context parameter, existing receivers can play the context role.

We can just add a new API:

// old
fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext, 
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    block: suspend CoroutineScope.() -> Unit
): Job
// new
@JvmName("launchContext") // They have the same signature on JVM!
context(scope: CoroutineScope)
fun launch(
    context: CoroutineContext = EmptyCoroutineContext, 
    start: CoroutineStart = CoroutineStart.DEFAULT, 
    block: suspend CoroutineScope.() -> Unit
): Job

This should be working.