Search code examples
listscalamonadsoption-type

What is the idiomatic (and fast) way of treating the empty list/Seq as failure in a short-circuiting operation?


I have a situation where I am using functions to model rule applications, with each function returning the actions it would take when applied, or, if the rule cannot be applied, the empty list. I have a number of rules that I would like to try in sequence and short-circuit. In other languages I am used to, I would treat the empty sequence as false/None and chain them with orElse, like this:


def ruleOne(): Seq[Action] = ???
def ruleTwo(): Seq[Action] = ???
def ruleThree(): Seq[Action] = ???

def applyRules(): Seq[Action] = ruleOne().orElse(ruleTwo).orElse(ruleThree)

However, as I understand the situation, this will not work and will, in fact, do something other than what I expect.

I could use return which feels bad to me, or, even worse, nested if statements. if let would have been great here, but AFAICT Scala does not have that.

What is the idiomatic approach here?


Solution

  • You have different approaches here. One of them is combining all the actions inside a Seq (so creating a Seq[Seq[Action]]) and then using find (it will return the first element that matches a given condition). So, for instance:

    Seq(ruleOne, ruleTwo, ruleThree).find(_.nonEmpty).getOrElse(Seq.empty[Action])
    

    I do not know clearly your domain application, but the last getOrElse allows to convert the Option produced by the find method in a Seq. This method though eval all the sequences (no short circuit).

    Another approach consists in enriching Seq with a method that simulated your idea of orElse using pimp my library/extensions method:

    implicit class RichSeq[T](left: Seq[T]) {
       def or(right: => Seq[T]): Seq[T] = if(left.isEmpty) { right } else { left }
    }
    
    

    The by name parameter enable short circuit evaluation. Indeed, the right sequence is computed only if the left sequence is empty.

    Scala 3 has a better syntax to this kind of abstraction:

    extension[T](left: Seq[T]){
      def or(rigth: => Seq[T]): Seq[T] = if(left.nonEmpty) { left } else { rigth }
    }
    

    In this way, you can call:

    ruleOne or ruleTwo or ruleThree
    
    

    Scastie for scala 2

    Scastie for scala 3