Android
Kotlin
coroutine

KotlinのドキュメントのAsynchronous programming stylesを読む

きっかけ

FluxとCoroutineを使ってイケているAndroidアプリが作りたかったので、DroidKaigiアプリで実験していました。
https://github.com/DroidKaigi/conference-app-2018/commit/cfe7a11c4e8baf6818f6398d8ac81a334eff4826
でも割と雰囲気で書いていて、よく分からなかったんですよね。。
そんな時にk-kagurazakaさんがPRを出してくれて、その時にこのドキュメントが便利だと教えてくれました。
https://github.com/DroidKaigi/conference-app-2018/pull/669
なのでそこを読んでみようかなと思います。

原文はこちらです。
https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#asynchronous-programming-styles

非同期プログラミングのスタイル

非同期プログラミングは異なるいくつかのスタイルが存在します。
コールバックは一般にcoroutineが置き換えるように設計されている最も簡単なスタイルです。(コールバックはasynchronous computationsのセクションで言及しています。)コールバックスタイルなAPIはここで示す相当するsuspending functionでラップする事ができます。

おさらいさせてください。例えばブロックするsendEmail関数を以下のようなシグネチャで作ったとします。

fun sendEmail(emailArgs: EmailArgs): EmailResult

この関数は操作中にスレッドの実行を長時間ブロックする可能性があります。

それをブロックしないようにするには、例えば、ブロックしないcallback-styleを代表してerror-firstなnode.js callback conventionを以下のシグネチャで使うことができます。

fun sendEmail(emailArgs: EmailArgs, callback: (Throwable?, EmailResult?) -> Unit)

しかし、coroutineで他の非同期のスタイルを使うことができます。それらの一つのスタイルはasync/awaitスタイルで、それはたくさんの有名な言語に組み込まれています。Kotlinでは、このスタイルはfuturesの使い方のセクションのパートとして示されているfuture{}.await()ライブラリ関数の導入によって利用されています。
このスタイルはコールバックをパラメーターとして返すのではなく、future objectのような何かを返す事によって示されています。sendEmailのこのasync-style関数シグネチャはこのようになります。

fun sendEmailAsync(emailArgs: EmailArgs): Future<EmailResult>

このスタイルでAsyncを追加するのは、パラメーターが同期的に行う場合と全く同じで、その処理が非同期であることを忘れるミスをしやすいためです。sendEmailAsync関数は非同期処理を開始し、並行処理の落とし穴をもたらす可能性があります。しかしこのスタイルを促進しているプログラミング言語では通常、実行を必要に応じてシーケンスに戻すためのawait(待つ)基本メソッドのようなものがあります。

Kotlinのnativeプログラミングスタイルでは一時停止(suspending)関数ベースです。このスタイルではsendEmailのシグネチャは自然に見え、パラメータや返り値などでわかりにくいものがありません。しかしsuspend修飾子を追加しています。

suspend fun sendEmail(emailArgs: EmailArgs): EmailResult

その非同期と一時停止スタイルはこれまでに私達がみた他のスタイルに簡単に置き換えることができます。
例えば、sendEmailAsyncfuture coroutine builderを使って一時停止するsendEmailで実装する事ができます。

fun sendEmailAsync(emailArgs: EmailArgs): Future<EmailResult> = future {
    sendEmail(emailArgs)
}

一時停止関数sendEmailはawait suspending functionを利用してsendEmailAsyncで実装できます

suspend fun sendEmail(emailArgs: EmailArgs): EmailResult = 
    sendEmailAsync(emailArgs).await()

そのため、ある意味この2つのスタイルは等価で、便利さでコールバックより優れています。
しかしsendEmailAsyncと一時停止sendEmailの違いを深く見てみましょう。

それらを入れ込む(compose)方法で始めに比べてみましょう。一時停止関数は普通の関数のように入れ込むことができます。

suspend fun largerBusinessProcess() {
    // ここにたくさんのコード..
    sendEmail(emailArgs)
    // その後にも続く..
}

対応するasync-style関数は以下のように入れ込みます。

fun largerBusinessProcessAsync() = future {
    // ここにたくさんのコード..
   sendEmailAsync(emailArgs).await()
    // その後にも続く..
}

async-style関数の入れ込む時は冗長でエラーが起こりやすいです。async-styleの例で.await()の呼び出しをなくしてもコードはコンパイルできて動作します。しかし、emailの送信が非同期になってしまうか、ビジネスプロセスの別の部分と同時に送信して、共有の状態を変更して、エラーを再現するのが非常に困難になる可能性があります。対照的に一時停止関数は逐次処理がデフォルトです。一時停止関数とともに、いつでもあなたが平行性が必要な時にfuture{}や似たcoroutineビルダーへの呼び出しのあるソースコード内で表現することができます。

どのようにこれらのスタイルがたくさんのライブラリを使った大きいプロジェクトでスケールするのか比べてみましょう。一時停止関数はKotlinで軽量なコンセプトです。全ての一時停止関数はどんなKotlin coroutineの中でも使うことができます。async-style関数はフレームワークに依存します。それぞれのpromises/futuresフレームワークはそれ自身のasyncのようなものを定義する必要があります。あらゆるpromises/futureのフレームワークは、フレームワークのpromises/futureのようなクラスとフレームワークのawaitのような関数を返す独自の非同期的な関数を定義しなければなりません。

パフォーマンスを比べてみましょう。一時停止関数は呼び出しごとに最小限のオーバーヘッドで提供されます。implementation detailsセクションをチェックすることができます。async-style関数では停止する仕組みに追加してとても重いpromise/futureの抽象化を維持する必要があります。いくつかのfutureのようなオブジェクトのインスタンスはasync-style関数から常に返さなければならず。それはもし関数が短く単純であるならば最適化することができません。async-styleは細かい分解には適していません。

JVM/JSのコードとの互換性について比べてみましょう。async-style関数はfutureのような抽象化を利用するJVM/JSコードとより互換性があります。JavaやJSではそれらはfutureのようなオブジェクトを返すただの関数です。一時停止関数はcontinuation-passing-styleに対応していないどの言語からでも変に見えます。しかしあなたは与えられたpromise/futureフレームワークのために簡単に一時停止関数をasync-style関数に変換する後にある例でみることができます。だからKotlinで一時停止関数を1度書くだけで、適切なfuture{}coroutine builder関数を使った一行のコードを利用して、どのpromise/futureのスタイルとも相互運用することができます。

感想

async-styleを使わずに書ける方が間違えにくいので良い、ということでそういう風に出来るように、プルリクエストを送ってくれたみたいで、勉強になりました。
https://github.com/DroidKaigi/conference-app-2018/pull/669/files