Rollie
Rollie

Reputation: 4769

Why does a member function invocation not infer a required receiver from the calling function?

Consider:

class Foo {
    fun CoroutineScope.foo() {

    }
}

class Bar {
    val f = Foo()

    fun CoroutineScope.bar() { // this could also be "suspend fun bar = coroutineScope {"
        f.foo() // unresolved reference

        with (f) { 
            foo() // OK
        }

        with (f) {
            with(this) {
                foo() // OK
            }
        }        
    }
}

It seems like the first attempt at f.foo() should infer the CoroutineScope receiver specified on bar(). It seems it doesn't; but in an attempt to understand receivers better, does anyone have an explanation as to why?

Edit

(Playground link)

After looking over some docs (specifically the "Declaring extensions as members") and Rene's response, I tried a few more things:

import kotlinx.coroutines.*

class Foo {
    fun CoroutineScope.foo() { println("Foo.foo")}
    fun Baz.fed(){ println("Foo.fed") }
}

class Baz {
    fun CoroutineScope.foo() { println("Baz.foo") }
    fun Foo.fed(){ println("Baz.fed") }
}

fun CoroutineScope.foo() { println("CoroutineScope.foo") }

fun foo() { println("Global.foo") }

fun bar(scope: CoroutineScope) {
    val f = Foo()
    val b = Baz()

    println ("Test 1")
    foo() // foo() from Global
    scope.foo() // foo() from CoroutineScope
    //f.foo() // unresolved reference

    println ("\nTest 2")
    with(scope) {
        foo() // foo() from CoroutineScope
        //f.foo() // unresolved reference
    }

    println ("\nTest 3")
    with(f) {
        scope.foo() // foo() from Foo
        foo() // foo() from Global
    }

    println ("\nTest 4")
    with(scope) {
        with (f) {
            foo() // foo() from Foo
            scope.foo() // foo() from Foo
        }
    }

    println ("\nTest 5")
    with(f) {
        with (scope) {
            foo() // foo() from Foo
            scope.foo() // foo() from Foo
        }
    }

    println ("\nTest 6")
    with(b) {
        with(f) {
            with (scope) {
                foo() // foo() from Foo

                fed() // fed() from Baz     
            }
        }
    }

    println ("\nTest 7")
    with(f) {
        with(b) {
            with (scope) {
                foo() // foo() from Baz

                fed() // fed() from Foo
            }
        }
    }
}

fun main() = runBlocking {
    bar(this)
}

It's interesting to see that when both contexts are made available via with, it is able to figure out which one is the dispatch context and which the extension context, regardless of what order they are provided. But if you specify the extension context directly like f.bar(), it will only look for versions of bar with extension receiver of type Foo, or a direct member of Foo (I'm still a bit hazy about how it views dispatch and extension receivers for a function that is simply defined in the class definition). So it seems the logic is something like:

Given expression x.y():

  1. Find all functions y() that take extension receiver x
  2. For each receiver available c, starting with the most recently added, choose the first x.y() that explicitly takes a dispatch receiver of type c - note that fun CoroutineScope.foo() in global scope acts like it has no dispatch receiver, since tests 6 and 7 show that even though scope is added to the available context list last, Foo (or Baz) version of foo() is used.

Given expression y():

  1. Try to find an x.y() with both a dispatch receiver and extension receiver (x) defined and in the list of available contexts. (note: it chooses the most recently added extension receiver first, then tries to find a matching dispatch receiver (see test 6 and 7 with fed())
  2. If #1 returned nothing, choose most recently added context with dispatch receiver x in available contexts that define function y()
  3. Still nothing? Choose the most recently added context with extension receiver x that define function y()
  4. Fall back to global version of y()

Upvotes: 2

Views: 51

Answers (1)

Rene
Rene

Reputation: 6258

Your declaration:

class Foo {
    fun CoroutineScope.foo() {

    }
}

defines an extension function foo of CoroutineScope in the context of an instance of the class Foo. This means you can only invoke foo on an instance of CoroutineScope if you are in a scope where this is of the type Foo.

The first attempt f.foo() does exactly the opposite. You invoke foo() on an instance of Foo and this references a CoroutineScope.

The two other examples uses with to set the this reference to Foo and therefor you can invoke foo on the outer CoroutineScope instance.

BTW: with(this) makes no sense, because this will be set to this.

Upvotes: 3

Related Questions