如何在 Scala 中重构 (if / elsif / elsif) 链?

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

我有一连串的 if / else if 语句不能自我解释。我想用一个清晰​​的解释性名称将每个函数提取到自己的函数中,然后将这些函数链接起来。

如何在 Scala 中途停止调用链?

这是一个代码示例:

// 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
}

您可以在 for 理解中使用 Option 类型和 return Option[_] 的方法来链式验证,同时提取部分结果。当一个选项 returns None

时处理停止
for {
    klass <- program.classes
    name <- checkName // Option[String]
    vars <- checkVars // Option[SomeType]
    methods <- checkMethods // Option[SomeOtherT]
    ctx <- addToContext // Option[...]
} {
// do something with klass
// if you got here, all the previous Options returned Some(_)
}

这在很大程度上取决于您希望在错误时发生什么,但这似乎是使用选项的链式映射的好案例:

def checkName(klass: Klass): Option[Klass] = if (compBoolean) Some(klass) else None
def checkVars(klass: Klass): Option[Klass] = if (compBoolean) Some(klass) else None
def checkMethods(klass: Klass): Option[Klass] = if (compBoolean) Some(klass) else None
def finalOp(klass: Klass): OutputClass = //your final operation

// Use the above checks
program.classes.map(checkName(_).flatMap(checkVars).flatMap(checkMethods).map(finalOp).getOrElse(defaultResult))

如果您想要 skip/omit 个未通过所有检查的元素,那么您可以使用 flatMap:

program.classes.flatMap(checkName(_).flatMap(checkVars).flatMap(checkMethods).map(finalOp))

将 Option 与过滤器一起使用将使您可以简单地进行检查 return 一个布尔值。例如

def checkName(klass: Klass): Boolean = ???
def checkVars(klass: Klass): Boolean = ???
def checkMethods(klass: Klass): Boolean = ???
def finalOp(klass: Klass): OutputClass = ???

Option(klass)
  .filter(checkName)
  .filter(checkVars)
  .filter(checkMethods)
  .map(finalOp)

如果所有检查都通过,您将得到 Some(),如果有任何检查失败,您将得到 None

使用 Option 的折叠链

您可以使用 Options fold 方法。

fold 在标准库中是这样定义的

final def fold[B](ifEmpty: => B)(f: Int => B): B

这可以应用于任何一般用例。您所要做的就是 return 不断地选择某事。在下面的情况下,如果任何方法 returns None 链中断。在下面的代码中,您通过发送 Some 或 None 作为消息来与下一个操作进行通信。

def f1: Option[_] = ???
def f2: Option[_] = ???
def f3: Option[_] = ???

f1.fold[Option[Unit]](None)(_ => f2).fold[Option[Unit]](None)(_ => f3)

Scala REPL

scala> Option(1).fold[Option[Unit]](None)(_ => Some(println("hello"))).fold[Option[Unit]](None)(_ => Some(println("scala")))
hello
scala
res59: Option[Unit] = Some(())

scala> None.fold[Option[Unit]](None)(_ => Some(println("hello"))).fold[Option[Unit]](None)(_ => Some(println("scala")))
res60: Option[Unit] = None

scala> Option(1).fold[Option[Unit]](None)(_ => None).fold[Option[Unit]](None)(_ => Some(println("scala")))
res61: Option[Unit] = None
program.classes foreach {
  case klass if checkName(klass) => error1
  case klass if checkVars(klass) => error2
  case klass if checkMethods(klass) => error3
  case klass => addToContext(klass)
}

为了使用偏函数组合回答问题(如问题所问),我们将每个检查定义为 PartialFunction。我们还使用 Try 作为结果类型。 Try然后可以保留处理过程中可能出现的具体错误信息。 (Option,这似乎是一个流行的选择,并没有保留找不到元素的原因。除非我们真的不关心任何错误,否则我不会用它来实现检查信息。)

简化示例:

import scala.util.{Try, Success, Failure}

val check1:PartialFunction[Int, Try[String]] = {case x if x==1 => Failure(new Exception("error1"))}
val check2:PartialFunction[Int, Try[String]] = {case x if x==2 => Failure(new Exception("error2"))}
val check3:PartialFunction[Int, Try[String]] = {case x if x==3 => Failure(new Exception("error3"))}
val process: PartialFunction[Int, Try[String]] = {case x => Success(s"[$x] processed OK")}

val checks = check1 orElse check2 orElse check3 orElse process

for (i <- 1 to 4) yield (checks(i))
//  scala.collection.immutable.IndexedSeq[scala.util.Try[String]] = Vector(
//    Failure(java.lang.Exception: error1),
//    Failure(java.lang.Exception: error2),
//    Failure(java.lang.Exception: error3),
//    Success([4] processed OK)
//)

这是我在 C 中经常使用的模式,它也适用于 Scala。今天早上我记起来了。如果您想将 && 重命名为 thenIfSuccess 或类似的名称(未显示),您可以创建一个隐式方法。

它利用了 && 在第二个参数中是惰性的这一事实。


def checkName(klass: Klass): Boolean = ???
def checkVars(klass: Klass): Boolean = ???
def checkMethods(klass: Klass): Boolean = ???
def finalOp(klass: Klass): Boolean = ???

// just chain the method calls :
checkName(cls) && checkVars(cls) && checkMethods(cls) && finalOp(cls)


// this will call each method in order and stop if one fails.

如果你仔细想想,它非常容易阅读,比使用 foldfilter 或模式匹配的其他答案要多得多。 for 表达式也很容易阅读恕我直言,但它迫使你 return Option[_] 这不是很自然。

最近我遇到了同样烦人的多个 if-else 块,看起来很糟糕
我想出了下一个选项:

选项 1:
最简单的方法是为每个 if-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"))

重构后的代码,看起来比多个 if-else 语句要好得多,现在我们可以给每个函数一个合理的名称,并且在我的例子中,它消除了循环来自样式检查器的复杂性警告

选项 2:
第一种方法仍然有 "else" 部分,我想不惜一切代价摆脱它,所以我使用了部分函数

// 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"))

这里我们也可以使用普通的函数名,并且由于部分函数的设计,我们去掉了else的部分。唯一的问题是,如果需要添加另一个检查(再添加一个 if-else 块),则应在 2 个位置添加:函数声明和作为新的 .orElse 函数调用

选项 3:
很容易注意到,上面所有的偏函数都可以添加到一个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 }
)

则可以遍历List,遍历时可以应用.orElse函数。它应该以任何方式完成,我选择了 foldLeft 函数

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

println(result.getOrElse("passed"))

现在如果我们需要再添加一个检查函数,它只能在一个地方完成 - List

的另一个元素

选项 4:
我想分享的另一个选择是,也可以通过匿名 class 覆盖 PartialFunction 特征并实现它的 2 个方法:isDefinedAt并且 申请

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
}

然后我们可以像在第二个选项中那样链接这些函数

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

println(result.getOrElse("passed"))