https://kotlinlang.org logo
Title
m

Mark

05/03/2021, 7:00 AM
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):
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

ephemient

05/03/2021, 12:57 PM
from the above case I don't see why you want property delegation at all.
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

Mark

05/03/2021, 1:03 PM
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

ephemient

05/03/2021, 1:03 PM
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

Mark

05/03/2021, 1:06 PM
The computedLambda is very efficient. No withLock call.
e

ephemient

05/03/2021, 1:10 PM
that's true, but it's also using the double-checked pattern which is actually not valid in Java without
volatile
m

Mark

05/03/2021, 1:12 PM
How to do that in pure Kotlin?
lateinit var
?
e

ephemient

05/03/2021, 1:14 PM
no, that's just syntactic sugar around
T?
. there's
@kotlin.jvm.Volatile
but I don't think it works on locals
m

Mark

05/03/2021, 1:19 PM
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
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

ephemient

05/03/2021, 1:20 PM
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

Mark

05/03/2021, 1:21 PM
The point is just to work on a local variable rather than an instance variable
e

ephemient

05/03/2021, 1:23 PM
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

Mark

05/03/2021, 1:24 PM
Good points. I’ll just add the
@Volatile
annotation for now
How about using
CompletableDeferred
instead of the computedLambda property?
e

ephemient

05/03/2021, 1:57 PM
I think you might lose compute-exactly-once in case of races, if that's important, but it would work
u

uli

05/04/2021, 1:45 PM
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

ephemient

05/04/2021, 3:58 PM
completableResult.complete(compute())
run concurrently may run multiple
compute()
concurrently; of course only one ends up as the result.
u

uli

05/04/2021, 6:51 PM
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() }