【Kotlin】suspend 関数を lazy に呼び出す

呼び出しコストが大きい関数（以下、重たい関数）は、
呼び出さずに済むなら呼び出さないようにしたいし、
呼び出さなくてはならないなら呼び出し回数を最小限にしたいものです。


## 非 suspend 関数の場合

重たい関数が suspend 関数ではない普通の関数であれば、[`Lazy`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-lazy/) [委譲プロパティ（delegated property）](https://kotlinlang.org/docs/delegated-properties.html)を使うことで簡単にそのようにできます。
`Lazy` 委譲プロパティは [`lazy` 関数](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html)を使うことで生成できます。

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

    // ...

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

fun main() {
    // Lazy 委譲プロパティ
    val myLazyProperty by lazy {
        myFunction()
    } // 何も出力しない。

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

重たい関数が呼ばれるのは `Lazy` 委譲プロパティを使ったプロパティ（以下、lazy プロパティ）（上の例では `myLazyProperty` プロパティ）が初めて参照されたときだけで、
以降の参照では同じ値が返されます。


## suspend 関数の場合

では、重たい関数が suspend 関数だった場合はどうでしょう。

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

    // ...

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


### 直接呼び出すことはできない

`lazy` 関数のブロック内では suspend 関数を直接呼び出すことはできません。

```Kotlin:ダメな例
val myLazyProperty: Int by lazy {
    mySuspendFunction() // コンパイルエラー！
}
```


### `runBlocking` はスレッドをブロックする

[`runBlocking` 関数](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html)を使うのはどうでしょう。

```Kotlin:ダメな例
val myLazyProperty: Int by lazy {
    runBlocking {
        mySuspendFunction()
    }
}

fun main() {
    println("new count: $myLazyProperty") // スレッドがブロックされてしまう！
}
```

コンパイルはできるようになります。
問題なく動く場合もあるでしょう。

しかし lazy プロパティを最初に呼び出したスレッドが、重たい関数が処理を返すまでブロックされてしまいます。
それが UI スレッドであれば、その間アプリがフリーズすることになります。
また、重たい関数の処理に使用されるスレッドと lazy プロパティを呼び出したスレッドが同じであれば、デッドロックになってしまいます。


### `async` 関数と `Deferred` 型を使うとよい

次の例のように、プロパティの返値型を [`Deferred` 型](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/) にして、`lazy` 関数のブロック内で [`async` 関数](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html)を使って重たい関数の呼び出しを囲いましょう。
プロパティの呼び出し側では、`Deferred` オブジェクトから本来の値を取り出すため、[`await` 関数](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html)を呼び出します。
`await` 関数は suspend 関数なので、コルーチンの中で呼び出す必要があります。

```Kotlin
// Lazy 委譲プロパティを使ったプロパティ
val myLazyProperty: Deferred<Int> by lazy {
    CoroutineScope(Dispatchers.Default) // 注意！　状況に応じて適切な CoroutineScope を使うこと！
        .async {
            mySuspendFunction()
        }
}

fun main() {
    runBlocking {
        // ^ suspend 関数である await 関数を呼び出すため、コルーチンビルダーで囲う必要があるが、
        // ^ launch 関数だと呼び出し元が即座に処理を返してメインスレッドが終了してしまうため、
        // ^ runBlocking 関数を使用する。
    
        println("new count: ${myLazyProperty.await()}")
        // ^ > old count: 0
        // ^ > new count: 1
        println("new count: ${myLazyProperty.await()}")
        // ^ > new count: 1
    }
}
```

`async` 関数は呼び出されると即座に `Deferred` オブジェクトを返します。

`Deferred` オブジェクトは結果の引換券のようなものです。
`await` 関数を呼び出すと、`async` 関数のブロックでの処理が終わって結果を得られるまで、呼び出し元コルーチンが<ruby>中断<rt>suspend</rt></ruby>されます。
1つの `Deferred` オブジェクトに対して `await()` を複数回呼び出しても、`async` ブロックは一度しか実行されず、同じ結果が返ります。
`async` ブロックの処理が終わってから `await()` を呼び出すと、即座に結果が返ります。


### `CoroutineScope` に注意

上の例では [`CoroutineScope(Dispatchers.Default)`](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html) を使用しましたが、状況に応じて適切な [`CoroutineScope` オブジェクト](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/)を使用するようにしてください。

lazy プロパティをメンバに持つオブジェクトが破棄されても、`CoroutineScope` オブジェクトがキャンセルされていないと、`async` ブロックが無駄に処理を続けてしまうことがあるかもしれません。

逆に `async` ブロックが処理を完了する前に `CoroutineScope` がキャンセルされてしまうと、`await()` を呼び出した際に `CancellationException` がスローされることになります。

### 改良版（2023-03-22 追記）

次の記事で使いやすく改良しました。

https://qiita.com/sdkei/items/459581888fe7d28648a6

／以上