きっかけ
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
なのでそこを読んでみようかなと思います。
非同期プログラミングのスタイル
非同期プログラミングは異なるいくつかのスタイルが存在します。
コールバックは一般に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
その非同期と一時停止スタイルはこれまでに私達がみた他のスタイルに簡単に置き換えることができます。
例えば、sendEmailAsync
はfuture 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