Home > Net >  How to use functional types or lambdas as type for Kotlin's context receivers?
How to use functional types or lambdas as type for Kotlin's context receivers?

Time:10-13

package yamin

typealias Foo = () -> Unit

fun main() {
    bar {
        baz()
    }
}

fun bar(foo: Foo) {
    foo()
}

context(Foo)
fun baz() {
    //
}

I tried to use a lambda type for the context of a function, which seems to be fine at this point but when I tried to call that function in that context, or at least what I think is that context I'm failing, and Kotlin's compiler is showing me this error for baz():

No required context receiver found: Cxt { context((yamin.Foo /* = () -> kotlin.Unit */)) public fun baz(): kotlin.Unit defined in yamin in file Main.kt[SimpleFunctionDescriptorImpl@7b5e305f] }

Maybe I misunderstood Kotlin's context receivers or I'm expecting something that it's not designed for. I just want to create a function that can only be called in certain context and in this example only in a certain lambda.

CodePudding user response:

As it is right now, your baz can be called like this:

val someFoo: Foo = {}
with(someFoo) {
    baz()
}

By using with, I bring an instance of () -> Unit into the context, so someFoo becomes a context receiver with which I can call baz. In baz, I can then access this someFoo by using this@Foo. This is how context receivers are supposed to work.

If you want baz to only be able to be called in bar's lambda, bar's lambda needs to provide the context receiver using a receiver parameter, similar to how with's lambda is declared T.() -> R.

object Foo

fun main() {
    bar {
        baz()
    }
}

fun bar(foo: Foo.() -> Unit) {
    Foo.foo()
}

context(Foo)
fun baz() {
    // ...
}

I changed the actual context here to just an object, because as far as I can see, there is nothing special about bar's lambda. Of course, if you want, you can make it provide extra information to baz by changing Foo to a class that does contain data.

Note that this does not prevent someone from doing:

with(Foo) {
    baz()
}

It is quite hard to prevent against this without adding another layer outside of bar, like:

// note that bar is no longer a global function
import bar.Foo.Companion.bar

class Foo private constructor() {
    companion object {
        fun bar(foo: Foo.() -> Unit) {
            Foo().foo()
        }
    }
}

context(Foo)
fun baz() {
    // ...
}

You can also do something simple like this prevents it from happening from outside the package:

sealed interface FooInterface

private object Foo: FooInterface

fun bar(foo: FooInterface.() -> Unit) {
    Foo.foo()
}

context(FooInterface)
fun baz() {
    // ...
}

CodePudding user response:

To make a function that can only be called in a certain scope, give the function itself a receiver. Instead of typealias Foo = () -> Unit, you would use something like typealias Foo = MyReceiver.() -> Unit.

You can do this with a regular extension receiver, but it's also possible with context receivers.

Here's a simple version that works for me:

typealias Foo = BarScope.() -> Unit

fun main() {
    bar {
        baz()
    }
}

object BarScope

fun bar(foo: Foo) {
    BarScope.foo()
}

context(BarScope)
fun baz() {

}
  • Related