Kotlin の Coroutines を理解するのに何も難しいことはありません。そう、JavaScript の async/await がわかればね。
まず、 1 から 10 までの値を合算する処理があったとします。
fun main(args: Array<String>) {
val numList = (1..10)
val sum = numList.reduce { acc, v -> acc + v }
println(sum) // 55
}
JS でいう Promise は Coroutines の世界では Deferred という名称です。これを生成する関数を用意します。
import kotlinx.coroutines.*
fun createDeferred(value: Int): Deferred<Int> {
return GlobalScope.async {
delay(1000)
value
}
}
上記は JS でいうと下記のようなコードに相当します。
function createPromise(value) {
return new Promise(resolve => setTimeout(() => resolve(value), 1000));
}
「GlobalScope.async で非同期処理を行う Deferred を生成できる」と理解すれば一旦は OK です。
Deferred を捌く main() は下記のように変わります。
suspend fun main(args: Array<String>) { // *1
val deferredList = (1..10).map { v -> createDeferred(v) } // *2
val numList = deferredList.awaitAll() // *3
val sum = numList.reduce { acc, v -> acc + v }
println(sum) // 55 (約1秒遅れ)
}
- *1 ...
suspend funはasync function相当だという理解で OK - *2 ... 変数
deferredListの型はList<Deferred<Int>> - *3 ...
await Promise.all()みたいなことをやっているという理解で OK
上記のコードは途中で await をしているので、関数宣言に suspend を付けないとエラーになります。
ここまでで正常系は理解できました。
ではエラーハンドリングはどうしたらいいでしょうか。
まず createDeferred() を下記のように変更します。
import kotlin.random.Random
fun createDeferred(value: Int): Deferred<Int> {
return GlobalScope.async {
delay(1000)
when (Random.nextFloat() < 0.9) { // *1, *2
true -> value
else -> throw Exception("Error for $value")
}
}
}
- *1 ...
Random.nextFloat()は0以上1未満の数値を返す - *2 ...
whenでパターンマッチングを表現できる
これにより createDeferred() で返される Deferred は10%の確率でエラーになります。
となると main() ではエラーハンドリングをしないといけません。
suspend fun main(args: Array<String>) {
val deferredList = (1..10).map { v -> createDeferred(v) }
try {
val numList = deferredList.awaitAll()
val sum = numList.reduce { acc, v -> acc + v }
println(sum) // 55
} catch (e: Exception) {
println(e) // "Error for n"
}
}
何も難しいことはなく、await するところを try catch で囲むだけです。
このコードでは10%の確率でエラーになるものを10個束ねているので、ほとんどのケースで catch の中を通ってエラーが出力されますが、たまに成功して 55 が出力されます。
最終的に全容はこのようになりました。
import kotlinx.coroutines.*
import kotlin.random.Random
suspend fun main(args: Array<String>) {
val deferredList = (1..10).map { v -> createDeferred(v) }
try {
val numList = deferredList.awaitAll()
val sum = numList.reduce { acc, v -> acc + v }
println(sum)
} catch (e: Exception) {
println(e)
}
}
fun createDeferred(value: Int): Deferred<Int> {
return GlobalScope.async {
delay(1000)
when (Random.nextFloat() < 0.9) {
true -> value
else -> throw Exception("Error for $value")
}
}
}
( https://pl.kotl.in/H1LuMeSWV ← 上記コードを実際に動かせる Kotlin Playground )
ちなみに Deferred は下記のようにして個別に await することもできます。
suspend fun main(args: Array<String>) {
val deferredList = (1..10).map { v -> createDeferred(v) }
val numList = deferredList.map { d ->
try {
d.await()
} catch (e: Exception) {
println(e)
0
}
}
val sum = numList.reduce { acc, v -> acc + v }
println(sum)
}
以上です。
とても簡単に Coroutines を理解することができました。
特に TypeScript に慣れていれば型の書き方はほとんど同じだし、非同期処理に関しては用語が違うくらいなのでとっつきやすいと思いました。