Here, the purpose of implementing `Lazy` is to enc...
# codereview
m
Here, the purpose of implementing
Lazy
is to encourage the result of
suspendableLazy()
to be used as a property (since it doesn’t make sense to use it otherwise):
Copy code
interface SuspendableLazy<T>: Lazy<suspend () -> T>

fun <T> suspendableLazy(scope: CoroutineScope, provider: suspend () -> T) = object : SuspendableLazy<T> {
    private val computed = scope.async(start = CoroutineStart.LAZY) { provider() }

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

    override fun isInitialized() = computed.isCompleted
}

fun <T> suspendableLazy(provider: suspend () -> T) = object : SuspendableLazy<T> {
    @Volatile // since we are using the double-checked pattern
    private var computedLambda: (suspend () -> T)? = null

    private val mutex = Mutex()

    override val value: suspend () -> T
        get() {
            computedLambda?.also {
                return it
            }
            return result@{
                // because now we are inside the lambda being returned
                computedLambda?.also {
                    return@result it()
                }
                mutex.withLock {
                    // because now we have the lock
                    computedLambda?.also {
                        return@withLock it()
                    }
                    provider().also { computedValue ->
                        computedLambda = { computedValue }
                    }
                }
            }
        }

    override fun isInitialized() = computedLambda != null
}

// usage
val fooDelegate by suspendableLazy { /* compute Foo */ }
suspend fun fooValue(): Foo = fooDelegate()
e
from the above case I don't see why you want property delegation at all.
Copy code
fun <T> lazySuspendOnce(
    scope: CoroutineScope,
    block: suspend () -> T
): suspend () -> T = scope.async(start = CoroutineStart.LAZY) { block() }::await

fun <T: Any> lazySuspendOnce(
    block: suspend () -> T
): suspend () -> T {
    val mutex = Mutex()
    var value: T? = null
    return {
        mutex.withLock {
            value ?: block().also { value = it }
        }
    }
}

val lazyFoo = lazySuspendOnce { /* compute Foo */ }
suspend fun fooValue() = lazyFoo()
m
This idea is to get a direct mapping between the classic lazy property and this one. Lazy -> SuspendableLazy by lazy -> by suspendableLazy There is also some logic to it. The final lambda (computedLambda) is lazily created (although not in your case where you execute the same lambda each time)
e
no reason to create the lambda lazily though, that doesn't trigger anything else to happen
and until we have suspend properties and suspend delegates, you can't get a direct mapping regardless
m
The computedLambda is very efficient. No withLock call.
e
that's true, but it's also using the double-checked pattern which is actually not valid in Java without
volatile
m
How to do that in pure Kotlin?
lateinit var
?
e
no, that's just syntactic sugar around
T?
. there's
@kotlin.jvm.Volatile
but I don't think it works on locals
m
Would it help to insert a
var lambda: (suspend () -> T)? = computedLambda
in the getter and then reference that instead? That would also be updated when computedLambda is updated
Copy code
override val value: suspend () -> T
    get() {
        var lambda: (suspend () -> T)? = computedLambda
        lambda?.also {
            return it
        }
        return result@{
            // because now we are inside the lambda being returned
            lambda?.also {
                return@result it()
            }
            mutex.withLock {
                // because now we have the lock
                lambda?.also {
                    return@withLock it()
                }
                provider().also { computedValue ->
                    lambda = { computedValue }
                    computedLambda = lambda
                }
            }
        }
    }
e
no, increasing the number of memory references doesn't help. it is still legal for the JVM to write to the lambda variable before it has finalized constructing the lambda object.
although... there's a good chance that there's enough synchronization points in coroutines that this doesn't matter, I never looked into it
m
The point is just to work on a local variable rather than an instance variable
e
it's not a "local" variable as far as the JVM is concerned - Kotlin automatically boxes it for two reasons - it is updated from within a non-inlined lambda, which means it can't be on the stack
and anything in coroutines that exists across a suspend point has to be put into the context anyway
so maybe there's enough compiler rewriting and other sync points in coroutines that it's ok? I don't know, and would avoid that pattern as a general rule.
m
Good points. I’ll just add the
@Volatile
annotation for now
How about using
CompletableDeferred
instead of the computedLambda property?
e
I think you might lose compute-exactly-once in case of races, if that's important, but it would work
u
Awaiting a CompletableDeferred will always do the calculation exactly once, because there is only on instance. all the awaits will just wait for the result
e
completableResult.complete(compute())
run concurrently may run multiple
compute()
concurrently; of course only one ends up as the result.
u
Ahh, correct. I was assuming you would just use async to produce the deferred. Any reasons not to do that? var suspendableLazy = async(LAZY) { compute() }