kotlinvisitor-pattern

Kotlin sequence yield in a visitor closure


I'm trying to use yield in a sequence to lazily emit items from a visitor but I'm getting the error "Suspension functions can be called only within coroutine body".

I'm not trying to suspend here, just emit an item:

private fun flatten(x: MyType): Sequence<MyType> {
    return sequence {
        x.accept(object : MyType.Visitor {
            override fun visit(a: MyType.SubTypeA) {
                this@sequence.yield(a)
            }

            override fun visit(b: MyType.SubTypeB) {
                this@sequence.yield(b)
            }

            override fun visit(c: MyType.SubTypeC) {
                this@sequence.yield(c)
            }
        })
    }
}

I suspected the closure was confusing the compiler so I added this@sequence but it didn't help. What am I doing wrong?


Solution

  • I'm not trying to suspend here

    You are. The yield method needs to suspend to make the sequence lazy. If it didn't suspend, accept will visit everything and yield everything immediately, when you try to consume the first element of the sequence.

    When you try to get an element of the sequence, the code in the sequence lambda runs, until yield gets called. It suspends the execution of the lambda, and gives you back the element you want. The lambda doesn't continue running, until you try to get the next element.

    So if your MyType.Visitor can't suspend, you can't do this lazily. Use something like buildList instead.

    Not only do the visitor methods need to suspend, they also must be extensions on SequenceScope. This is because sequence uses restricted suspension. The idea is that when suspending in a sequence lambda, you must keep passing the SequenceScope along, to keep track of the sequence you are yielding.

    As a result, all the visit methods, as well as the accept method, need to be suspending, and need to be extensions of SequenceScope<MyType>.

    class MyType {
        suspend fun SequenceScope<MyType>.accept(visitor: Visitor) {
            with(visitor) {
                visit(a = something)
                visit(b = somethingElse)
                // etc...
                // note that if you want to write a receiver for these visit calls,
                // you should write this@accept (i.e. the sequence scope) as the receiver, not "visitor"
            }
        }
    }
    

    The visit methods would be declared like:

    suspend fun SequenceScope<MyType>.visit(a: MyType.SubTypeA)
    

    In sequence, you would need to wrap x with a with:

    with(x) {
        accept(object: MyType.Visitor { ... })
    }
    

    If you really want to go down this route, you should probably add a new SuspendVisitor, and acceptSuspend, instead of modifying the existing methods, so that this can still be used in a non-suspending way.