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?
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.