To loop or not to loop?

Artem Bagritsevich
MobilePeople
Published in
5 min readJul 25, 2023
Photo by Ben Bracken on Unsplash

Introduction

About a month ago I had an online code review session. I wanted to tell the audience about the modern stack and approaches in Android development. As an example, I took a project of one of my students and did a short code review with a focus on project structure and architecture. But during the session, this piece of code appeared on the screen:

class CurrencyRepoImpl @Inject constructor(
private val apiService: ApiService
): CurrencyRepo {

private var isObtain = true

override fun getCurrenciesStream(): Flow<List<Currency>> {
return flow {
while (true) {
if (isObtain) {
val currencies = apiService.getCurrencies()
emit(currencies)
}
delay(TIME_REFRESH_MS)
}
}
}

override fun startObtaining() {
isObtain = true
}

override fun stopObtaining() {
isObtain = false
}
}

Let’s take a closer look at the getCurrenciesStream() function. It creates and returns Kotlin coroutines flow. I know that every time after calling this method new flow will be created and it would be nice to have a caching mechanism or maybe redesign the class to share a single flow with data. Or think about concurrency issues with the isObtain variable. But anyway it has not been an issue. Instead, I was asked about the infinite while loop. I got several comments from different people that while(true) loops have been treated as a bad practice in Android for a long time and even asked how to change the solution. So is it that bad to use an infinite loop?

About Infinite Loops

There are many different programming languages and different ways of running the infinite loop:

for(;;) // C++
while(true) // Java

What is even more interesting is that some languages have special constructions just for infinite loops like Rust loop:

loop {
...
}

or forever operator in Go:

for {
...
}

And let me remind you that almost every modern program is based on a loop concept and Android is no exception. You can open android.app.ActivityThread, then find the main method and check that it ends with the Looper.loop().

Ok, infinite loops are well-known, widely used, and even included in programming languages by their creators. But why they are treated as something wrong? Let’s continue investigation!

What happens if we run the following code?

fun myAwesomeLoop() {
while(true) {
...
}
}

The first answer which comes to mind is probably — your thread will be stuck forever! But actually, we don’t know what’s hidden inside the loop. It could contain anything from thrown exceptions to break, return, or even goto statement which is still allowed in some programming languages. In every case, the loop will be finished:

fun myAwesomeLoop() {
while(true) {
if(condition) {
break
} else {
return
}
throw InfiniteLoopException()
}
}

Exit by boolean flag

Sometimes people propose to replace true with a boolean variable and use it as loop exit condition, let’s do it in Java:

public static void main(String[] args) {
boolean isRunning = true;

new Thread() {
public void run() {
while(isRunning) {
...
isRunning = false;
}
}
}.start()

...
}

Is it better now? We don’t have an infinite loop and can control invocation with a boolean flag. But there is one problem — it is an old tricky example from well known Effective Java book. In this code boolean variable has not been marked as volatile and some Java compilers could replace it with the initial value — true during the compile time. And we will get an endless loop which brakes the initial program. It’s quite a rare case but I saw such bugs in production code a couple of times during my career.

I'm not talking that the boolean variable is a bad approach — it is just another way of exiting the loop and it also can be the cause of bugs. I don’t care which loop exit way to use but when you are writing an infinite loop you should know what you do and how your programming language will handle it.

Let’s delegate this task

We reviewed some ways how to exit the loop but there is another option — stop the thread! I know that it’s a bad practice to use thread.stop() but we can use interrupt() and check the interrupted flag in the code.

Getting back to the initial case we are dealing with coroutines and flow. As you know it’s impossible to collect our cold flow outside of coroutine as it is a suspend function. So we should have running Coroutine. Moreover, the flow function under the hood creates SafeFlow:

public fun <T> flow(
@BuilderInference block: suspend FlowCollector<T>.() -> Unit
): Flow<T> = SafeFlow(block)

private class SafeFlow<T>(
private val block: suspend FlowCollector<T>.() -> Unit
) : AbstractFlow<T>()

public abstract class AbstractFlow<T> : Flow<T>, CancellableFlow<T>

private class CancellableFlowImpl<T>(
private val flow: Flow<T>
) : CancellableFlow<T> {
override suspend fun collect(collector: FlowCollector<T>) {
flow.collect {
currentCoroutineContext().ensureActive()
collector.emit(it)
}
}
}

And as you can see safe flow is a child of CancellableFlow so we can use the cancellable() function on the client side to force ensureActive() before emitting the next value and handling it properly.

With coroutines and flows, today we are even safer than with old good threads. We can not call suspend function outside the coroutine, we can not ignore the coroutine scope. We have strict child-parent relations. And the most important thing — we are not blocking the thread anymore because even delay is a suspend function. Finally, we can cancel the coroutine outside which means that we are delegating graceful loop cancellation to the higher-level code.

Readability and purpose of the code

After writing the above article I decided to have a chat with other colleagues and we came to the old good rule that every construction in programming should be self-descriptive. This means that an endless loop should be treated as an endless loop by the programmer and be really endless in ideal case. Only in this case, such construction will ensure the best code readability. Getting back to the code — we have a real endless loop here which is used as Generator. Cancellation is delegated to the higher-level code and we don’t see any issue here.

This is an interesting topic to discuss! Do you utilize infinite loops in your coding? If so, in what scenarios? Please feel free to share your opinions in the comments. Thank you!

--

--