I'm trying to implement something similar to lazy,...
# coroutines
c
I'm trying to implement something similar to lazy, but suspending. At the start of the program the value is empty, on first access I want to compute it, then reuse the same value throughout the program's lifetime. The easiest solution would be a lock/mutex, however it seems like they are quite expensive, so seem a bit overkill (since the data is immutable, locking is unnecessary after it is initialized)
m
Depending on how you access it, you can launch an
async
with
CoroutineStart.LAZY
. Then you can have a suspending function that waits on the deferred that is returned. Unfortunately this ties the lifecycle of that
Deferred
to some scope that may not be related to the scope that is using the results. Depending on what you are doing that might be a problem. https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/index.html
Copy code
private val httpClient = scope.async(start = CoroutineStart.LAZY) {
        val cacheDirectory = options.createCacheDirectory(cacheRootProvider)
        val cacheSize = options.cacheSize ?: cacheDirectory?.calculateRecommendedCacheSize() ?: 0L

        createOkHttpClient(cacheSize, cacheDirectory)
    }

    override suspend fun get(requestSettings: HttpRequestSettings): HttpResult<HttpResponse> {
        @Suppress("BlockingMethodInNonBlockingContext") // Suspend is there for engines that support async io
        return try {
            HttpSuccess(OkHttpResponse(httpClient.await().newCall(Request.Builder().apply {
                url(requestSettings.url)
                for (header in requestSettings.headers) {
                    header(header.first, header.second)
                }
            }.build()).execute()))
        } catch (e: IOException) {
            HttpIoFailure(e)
        }
    }
r
You can create a custom implementation of
Deferred
which just delegates everything but
join()
and
await()
to an internal
CompletableDeferred
. Then since
await()
and
join()
are suspend functions, use them to perform the work once and
complete
the delegate.
Copy code
inline fun <reified T> lazyDeferred(crossinline action: suspend () -> T): Deferred<T> {
  val delegate = CompletableDeferred<T>()

  val lock = Mutex()

  return object : Deferred<T> by delegate {

    override suspend fun join() {
      lock.withLock {
        if (!delegate.isCompleted) {
          delegate.complete(action())
        }
      }
      return delegate.join()
    }

    override suspend fun await(): T {
      lock.withLock {
        if (!delegate.isCompleted) {
          delegate.complete(action())
        }
      }
      return delegate.await()
    }
  }
}
usage:
Copy code
val myExpensiveThing: Deferred<ExpensiveThing> = lazyDeferred { createExpensiveThing() }
❤️ 1
u
How about starting the coroutine on your own scope, without lazy. And just delegate your property
by lazy
? You can then await your deferred over and over again.
Copy code
val myExpensiveThing: Deferred<ExpensiveThing> by lazy { myScope.async { createExpensiveThing() } }
(Written on mobile. Typos and syntax errors expected)
👍 1
j
@uli what would be the benefit of this over just:
Copy code
val myExpensiveThing = myScope.async(start = CoroutineStart.LAZY) { createExpensiveThing() }
u
by lazy
will not only postpone the execution of the coroutine but also the construction. No access, no coroutine. On the other hand each getter will go through another indirection. Both probably not detectable. So more a matter of taste.
j
Well
lazy
avoids the coroutine construction but adds a delegate construction, so I don't believe we're saving much here. However we do add some indirection. But yeah as you said not much difference, I just find the direct
async
call simpler.