When can you use suspended sequence functions of classes


#1

I’m using suspended iterators to generate state machine code for me. So far i only called stuff within class hierarchy and everything worked fine. I now want to move some code to other objects and just don’t understand when (and why) kotlin allows or wont allow me to call suspend funs of other objects. Example code:

// kotlin_version = '1.2.31'
import kotlin.coroutines.experimental.*

class SBTest{
    suspend fun SequenceBuilder<Int>.myCommand(data: Int): Boolean{
        yield(data)
        return true
    }
    class someWrapper(val sb: SBTest){
        suspend fun SequenceBuilder<Int>.failingFun(){
            sb.myCommand(1) // fails?
            this@failingFun.myCommand(1) // fails as expected
            sb.run {
                myCommand(1) // works
                this@failingFun.myCommand(1) // works, now?! why?
                this.myCommand(1) // fails with different error
            }
        }
    }
    suspend fun SequenceBuilder<Int>.workingFun(){
        myCommand(1) // works
        this.myCommand(1) // works
        this@workingFun.myCommand(1) // works
    }
    fun i() = buildIterator {
        workingFun() // works
        someWrapper(this@SBTest).failingFun(); // fails
        someWrapper(this@SBTest).run { 
            failingFun() // works
        }
    }
}
            
fun main(args: Array<String>) {
    SBTest().i().forEach {
      println("Hello, world!")
    }
}

I’d expect all but one calls to myCommand to work. Can someone explain why they fail? Is that intended, am I doing something wrong, or am I running into limitations of an unfinished feature? Might there be any better way to get “suspend after every step” state machine that also yields data and has individual functions returning values?
Thanks for you help.


#2

The code doesn’t build?

Kotlin version 1.2.31-release-95 (JRE 1.8.0_144-b01)
sb.kt:11:16: error: unresolved reference: myCommand
            sb.myCommand(1) // fails?
               ^
sb.kt:12:29: error: unresolved reference: myCommand
            this@failingFun.myCommand(1) // fails as expected
                            ^
sb.kt:16:22: error: unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public final suspend fun SequenceBuilder<Int>.myCommand(data: Int): Boolean defined in SBTest
                this.myCommand(1) // fails with different error
                     ^
sb.kt:27:34: error: unresolved reference: failingFun
        someWrapper(this@SBTest).failingFun(); // fails
                                 ^

#3

Yes, I meant those fail to compile and i’d like to understand why or how elegantly work around it. I’m starting to realize my problem has less to do with suspension but with receivers. Understanding a bunch of different nested “this” scopes is no big deal for me, but now passing multiple of those scopes/references/receivers/whatever it’s called around is kind of confusing. Or can you guess what function calls in func1 of the following code will result in build errors?

    class bar {
        fun foo.func2(){}
    }
    class foo {
        fun bar.func1(){
            val otherFoo = foo()
            val self = this
            val selfFoo = this@foo

            func1()
            func2()
            this.func1()
            this.func2()
            self.func1()
            self.func2()
            selfFoo.func1()
            selfFoo.func2()
            otherFoo.func1()
            otherFoo.func2()
        }
    }

Kotlin seems smart enough to usually grab the correct “this” (and multiple, if needed) if you just give it a function name, but “something.” always has to be the receiver if required by the target method. That makes switching to other non-receiver-“this” kind of tricky. That’s a problem for me.

I want to structure my code around different classes, thus i’d like to easily switch to a different non-receiver-“this”, preferable by calling myObject.myFunction()

I also want my code to suspend on specific points with a results, something buildIterator / SequenceBuilder with yield seems a perfect fit for.

But SequenceBuilder seems to necessarily be a receiver to have a suspending function that can yield. (is that correct? I didn’t manage to arrange and use yield any other way without compiler errors)

Thus to call a SequenceBuilder<>.partialSequence() on a different otherObject, i have to do otherObject.run{ partialSequence() }

I think i now at least fully understand my issue. But still see no nice way to call other objects methods with the same SequenceBuilder receiver. Anyway, maybe “Language Design” would have been a more fitting category? At least this doesn’t feel like the final evolution of programming languages, yet, unless I am missing something.

ps: Now i’m kind of curious how reflection handles method with receivers.


#4

I am not an expert on this but I can try to explain it as far as I understand it. I can’t just now find the matching documentation so I’m sorry if I don’t use the correct terms for what I describe.

Normally a method has 1 receiver

class A{
    fun foo() {}
}

