What is the standard way to refactor a lazy proper...
# coroutines
m
What is the standard way to refactor a lazy property to a suspend function that uses a cached value on subsequent runs?
CompletableDeferred
or something else?
Copy code
// from this
val someProperty: Int by lazy {
    // refactoring to perform some suspendable logic
}

// to this
suspend fun someProperty(): Int {
    // only should be executed max once (and then cached)
}
Maybe to use this:
Copy code
fun <T> suspendableLazy(scope: CoroutineScope, provider: suspend () -> T) = object : SuspendableProvider<T> {
    private val computed = scope.async(start = CoroutineStart.LAZY) { provider() }

    override val isCompleted: Boolean
        get() = computed.isCompleted

    override suspend fun get() = computed.await()
}
But then I end up passing some application-scope (e.g.
GlobalScope
). Wouldn’t it be better to use a scope of the coroutine calling the function for the first time?
e
and what do you do if it's cancelled before the value is computed?
m
The next call would end up doing the work
e
wrap a var with a mutex, then. similar to how Lazy (in the default LazyThreadSafetyMode.SYNCHRONIZED) works but with coroutines
Copy code
fun <T : Any>  suspendableLazy(provider: suspend () -> T) = object : SuspendableProvider<T> {
    private val mutex = Mutex()
    private var result: T? = null
    override val isCompleted: Boolean get() = result != null
    override suspend fun get() = mutex.withLock {
        result ?: provider().also { result = it }
    }
}
or something like that, tweak as needed
m
Thanks! I wonder if that logic can be wrapped in a delegated property of type Deferred<T> so that the suspending fun just has to do
deferredProperty.await()
This doesn’t look right (not sure I’m getting the benefits of a lazy delegate here!)
Copy code
private class CachingSuspendableProvider<T>(private val provider: suspend () -> T) : SuspendableProvider<T> {
    private var result: T? = null

    private val mutex = Mutex()

    override suspend fun get(): T = result
        ?: mutex.withLock {
            result ?: provider().also {
                result = it
            }
        }

    override val isCompleted: Boolean
        get() = result != null
}

private class SuspendableLazy<T>(provider: suspend () -> T): Lazy<suspend () -> T> {
    private val suspendableLazy = CachingSuspendableProvider(provider)

    override val value: suspend () -> T
        get() = suspendableLazy::get

    override fun isInitialized() = suspendableLazy.isCompleted
}

fun <T> suspendableLazy(provider: suspend () -> T): Lazy<suspend () -> T> = SuspendableLazy(provider)

// usage
private val fooDelegate by suspendableLazy { /* compute Foo */ }
suspend fun getFoo(): Foo = fooDelegate()
e
no, you're not. equivalent to
val lazyValue = suspendableLazy { compute() }::get
with the previous implementation, and less ceremony
o
@ephemient Looks like the above implementation of
suspendableLazy
contains an unsynchronized read on `result`:
Copy code
override val isCompleted: Boolean get() = result != null
Wouldn't that require annotating
result
with
@Volatile
?
e
isComplete is racy but it doesn't matter
what are you going to do,
if (isComplete) get()
? it's going to wait on the mutex for the value no matter what
there's just inherent races with callers using isComplete, no need to try to narrow it.
o
The problem is not the inherent race but that a caller reading from one thread might not immediately see the update from another thread due to weak processor cache coherency. Depends on what you use
isCompleted
for. Anyway, I don't really see it working:
val lazyValue = suspendableLazy { compute() }::get
returns a function, not a value.
e
it has to be a function, there's no suspendable property delegates yet. https://youtrack.jetbrains.com/issue/KT-20414