Kotlin Coroutines: How SelectBuilder
magically pulls coroutines out of the select
block
I was puzzled for a while about the syntactic sugar around select
— Kotlin's experimental feature to process streams from multiple sources (docs). Take a look at this:
fun main() = runBlocking<Unit> {
select<Unit> {
launch {
delay(100)
}.onJoin { Unit }
produce {
delay(200)
send("foo!")
}.onReceive { Unit }
}
}
Note that the on...
methods aren't suspending, but the async
block is run somewhere somehow, so it seems like something that smells a lot like suspend (Deferred, Job, Channel, ...) -> Unit
must be happening behind the curtains. The signature of select
looked like it answered this mystery:
Code samples below and beyond come from kotlinx.coroutines v1.0.1
// from selects/Select.kt
public suspend inline fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R
Ah, so SelectBuilder<R>.()
binds this
within select
's block to a SelectBuilder
, so that's how it grabs up those coroutines! ...right?
But that's obviously not quite enough. Each of those on...
calls looks totally siloed; after all they evaluate to Unit
s, and there's no mention of this
anywhere! launch(...)
and produce
are still CoroutineContext.launch(...)
and CoroutineContext.produce(...)
, so at first glance, it actually looks like SelectBuilder<R>.()
does squat.
Things need to get even stranger before they get clearer. Looking at the definition of any of the on...
methods reveals they're actually... values?
// from src/Job.kt
interface Job: SelectClause0 {
// ...
/**
* Clause for [select] expression of [join] suspending function that selects when the job is complete.
* This clause never fails, even if the job completes exceptionally.
*/
public val onJoin: SelectClause0
// ...
}
And these values are... this
??
// from src/JobSupport.kt
class JobSupport : Job {
// ...
public final override val onJoin: SelectClause0
get() = this
// ...
}
What. This is to say some_coroutine { } .on...(<clause>)
is really some_coroutine { }(<clause>)
? We need to look at that select
implementation pronto. It's actually very terse; to protect against changes in the source I'll just point out that it constructs a SelectBuilderImpl
as the context of the select clause, so let's peek at that:
// from src/Select.kt
class SelectBuilderImpl {
// ...
override fun SelectClause0.invoke(block: suspend () -> R) {
registerSelectClause0(this@SelectBuilderImpl, block)
}
// ...
}
Oh. Well that was a fairly abrupt conclusion. We do call the objects themselves so that the context SelectBuilder
can swap out the invoke
for one that registers the object to the context. This is because all the coroutines are one of (SelectClause0, SelectClause1<Q>, SelectClause2<P, Q>)
depending on their outputs — Job : SelectClause0
for example.
That's a pretty clever one JetBrains. Above preserving the no-frills lambda syntax we love for those on...
invocations, it also avoids careless/rushed/human developers forgetting to register our coroutines with select
. I'm not really aware of other languages that allow you to do this; are there other examples of scoped class extension? It's nuts in my opinion. Absolutely nuts.
- kotlinx.coroutines