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
}
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"))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With