How to create an extension function with multiple receivers in Kotlin?
Asked Answered
H

4

20

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?

Holusbolus answered 17/1, 2022 at 9:57 Comment(0)
H
30

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"
        ]
    }
}
Holusbolus answered 12/4, 2022 at 18:20 Comment(2)
For Kotlin/Native, the code for the build.gradle.kts file is: tasks.withType<KotlinNativeCompile>().configureEach { kotlinOptions { freeCompilerArgs += "-Xcontext-receivers" } }. That's how you add free compiler args when using Kotlin/Native. I didn't find this information anywhere.Master
To use the context receiver this value you can add a label to the context receiver, i. e. context(scope@CoroutineScope), then you can access it using this@scope (even if the IDE doesn't show it in autocomplete). Otherwise, just use the type as label name, for example this@CoroutineScope.Master
D
4

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()
}
Discophile answered 17/1, 2022 at 17:44 Comment(0)
B
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.

Brazee answered 17/1, 2022 at 10:3 Comment(1)
They keep mentioning it roadmap talks, so I think they're actively working on it.Discophile
P
1

Here is workaround you can use:

val <T> Iterable<T>.handle: CoroutineScope.() -> Unit get() = {
  map {
    launch {  }
  }
}
Phthisic answered 28/3, 2022 at 8:18 Comment(2)
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!Holusbolus
It would be possible, if you swap receivers (Iterable<T> 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()Phthisic

© 2022 - 2024 — McMap. All rights reserved.