0

I am writing a custom loop dsl and I want it's usage to look like below


    var counter1 = 0
    var counter2 = 0
    loop {
            counter1 += 1
            println(counter1)
            stopIf(counter1 == 5) // loop should terminate here and not execute rest of the code if condition matches

            counter2 += 2
            println(counter2)
            stopIf(counter2 == 8) // loop should terminate here and not execute rest of the code if condition matches

        }

I have following code which does allows me to write stopIf any number of times and anywhere in the loop body but when condition matches it does not terminate immediately but executes rest of the loop body and then terminates.


    @UseExperimental(ExperimentalTime::class)
    open class Loop {
        var stop = false

        val loopInterval = 1.seconds

        suspend fun loop(block: suspend () -> Unit): Unit = loop(loopInterval, block)

        suspend fun loop(minimumInterval: Duration, block: suspend () -> Unit): Unit =
            loopWithoutDelay { delayedResult(maxOf(minimumInterval, loopInterval), block) }

        private suspend fun loopWithoutDelay(block: suspend () -> Unit) {
            block()
            if (stop) return else loopWithoutDelay(block)
        }

        suspend fun <T> delayedResult(minDelay: Duration, f: suspend () -> T): T = coroutineScope {
            val futureValue = async { f() }
            delay(minDelay.toJavaDuration())
            futureValue.await()
        }

        fun stopIf(condition: Boolean) {
            if (condition) {
                stop = condition // once stop condition matches, then do not override it with following false condtions
            }
        }
    }

    @ExperimentalTime
    suspend fun loop(block: suspend Loop.() -> Unit) =
        Loop().run { loop { block(this) } }

I have tried to use return with label but it did not work. Is there any way I can achieve this?

Pritam Kadam
  • 2,388
  • 8
  • 16

2 Answers2

2

It can be done for example with throwing a lightweight exception. You have to declare custom exception:

class LoopStopException : Throwable("Stop look", null, false, false) // lightweight throwable without the stack trace

and catch it in loopWithoutDelay:

private suspend fun loopWithoutDelay(block: suspend () -> Unit) {
    try {
        while (true) {
            block()
        }
    } catch (e: LoopStopException) {
        //do nothing
    }
}

Andrei Tanana
  • 7,932
  • 1
  • 27
  • 36
1

I didn't understand much about the function delayedResult, because none of the dsl's public functions return a result. However, I come up with an solution for cancelling the loop.

As far as I understood, we have to have a loop that doesn't block the current thread. Therefore, it must be run in a coroutine, but in order to be able to cancel the loop, the dsl must run its own coroutine. This inner coroutine is run using coroutineScope, so it suspends the parent coroutine until it's finished or cancelled.

@ExperimentalTime
class Loop {
    private val loopInterval = 1.seconds

    suspend fun loop(block: suspend () -> Unit) = loop(loopInterval, block)

    suspend fun loop(minimumInterval: Duration, block: suspend () -> Unit):Job  = coroutineScope {
        launch {
            while (true) {
                block()
                delay(minOf(minimumInterval, loopInterval).toLongMilliseconds())
            }
        }
    }

    suspend fun stopIf(condition: Boolean) = coroutineScope {
        suspendCancellableCoroutine<Unit> {
            if (condition) it.cancel() else it.resumeWith(Result.success(Unit))
        }
    }
}

@ExperimentalTime
suspend fun loop(block: suspend Loop.() -> Unit):Job {
    return Loop().run {
        this.loop {
            block(this)
        }
    }
}
Stanislav Shamilov
  • 1,746
  • 11
  • 20
  • This almost covers all the use cases but if I have a code after `stopIf` which is not `coroutine`, then that gets executed for ex. `println` – Pritam Kadam Sep 13 '19 at 20:16