Islands in the byte stream

Technical notes by a software engineer

Kotlin Coroutines の様子を眺める

先日、JetBrainsのブログでKotlinにコルーチンが導入されるという発表がありました。

以下のリポジトリで先行事例の調査や仕様の検討が行われています。

https://github.com/Kotlin/kotlin-coroutines

先行事例の調査は以下のissueです。

https://github.com/Kotlin/kotlin-coroutines/issues/2

Kotlinにおける仕様の草稿(と思われるもの)は以下の文書です。

https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md

examples/ にあるのは、coroutinesがどのように実装されるのかのスケッチにみえます。つまり、coroutinesを使ったコードがコンパイルされる結果の例で、標準ライブラリに含まれるであろうヘルパーコードの実装も含みます。

https://github.com/Kotlin/kotlin-coroutines/tree/master/examples

検討は始まったばかりであり、Kotlinへのコルーチンの実装はまだまだ先になりそうですが、このリポジトリをみるとコルーチンの振る舞いをある程度予想できるようになっているため、少し解説してみましょう。


さて、コルーチンは、あるメソッドを、ローカル変数などの状態を維持したまま中断・再開させる機能です。C# の yieldasync/await はコルーチンの一種です。最近はJavaScriptにも yieldasync/await が導入され、babelなどのトランスパイラで利用できます。

さて、ここでは yield (ジェネレータ)を見てみましょう。

Kotlinの場合、 yield は次のように使います。gen() の戻り値は [1, 2] という反復子を表す Iterator<Integer> となります。

fun gen() = generate {
    println("yield(1)")
    yield(1)
    println("yield(2)")
    yield(2)
    println("done")
}

次の f()gen() を使うメソッドです。

fun f() {
    val iter = gen() 
    iter.next(); // "yield(1)" を出力したあと 1 を返す
    iter.next(); // "yield(2)" を出力したあと 2 を返す
}

まず、gen() を呼び出すと即座にジェネレータを生成して呼び出し元の f() に返します。次の iter.next() により gen() の実行が再開して println("yield(1)") が呼ばれたあと、 yield(1) によってまた gen() の実行が中断され、 f() に戻り、 iter.next() の戻り値として 1 を得ます。

そして次の iter.next() によって再び gen() の実行が再開され、 yeild(2) によって一時的に中断して呼び出しもとにもどり、 iter.next()2 を返します。

iterIterable<T> なので、拡張for文で反復することもできると思われます。 Iterator<T> と異なり、関数内のコンテキストを維持したまま値を生成することができるため、可読性の高い Iterator<T> を定義できるといえます。


さてこの yeild ですが、JVMやDalvikで実行する以上はバイトコードレベルの特別な機能は期待できません。したがって、Kotlinそのものでも表現できるようなコード変換が行われるだけだと思われます。

この変換結果は、たとえば次のファイルにあります。コメント内が yield を使ったコード、その他の部分がヘルパー実装とコンパイル後の実装と思われます。日本語のコメントは筆者によります。

https://github.com/Kotlin/kotlin-coroutines/blob/dd07d35/examples/yield.kt

// ...

fun main(args: Array<String>) {
/*
    fun gen() = generate {
        println("yield(1)")
        yield(1)
        println("yield(2)")
        yield(2)
        println("done")
    }
*/
    // 上記コメントのコードが変換されるのが class __anonymous__ となる
    fun gen() = generate({__anonymous__()})

    // gen() を使うコード。戻り値は普通の `Iterator<T>` として使える
    println(gen().joinToString())

    val sequence = gen()
    println(sequence.zip(sequence).joinToString())
}

// LIBRARY CODE
// Note: this code is optimized for readability, the actual implementation would create fewer objects
// yieldの実装を担うライブラリコード(可読性のため多少いじってあり、本物のコードではない、と書いてある)

// ジェネレータ生成用メソッド
fun <T> generate(@coroutine c: () -> Coroutine<GeneratorController<T>>): Sequence<T> = object : Sequence<T> {
    override fun iterator(): Iterator<T> {
        val iterator = GeneratorController<T>()
        iterator.setNextStep(c().entryPoint(iterator))
        return iterator
    }
}

// ジェネレータヘルパの実体で、 `Iterator<T>` を実装している
class GeneratorController<T>() : AbstractIterator<T>() {
    private lateinit var nextStep: Continuation<Unit>

    override fun computeNext() {
        nextStep.resume(Unit)
    }

    fun setNextStep(step: Continuation<Unit>) {
        this.nextStep = step
    }


    @suspend fun yieldValue(value: T, c: Continuation<Unit>) {
        setNext(value)
        setNextStep(c)
    }

    @operator fun handleResult(result: Unit, c: Continuation<Nothing>) {
        done()
    }
}

// GENERATED CODE
// メソッド内でyeildを使うと次のようなクラスに変換される

class __anonymous__() : Coroutine<GeneratorController<Int>>, Continuation<Any?> {
    private lateinit var controller: GeneratorController<Int>

    override fun entryPoint(controller: GeneratorController<Int>): Continuation<Unit> {
        this.controller = controller
        return this as Continuation<Unit>
    }

    override fun resume(data: Any?) = doResume(data, null)
    override fun resumeWithException(exception: Throwable) = doResume(null, exception)

    /*
        generate {
            println("yield(1)")
            yield(1)
            println("yield(2)")
            yield(2)
            println("done")
        }
     */
    // gen() が中断・再開しているように見えるのは、
    // ジェネレータの実体がステートをもつオブジェクトであり
    // label (現在のメソッドの位置) により移動できるように振る舞うから
    private var label = 0
    private fun doResume(data: Any?, exception: Throwable?) {
        when (label) {
            0 -> {
                if (exception != null) throw exception
                data as Unit
                println("yield(1)")
                label = 1 // 次の「位置」を更新
                controller.yieldValue(1, this) // yield(1) に相当
                // このあとdoResume() は中断して、再開後はlabel=1を処理する
            }
            1 -> {
                if (exception != null) throw exception
                data as Unit
                println("yield(2)")
                label = 2
                controller.yieldValue(2, this) // yield(2) に相当
                // このあとdoResume() は中断し、再開後は label=2 を処理する
            }
            2 -> {
                if (exception != null) throw exception
                data as Unit
                println("done")
                label = -1 // ジェネレータの終了
                controller.handleResult(Unit, this)
            }
            else -> throw UnsupportedOperationException("Coroutine $this is in an invalid state")
        }
    }
}

このように、 yieldIterator<T> を実装する方法の一つですが、素のままだと非常に煩雑なコードをシンプルに書けることが期待できます。Kotlinに実装されるのが楽しみですね。

コルーチンについては C# に一日の長があります。以下の書籍は2012年のものですが、このようなコルーチンをつかったコードの変換についても触れており、参考になります。