Coroutines - going "backwards," other control flow

So I have some code for a command-line based flash card memorization app, and the pattern I came up with before discovering coroutines is very reminiscent of how a suspend function would look after compiler transformation (assuming every “expect” call were suspending instead of blocking):

    private fun doGuessingGameWithOptionsLoop(context: Context, flashCardSetFiles: Array<File>) {
        val out = context.out

        var flashCardSet: YamlFlashCardSet? = null
        var tables: Array<YamlTable>? = null
        var guessColumnIndices: IntArray? = null
        var displayColumnIndices: IntArray? = null
        var rowChoiceCount = -1
        var randomSeed: Long = -1

        val stateMgr = StateManager(7)
        while (stateMgr.nextState(context)) {
            if (!stateMgr.isFirstState) out.println()
            when (stateMgr.state) {
                0 -> flashCardSet = expectFlashCardSet(context, flashCardSetFiles)
                1 -> tables = expectFlashCardTables(context, flashCardSet!!, "Which table(s) would you like to play with?")
                2 -> displayColumnIndices = expectColumnIndices(context, flashCardSet!!, "Which column(s) would you like to see?")
                3 -> guessColumnIndices = expectColumnIndices(context, flashCardSet!!, "Which column(s) would you like to guess?")
                4 -> {
                    out.println("How many choices per round?")
                    rowChoiceCount = Input.expectInt(context, 1, Int.MAX_VALUE)
                }
                5 -> randomSeed = Input.expectRandomSeed(context, "What seed would you like to use?")
                6 -> {
                    out.println("Game commencing with seed " + java.lang.Long.toHexString(randomSeed) + "...")
                    out.println()
                    GuessingGameOptions(Random(randomSeed), flashCardSet!!, tables!!, guessColumnIndices!!, displayColumnIndices!!, rowChoiceCount).playGame(context)
                    return
                }
            }
        }
    }

I.e., a big switch statement to control flow after returning from suspensions. The main difference, however, is that StateManager supports flows of NORMAL, BACK_ONE, RESTART, EXIT_APP. The BACK_ONE is particularly tricky - if I were to remove it, could I effectively implement this using normal coroutines and eliminating the big switch statement? If I were to keep it, would it even be possible without the switch, and if so, how much hackery would be involved? What conventions would I have to follow to assure safety?

My use case is to get useful input from the user and allow stepping back in the process, exiting the app, exiting just a game within the app, etc. Being able to write simple functions that elegantly reflect this sequential process is very appealing. E.g.:

    private suspend fun doGuessingGameWithOptionsLoop(context: Context, flashCardSetFiles: Array<File>) {
        val out = context.out

        val flashCardSet = expectFlashCardSet(context, flashCardSetFiles)
        val tables = expectFlashCardTables(context, flashCardSet!!, "Which table(s) would you like to play with?")
        val displayColumnIndices = expectColumnIndices(context, flashCardSet, "Which column(s) would you like to see?")
        val guessColumnIndices = expectColumnIndices(context, flashCardSet, "Which column(s) would you like to guess?")
        
        out.println("How many choices per round?")
        val rowChoiceCount = Input.expectInt(context, 1, Int.MAX_VALUE)
        
        val randomSeed = Input.expectRandomSeed(context, "What seed would you like to use?")
        
        out.println("Game commencing with seed " + java.lang.Long.toHexString(randomSeed) + "...")
        out.println()
        GuessingGameOptions(Random(randomSeed), flashCardSet, tables!!, guessColumnIndices!!, displayColumnIndices!!, rowChoiceCount).playGame(context)
    }

In this case, stepping back would be “safe,” as it’d just be overwriting the old variable. Any bits of code between “expects” would get re-executed, which in this case would be desirable. An all-caps comment on the top indicating the dangerous flow would hopefully be sufficient to prevent errors (in lieu of the language-level support you’d ideally have for something like this.)

Thoughts? Is going backwards impossible? Am I a crazy person? Thanks for reading.

I don’t think coroutines is the answer you are looking for. Coroutines allow you to resume where you left off once. I believe you’d need need to be able to resume multiple times to go-back.

Note that you could turn your when statement into a list of lambdas. Then the numbering is implicit with the list ordering. Also, I’d suggest moving that loop into StateManager, and you could pass the lambda list in.

val stateMgr = StateManager(
    separatorTask = { out.println() },
    taskList = listOf(
        { flashCardSet = expectFlashCardSet(context, flashCardSetFiles) },
        ...
    )
)
stateMgr.execute(context)

//StateManager code
class StateManager(private val separatorTask: () -> Unit, private val taskList: List<() -> Unit>) {
    ....
    fun execute(context: Context) {
        while (nextState(context)) {
            if (!isFirstState) separatorTask()
            taskList[stateMgr.state]()
        }
    }
}

If you ditch the BACK_ONE ability, then you could use exceptions to get your RESTART and EXIT_APP behavior.

while(true) {
    try {
        // Just write the code out without switch statement
        // Throw the appropriate exception to restart/exit
        break;
    } catch (e: MyRestartException) {}
    } catch (e: MyExitException) { break; }
}