Once upon a time in Kotlin

Alexey Soshin
ProAndroidDev
Published in
4 min readMar 5, 2020

--

Photo by Cayetano Gil on Unsplash

Lately I’ve stumbled upon an interesting task — executing a function exactly once, while invoked from multiple coroutines.

That is, having:

fun compute() {
println("Computing")
return 1
}

Invoking:

for (i in 1..10) {
launch {
println(compute())
}
}

Should print “Computing” once only.

What are our options?

First, let’s make an observation that whether the function has been executed once is a state. So, we need an object to keep that state. We’ll name it Once and use it as follows:

val once = Once {
compute()
}
for (i in 1..10) {
launch {
println(once())
}
}

What are the properties of this object?

First, it should receive a block as its constructor argument:

class Once<out T>(block: () -> T)

And second, it should have an invoke method for nicer invocation syntax:

class Once<out T>(block: () -> T) {
operator fun invoke() = ???
}

The implementation then will look trivial to anyone familiar with Kotlin.

Let’s use a lazy delegate:

class Once<out T>(block: () -> T) {
private val result: T by lazy {
block()
}

operator fun invoke() = result
}

And indeed, if you run the code from above, it works as expected.

Let’s complicate our requirements a bit then.

What happens, if we would like to invoke only once a suspending function?

suspend fun compute(): Int {
println("Invoked")
delay(Random.nextLong(10, 100))
return 1
}

Initial changes seem quite trivial. First, the block our class receives should be a suspending one:

class Once<out T>(block: suspend () -> T) {
...
}

But lazy delegate cannot handle suspending functions:

private val result: T by lazy {
block() // should be called only from a coroutine
// or another suspend function
}

We could throw runBlocking into the mix, and it will even work, but we obviously wouldn’t like to block our coroutines.

private val result: T by lazy {
runBlocking {
block()
}
}

So, we need another solution.

Let’s simplify our problem a bit first.

What if our compute() didn’t return any value?

Then, we could use AtomicBoolean :

class Once(private val block: suspend () -> Unit) {
private val ran = AtomicBoolean(false)
suspend operator fun invoke() {
if (ran.compareAndSet(false, true)) {
block()
}
}
}

That works, but doesn’t print the result.

What if we memorize the result then, and return it?

class Once<out T>(private val block: suspend () -> T) {
private var r: T // Property must be initialized or be abstract
private val ran = AtomicBoolean(false)
suspend operator fun invoke(): T {
if (ran.compareAndSet(false, true)) {
r = block()
}
return r
}
}

Trying to initialize the property using lazy will bring us to the previous problem. So, maybe using nullable value will help?

class Once<out T>(private val block: suspend () -> T) {
private var r: T? = null
private val ran = AtomicBoolean(false)
suspend operator fun invoke(): T {
if (ran.compareAndSet(false, true)) {
r = block()
}
return r!!
}
}

And we get a classical race with a NullPointerException as a result.

So, this is obviously not the right way.

Kotlin has it’s own implementation of Mutex, which is compatible with coroutines.

class Once<out T>(private val block: suspend () -> T) {
private val mutex = Mutex()
private var r : T? = null

suspend operator fun invoke(): T {
mutex.lock()
if (r == null) {
r = block()
}
mutex.unlock()
return r!!
}
}

This looks much better!

Now, we can also replace nullable variable with a lateinit variable:

class Once<out T : Any>(private val block: suspend () -> T) {
private val mutex = Mutex()
private lateinit var r : T

suspend operator fun invoke(): T {
mutex.lock()
if (!this::r.isInitialized) {
r = block()
}
mutex.unlock()
return r
}
}

Note that our type definition had to change to <out T : Any>

And that we now check if the variable was already set using this::r.isInitialized

We could also rewrite mutex invocation in a less imperative way (this is not Go, after all):

suspend operator fun invoke(): T {
return mutex.withLock {
if (!this::r.isInitialized) {
r = block()
}
r
}
}

This solution doesn’t involve blocking code, doesn’t crash with NullPointerException (what’s the point of using Kotlin otherwise?), but it’s still quite verbose.

Is there a better way of implementing lazy concurrency?

There is, provided by no other than Roman Elizarov himself. Behold:

class Once<T>(block: suspend () -> T) {
private val r = GlobalScope.async(start = CoroutineStart.LAZY) {
block()
}

suspend fun run() = r.await()
}

Using GlobalScope is definitely not the best practice, so we could define our coroutine scope ourselves:

class Once<T>(block: suspend () -> T) {
private val scope = CoroutineScope(Dispatchers.Default)
private val r = scope.async(start = CoroutineStart.LAZY) {
block()
}

suspend operator fun invoke() = r.await()
}

With that solution, we get all the benefits we expect from Kotlin.

It’s concurrent, type safe and easy to follow.

Following that idea, we can even get rid of the Once class completely:

fun CoroutineScope.computeAsync() = async(start = CoroutineStart.LAZY){
println("Invoked")
delay(Random.nextLong(10, 100))
1
}

This will produce same results:

val once = computeAsync()
for (i in 1..10) {
launch {
println(once.await())
}
}

Conclusions

Being lazy with Kotlin is easy. Being lazy and concurrent is slightly harder.

But only so slightly.

Footnotes

https://gist.github.com/elizarov/f27400a55c1502aacc35b4a3b2f5c9af

https://medium.com/@sampsonjoliver/lazy-evaluated-coroutines-in-kotlin-bf5be004233

If you liked this article, be sure to check video course I recently published: Web Development with Kotlin

--

--

Solutions Architect @Depop, author of “Kotlin Design Patterns and Best Practices” book and “Pragmatic System Design” course