Pretty straight forward. If we extend the class like this

class B { val someValue: Int = 6 }
class  A{
    val someValue: Int = 5
    fun foo() {}
    fun B.bar() {
        println(someValue)
    }
}

The method bar has 2 receivers. A primary receiver and a secondary (not sure how they are called in Kotlin). When you access someValue first the function scope gets checked. In the function local scope of bar is no someValue so the next scope checked is the public scope of B as it is the main receiver of bar, therefor someValue == 6. If B does not contain someValue the secondaries receiver is checked and after that global scopes. I’m not sure how far you can stack those secondary receivers.

In your buildIterator function you call:

someWrapper(this@SBTest).failingFun() // fails
someWrapper(this@SBTest).run { 
    failingFun() // works
}

The first call to failingFun fails because you define the primary receiver of your function to be of type someWrapper but failingFun expects a primary receiver of type SequenceBuilder. The second call works, because now you are in a scope where failingFun is visible but you call it on an object of type SequenceBuilder. Kotlin knows to pass your than secondary receiver to the primary receiver of failingFun. (hope this makes sense).

In your wrapper you have this code

suspend fun SequenceBuilder<Int>.failingFun(){
            sb.myCommand(1) // fails?
            this@failingFun.myCommand(1) // fails as expected
            sb.run {
                myCommand(1) // works
                this@failingFun.myCommand(1) // works, now?! why?
                this.myCommand(1) // fails with different error
            }
        }

sb.myCommand(1) fails for the same reason I explained above.
You said this@failingFun.myCommand(1) fails as expected, but I would think it should actually work, but I guess this answers my above question as to how far secondary receivers can be stacked. Apparently there can only be one secondary receiver. As myCommand would be visible on the third level, this call fails.
When you call sb.run { ... you switch your receivers again. Your primary receiver should at that point be of type SBTest and your secondary receiver of type SequenceBuilder. Therefore you can call myCommand(1).
this@failingFun.myCommand(1) works because this@failingFun is a sequenceBuilder. Your primary receiver is of type SBTest, therefore myCommand is visible to you and you call it with the correct receiver.
The last this.myCommand(1) is not valid, because this is of type SBTest and the function myCommand expects a SequenceBuilder as primary receiver.

I hope all of that made sense. My suggestion to make all of this easier to understand are those:

  • When possible do not stack different receivers
  • Many of the standard library functions have equivalent implementations passing the receiver as the first argument, which than can be named: let { name /* or explicit it */ -> ... } instead of run.

Extension functions are implemented as static functions taking the receiver as the first argument so

fun Bar.foo() {}

becomes the following java code

class FileNameKt {
    public static void foo(Bar primaryReceiver) {}
}

PS: I moved this to "Language Design” :slight_smile:


#5

Thanks. I’ve now also found some matching receiver documentation in Extensions and thus can now use proper names :slight_smile:

    class A(val v: Int) {  // A is called "dispatch receiver"
        suspend fun SequenceBuilder<Int>.tryWork(): Boolean{ // SeqBuilder is "extension receiver"
            ...
        }
    }
    val a1 = A(0)
    val a2 = A(1)

Well, there doesn’t seem to be a way to get to code as simple as

    suspend fun SequenceBuilder<Int>.doWork(){
        if(a1.tryWork()){
            a2.tryWork()
        }
    }

and run seems like the best actually working solution.

    suspend fun SequenceBuilder<Int>.doWork(){
        if(a1.run { tryWork() }){
            a2.run { tryWork() }
        }
    }

I still don’t love it. Not using different receivers isn’t really an option when using iterators and yield. “let” doesn’t seem very helpful here, since it just gives a name to some scope (that is likely already in this) while I want to call my method with a different dispatch receiver. Seems like that dispatch receiver has to be taken from a “this” scope (since sequenceBuilder always has to be the extension receiver), thus i have to bring my object into a some “this” scope.

I don’t see a good approach that could improve code, even with radically changing the language.

  • Getting rid of SequenceBuilder as extension receiver is probably impossible, since required for the state machine
  • Allowing more complicated extension methods with even more receivers might help, but at the cost of high complexity
  • Allowing a1.tryWork by looking up the method for whatever for all possible implicit receivers taking the missing ones from this would probably work, but also make the language a lot more ambiguous

So yeah, something.run{method()} it is.

And maybe i shouldn’t complain. C# for example doesn’t allow functions that yield values as an iterators but also has separate return values at all :slight_smile: