【Kotlin】suspend 関数を lazy に呼び出す《改良版》

次の記事の改良版です。併せてお読みください。

https://qiita.com/sdkei/items/20832b83793cd1e28081

元記事では `Deferred` 型の lazy プロパティを使用していました。
しかしそれだと次の2点が美しくありませんでした。
- 拡張プロパティなのに型が本来の値の型ではなく `Deferred` になっている。
- 本来の値を取得するのに `await()` の呼び出しが必要である。

そこで次のラッパークラスを作ることでこれを改善します。

```Kotlin
/**
 * 初回 [invoke] 時のみ評価され、以降の [invoke] 時には同じ結果を返す、
 * suspend 関数オブジェクトクラス。
 *
 * @param R 返値の型。
 */
class LazySuspendFunction<R>(
    /** [initializer] を実行する際に使用されるコルーチンスコープ。 */
    private val scope: CoroutineScope,
    /**
     * 初期化関数。
     *
     * 初回 [invoke] 時のみ実行され、その結果がそのときおよびそれ以降の [invoke] の返値となる。
     */
    private val initializer: suspend () -> R,
) {
    private val deferred: Deferred<R> by lazy {
        scope.async {
            initializer()
        }
    }

    /**
     * [initializer] を実行した結果を返す。
     *
     * [initializer] の実行は初回呼び出し時にのみ行い、
     * それ以降の呼び出しでは同じ値を返す。
     */
    suspend operator fun invoke(): R =
        deferred.await()
}
```

これを使うと次のように、普通の suspend 関数のように扱うことができます。

```Kotlin
// 「呼び出しコストが大きい」suspend 関数。
// 今回の呼び出しを除いた呼び出し回数を出力し、
// 今回の呼び出しを含めた呼び出し回数を返す。
suspend fun mySuspendFunction(): Int {
    println("old count: $myCount")

    delay(1_000)

    myCount += 1
    return myCount
}
private var myCount = 0

val scope = CoroutineScope(Dispatchers.Default)

val myLazySuspendFunction =
    LazySuspendFunction(scope) {
        mySuspendFunction()
    } // 何も出力しない。

fun main() {
    runBlocking {
        myLazySuspendFunction() // > old count: 0
            .also { println("new count: $it") } // > new count: 1
        myLazySuspendFunction() // 何も出力しない。
            .also { println("new count: $it") } // > new count: 1
    }
}
```

```txt:出力
old count: 0
new count: 1
new count: 1
```

処理中にキャンセルした場合は次のようになります。

```Kotlin
fun main() {
    runBlocking {
        launch {
            try {
                myLazySuspendFunction() // > old count: 0
                    .also { println("new count: $it") } // 出力されない。
            } catch (e: CancellationException) {
                println("Cancelled!")
                // ^ scope.cancel() が実行されたときに出力される。
                // ^ > Cancelled!
            }
        }

        // ↑の launch の処理が完了する前にキャンセルする。
        delay(500)
        scope.cancel()

        launch {
            try {
                myLazySuspendFunction() // 何も出力しない。
                    .also { println("new count: $it") } // 出力されない。
            } catch (e: CancellationException) {
                println("Cancelled!") // > Cancelled!
            }
        }
    }
}
```

```txt:出力
old count: 0
Cancelled!
Cancelled!
```

処理後にキャンセルした場合は次のようになります。

```Kotlin
fun main() {
    runBlocking {
        launch {
            try {
                myLazySuspendFunction() // > old count: 0
                    .also { println("new count: $it") } // > new count: 1
            } catch (e: CancellationException) {
                println("Cancelled!") // 出力されない。
            }
        }

        // ↑の launch の処理が完了した後にキャンセルする。
        delay(1_500)
        scope.cancel()

        launch {
            try {
                myLazySuspendFunction() // 何も出力しない。
                    .also { println("new count: $it") } // > new count: 1
            } catch (e: CancellationException) {
                println("Cancelled!") // 出力されない。
            }
        }
    }
}
```

```txt:出力
old count: 0
new count: 1
new count: 1
```

なお、`CoroutineScope` の使用についての注意点は変わっていませんのでご注意ください。

／以上