What is the receiver supposed to be in a `withContext` block (and an extension function)?

What is the receiver supposed to be in a withContext block in an extension function?

Consider the following example:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.net.URI

suspend fun URI.foo() = withContext(Dispatchers.IO) {
    println(scheme)
    println(toString())
}

fun main() {
    val u = URI("https://example.com/some/path")
    runBlocking {
        u.foo()
    }
}

That prints:

https
"coroutine#1":DispatchedCoroutine{Active}@d03e0e8

In other words, in URI.foo(), println(scheme) is equvalent to println(this@foo.scheme), but println(toString()) is equvalent to println(this.toString())

I would expect that to:

1: Print

https
https://example.com/some/path

because the receiver is the URI the extension function is being called on. I.e., as if the code was:

suspend fun URI.foo() = withContext(Dispatchers.IO) {
    println(this@foo.scheme)
    println(this@foo.toString())
}

Or

2: Be a compilation error, because this inside the withContext block refers to the coroutine, and the coroutine doesn’t have a scheme property. I.e., as if the code was:

suspend fun URI.foo() = withContext(Dispatchers.IO) {
    println(this.scheme)
    println(this.toString())
}

(which fails to compile with the error ā€œUnresolved reference ā€˜schemeā€™ā€)

2 Likes

From the docs:

suspend fun <T> withContext(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T

so the receiver is a CoroutineScope.
To access an outer receiver, you can refer to it by the function name this@foo. You can also use a function like so:

fun <T> T.receiver(): T = this

to be called like receiver<URI>() so that you can do it by type

2 Likes

Yes. But that doesn’t explain why using the bare scheme works. Why doesn’t that cause an error in the original code?

This behaviour exists in Java (I believe) with inner classes.
Consider the following code:

class Foo {
  fun foo() {}
  fun bar() {}
  inner class Bar {
    fun bar() {}
    fun baz() {
      foo() // uses this@Foo
      bar() // uses this
    }
  }

The point is that lexical scoping tells you what declarations are accessible.
An extension function then acts semantically as-if it was magically written inside the class (but it’s always final, has no access to private or protected declarations beyond what a non-extension function has in the same scope, etc).
It thus makes sense that multiple receivers would have their declarations accessible.

Digging in the docs, I found this:

You can call methods of every available implicit receiver inside a lambda

There’s probably a more helpful docs page about this, I just couldn’t find it quickly.

EDIT: Here’s another clarification about implicit receivers:

You can declare extensions for one class inside another. Extensions like this have multiple implicit receivers. An implicit receiver is an object whose members you can access without qualifying them with this:

  • The class where you declare the extension is the dispatch receiver.
  • The extension function’s receiver type is the extension receiver.

There’s also this, specifically about extension lambdas (but you might need to squint a little to see this as saying that the implicit receiver is available even to nested extension lambdas):

Function types with receiver, such as A.(B) -> C, can be instantiated with a special form of function literals – function literals with receiver.

As mentioned above, Kotlin provides the ability to call an instance of a function type with receiver while providing the receiver object.

Inside the body of the function literal, the receiver object passed to a call becomes an implicit this, so that you can access the members of that receiver object without any additional qualifiers, or access the receiver object using a this expression.

This behavior is similar to that of extension functions, which also allow you to access the members of the receiver object inside the function body.

Thanks, that’s very helpful. I’ve filed https://youtrack.jetbrains.com/issue/KT-85700/Unqualified-references-in-lambdas-with-multiple-receivers-should-warn-by-default to suggest this should be a compiler warning.

This is an intended feature which powers many (most?) DSLs. If a compiler warning is introduced, many things will break or have spurious warnings.

Context parameters will be stable very soon and do not have this ambiguity. However, replacing extension functions by context parameters is a breaking change, so it won’t happen for existing methods like `withContext`.

2 Likes