Search code examples
scalaoption-typescala-3

How to flatten if-else-if-statements with caching in Scala 3?


I have the following very ugly nesting of if-statements that involve slow operations (tryFindResult and tryFindOtherResult) that I need to execute as little as possible. Just to give some context, it's part of a parser that looks at the current line (line) at some position (i). Each case in the if-statement produces side-effects (increases i).

if (cond) {
    // increase i
} else {
    val maybeResult: Option[Result] = tryFindResult(line, i)
    if (maybeResult.nonEmpty) {
        // depending on `maybeResult`, increase i
    } else {
        val (newOffset, maybeOtherResult): (Int, Option[Result2]) = tryFindOtherResult(line, i)
        if (maybeOtherResult.nonEmpty) {
            // depending on `maybeOtherResult`, increase i
        } else {
            // i = newOffset
        }
    }
}

Is there a way I can flatten this? As far as I know, Scala has no inline assignment syntax like if (val o = tryFindOtherResult && o.nonEmpty), right?
Note that moving tryFindResult and tryFindOtherResult to the beginning of the program would solve everything, but these are long-running operations that I can't execute unconditionally (the program is part of a parser and needs to jump forward in the last if/else-branch to avoid quadratic complexity).

Are there tricks with match statements, or map/flatMap/getOrElse on Options that I can use to avoid this kind of nesting?


Solution

  • You can use the methods defined on the Option type and its companion object. I'm not entirely sure of the semantics of each case, but it would roughly look as follows:

    final case class Result(i: Int)
    final case class Result2(i: Int)
    
    def tryFindResult(line: Int, i: Int): Option[Result] =
      if (line == 42) Some(Result(42)) else None
    def tryFindOtherResult(line: Int, i: Int): (Int, Option[Result2]) =
      if (line == 47) (47, Some(Result2(47))) else (-1, None)
    
    def method(cond: Boolean, line: Int, i: Int, fallback: Int): Int =
      Option
        .when(cond)(i + 1)
        .orElse(tryFindResult(line, i).map(_.i + 1))
        .orElse {
          val (_, result) = tryFindOtherResult(line, i)
          result.map(_.i + 1)
        }
        .getOrElse(fallback)
    
    assert(method(cond = true, line = 42, i = 0, fallback = 100) == 1)
    assert(method(cond = true, line = 42, i = 100, fallback = 100) == 101)
    assert(method(cond = false, line = 42, i = 0, fallback = 100) == 43)
    assert(method(cond = false, line = 47, i = 0, fallback = 100) == 48)
    assert(method(cond = false, line = 33, i = 0, fallback = 100) == 100
    

    Notice that the fallback passed to orElse and the function passed to when are both by-name parameters (see the documentation here), which makes them evaluated lazily, i.e. only if they need to be used.

    You can play around with this code here on Scastie.

    You can read more about orElse here and about when here.

    It looks like you actually need to mutate a variable. Consider the option of keeping most of your code as is, if you realize that the usage of the methods I suggested obscures the side effects. Sometimes a nested if might be precisely what you need.