Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to refactor (if / elsif / elsif) chain in Scala?

I have a chain of if / else if statement that are not self explanatory. I would like to extract each into its own function with a clear explanatory name and then chain those functions.

How can I stop the call-chain mid-way in scala ?

Here is a code example :

// actual code 

for( klass <- program.classes ) {
    if ( complicated boolean ) { //checkVars
        error1
    } else if ( complicated boolean ) { //checkMethods
        error2
    } else if ( ... ) { //...
        error3
    } else {
        complicated good case code
    }
}

// wanted 

for( klass <- program.classes ) {
    (checkName 
     andThen checkVars
     andThen checkMethods
     andThen addToContext) (klass)
// where the chaining stops if a check fails
}
like image 537
Julien__ Avatar asked Nov 15 '16 19:11

Julien__


1 Answers

Recently I had the same annoying multiple if-else block which looked terrible
I come up with the next options:

Option 1:
The simplest approach is to introduce a separate function for each if-else block, for an example condition I'm just comparing an integer constant with a literal, but you can substitute it with anything else

val x = 3

def check1: Option[String] = {
  if (x == 1) Some("error 1") else None
}

def check2: Option[String] = {
  if (x == 2) Some("error 2") else None
}

def check3: Option[String] = {
  if (x == 3) Some("error 3") else None
}

// we can chain Option results together
// using "orElse" function
val result = check1
  .orElse(check2)
  .orElse(check3)

// result contains a returned value from one
// of the above functions,
// or if no checks worked, it ends up with "Option.None"
println(result.getOrElse("passed"))

Refactored code as it is, looks much better than multiple if-else statements, now we can give each function a reasonable name, and, in my case, it eliminated cyclomatic complexity warnings from style checker

Option 2:
The first approach still had "else" part and I wanted to get rid of it at all costs, so I used partial functions

// just an alias, so I don't need to write
// the full parameter type for every function
type Validator = PartialFunction[Int, Option[String]]

def check1: Validator = { case x if x == 1 => Some("error 1") }
def check2: Validator = { case x if x == 2 => Some("error 2") }
def check3: Validator = { case x if x == 3 => Some("error 3") }
def default: Validator = { case _ => None }

// we can chain together partial functions
// the same way as we did with Option type
val result = check1
  .orElse(check2)
  .orElse(check3)
  .orElse(default) {
    3 // this is an actual parameter for each defined function
  }

// the result is Option
// if there was an error we get Some(error)
// otherwise the result is Option.None in which case
// we return "passed"
println(result.getOrElse("passed"))

Here we can use normal function names as well and we got rid of else's part thanks to the design of the partial function. The only thing is that if there is a need to add another check (one more if-else block), it should be added in 2 spots: function declaration and as a new .orElse function call

Option 3:
It is easy to notice that all the above partial functions can be added in a List

type Validator = PartialFunction[Int, Option[String]]

val validations: List[Validator] = List(
  { case x if x == 1 => Some("error 1") },
  { case x if x == 2 => Some("error 2") },
  { case x if x == 3 => Some("error 3") },
  { case _ => None }
)

Then List can be traversed and .orElse function can be applied during the traversal. It should be done in any way, I chose the foldLeft function

val result = validations.tail.foldLeft(validations.head)(_.orElse(_)) {
  3
}

println(result.getOrElse("passed"))

Now if we need one more check function to add, it can be done only at one spot - another element of the List

Option 4:
Another option I wanted to share is that it is also possible to override PartialFunction trait by anonymous class and implement its 2 methods: isDefinedAt and apply

type Validator = PartialFunction[Int, Option[String]]

val check1 = new Validator {
  override def isDefinedAt(x: Int): Boolean = x == 1
  override def apply(v1: Int): Option[String] = Some("error 1")
}

val check2 = new Validator {
  override def isDefinedAt(x: Int): Boolean = x == 2
  override def apply(v1: Int): Option[String] = Some("error 2")
}

val check3 = new Validator {
  override def isDefinedAt(x: Int): Boolean = x == 3
  override def apply(v1: Int): Option[String] = Some("error 3")
}

val default = new Validator {
  override def isDefinedAt(x: Int): Boolean = true
  override def apply(v1: Int): Option[String] = None
}

Then we can chain those functions the same way we did in the 2nd option

val result = check1
  .orElse(check2)
  .orElse(check3)
  .orElse(default) {
    3
  }

println(result.getOrElse("passed"))
like image 67
Stanislav Parkhomenko Avatar answered Sep 20 '22 03:09

Stanislav Parkhomenko