13

I want my extension function to have a couple of receivers. For example, I want function handle to be able to call methods of both CoroutineScope and Iterable instances:

fun handle() {
    // I want to call CoroutineScope.launch() and Iterable.map() functions here
    map {
        launch { /* ... */ }
    }
}

I thought this might work:

fun <T> (Iterable<T>, CoroutineScope).handle() {}

But it gives me an error:

Function declaration must have a name

I know that I can create the function with parameters, but

Is it possible to have multiple receivers for a single function and how to do that without parameters?

Sergio
  • 27,326
  • 8
  • 128
  • 149

4 Answers4

16

In the Kotlin version 1.6.20 there is a new feature called Context receivers. This is a first prototype of context receivers. This feature allows to make functions, properties and classes context-dependent by adding context receivers to their declaration. There is a new syntax for that. In front of the function declaration we can specify a list of contextual types that would be required to invoke this function. A contextual declaration does the following:

  • It requires all declared context receivers to be present in a caller's scope as implicit receivers.
  • It brings declared context receivers into the body scope of implicit receivers.

The solution with context receivers looks like the following:

context(CoroutineScope)
fun <T> Iterable<T>.handle() {
    map {
        launch { /* ... */ }
    }
}

someCoroutineScope.launch {
    val students = listOf(...)
    students.handle()
}

In the context(CoroutineScope) we can declare multiple types, e.g context(CoroutineScope, LogInterface).

Since context receivers feature is a prototype, to enable it add -Xcontext-receivers compiler option in the app's build.gradle file:

apply plugin: 'kotlin-android'
android {
    //...
    kotlinOptions {
        jvmTarget = "11"
        freeCompilerArgs += [
                "-Xcontext-receivers"
        ]
    }
}
Sergio
  • 27,326
  • 8
  • 128
  • 149
2

This is a very narrow case, but if your use case is that you have a higher order function where you want code in the lambda to have multiple receivers, and if the types you're wanting to combine are interfaces, you can create a class that wraps the interfaces as delegates. Within the lambda passed to the below function, you can call both Iterable and CoroutineScope functions.

class CoroutineScopeAndIterable<T>(
    private val coroutineScope: CoroutineScope,
    private val iterable: Iterable<T>
): CoroutineScope by coroutineScope, Iterable<T> by iterable

suspend fun <T> CoroutineScope.runSomething(
    iterable: Iterable<T>, 
    block: suspend CoroutineScopeAndIterable<T>.() -> Unit
) {
    CoroutineScopeAndIterable(this, iterable).block()
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
1

As far as I know, this is currently impossible for types that we don't control. There are plans to add such feature, it is processed under KEEP-259.

I don't know what is the planned roadmap or when we could expect it to be added, but I hope we will see at least some previews this year.

broot
  • 21,588
  • 3
  • 30
  • 35
1

Here is workaround you can use:

val <T> Iterable<T>.handle: CoroutineScope.() -> Unit get() = {
  map {
    launch {  }
  }
}
Semyon
  • 26
  • 3
  • It would be great if there was a possibility to call this property without passing `CoroutineScope` instance as a parameter, like this: `list.handle()`. With your solution I have to call it like the following: `list.handle(this)` where `this` is a `CoroutineScope` instance. But this definitely works, thanks! – Sergio Apr 02 '22 at 17:10
  • It would be possible, if you swap receivers (`Iterable` with `CoroutineScope`). However, you can't do this in this particular case, as generic parameter must be used in receiver type. But if there is no generic parameter in both receivers, you can achieve this. E.g., with `val CoroutineScope.handle: Iterable<*>.() -> Unit` you can actually write just `list.handle()` – Semyon Apr 04 '22 at 19:56