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

次の記事の別案。

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

元記事では `LazySuspendFunction` に `CoroutineScope` を持たせた。
しかしそうすると `CoroutineScope` の生存期間を考慮しなければならなくなりちょっと面倒だ。
そこで `CoroutineScope` を持たせないバージョンを作成する。

まず準備として、
未初期化の状態を表せるように次の型を用意する。

```Kotlin
/**
 * 未初期化／初期化済みのいずれかの具象型のみを持つシールドクラス。
 */
sealed interface Initializable<out T> {
    /** 未初期化であることを表す、[Initializable] インターフェイスのオブジェクト。 */
    object Uninitialized : Initializable<Nothing>

    /** 初期化済みの値を持つ、[Initializable] インターフェイスを実装した具象クラス。 */
    data class Initialized<T>(
        /** 初期化された値。 */
        val value: T
    ) : Initializable<T>
}
```

`LazySuspendFunction` クラスは次のようになる。

```Kotlin

/**
 * 初回 [invoke] 時のみ評価され、以降の [invoke] 時には同じ結果を返す、
 * suspend 関数オブジェクトクラス。
 *
 * @param R 返値の型
 */
class LazySuspendFunction<R>(
    /**
     * 初期化関数。
     *
     * 初回 [invoke] 時に評価され、その結果がそのときおよび以降の [invoke] の返値となる。
     */
    private val initializer: suspend () -> R
) {
    @Volatile
    private var initializable: Initializable<R> = Initializable.Uninitialized

    private val mutex: Mutex = Mutex()

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

    private suspend fun ensureInitialized(): Initializable.Initialized<R> {
        // ダブルチェックロッキング
        initializable.also {
            if (it is Initializable.Initialized<R>) {
                return it
            }
        }

        mutex.withLock {
            // ダブルチェックロッキング
            initializable.also {
                if (it is Initializable.Initialized<R>) {
                    return it
                }
            }

            return initializer()
                .let { Initializable.Initialized(it) }
                .also {
                    initializable = it
                }
        }
    }
}
```

元記事の方では、`LazySuspendFunction.invoke()` で初期化中に呼び出し元がキャンセルされても初期化は続く。しかし初期化中に `CoroutineScope` がキャンセルされると「キャンセルされた」という状態で初期化され、常に `CancellatioException` をスローするようになる。
この記事の方では、初期化中に呼び出し元がキャンセルされると、初期化が行われていない状態になり、次の呼び出しの際に再度初期化が行われる。

動作確認。

```Kotlin

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

        // ...
        delay(1_000)

        myCount += 1
        return myCount
    } catch (e: CancellationException) {
        println("Cancelled!")
        throw e
    }
}

private var myCount = 0


fun main() {
    // 遅延評価される suspend 関数。
    // この宣言はコルーチン外で行うことができる。
    val myLazySuspendFunction =
        LazySuspendFunction {
            mySuspendFunction()
        } // 何も出力しない。

    runBlocking {
        // ^ suspend 関数を呼び出すことからコルーチンビルダーで囲う必要があるが、
        // ^ launch 関数だと呼び出し元が即座に処理を返してメインスレッドが終了してしまうため、
        // ^ runBlocking 関数を使用する。

        val job = launch {
            myLazySuspendFunction() // > old count: 0
                .also { println("new count: $it") } // > new count: 1
        }
        delay(500)
        job.cancel() // > Canceled!

        myLazySuspendFunction() // > old count: 0
            .also { println("new count: $it") } // > new count: 1
    }
}
```

／以上