我怎样才能在for-comprehension中做'if..else'?

时间:2010-11-16 08:22:04

标签: scala for-comprehension

我问的是一个最近困扰我的基本问题。 我想编写一个Scala For表达式来执行以下操作:

for (i <- expr1) {
  if (i.method) {
    for (j <- i) {
      if (j.method) {
        doSomething()
      } else {
        doSomethingElseA()
      }
    }
  } else {
    doSomethingElseB()
  }
}

问题在于,在多个生成器For表达式中,我不知道我可以在哪里放置表达体。

for {i <- expr1
  if(i.method) // where can I write the else logic ?
  j <- i 
  if (j.method)
} doSomething()

如何在Scala Style中重写代码?

6 个答案:

答案 0 :(得分:18)

您编写的第一个代码完全有效,因此无需重写它。在其他地方,你说你想知道如何做Scala风格。没有真正的“Scala风格”,但我会假设一种更具功能性的风格并且更加坚定。

for (i <- expr1) {
  if (i.method) {
    for (j <- i) {
      if (j.method) {
        doSomething()
      } else {
        doSomethingElseA()
      }
    }
  } else {
    doSomethingElseB()
  }
}

首先要注意的是,这不会返回任何值。它所做的就是副作用,这也是要避免的。所以第一个改变是这样的:

val result = for (i <- expr1) yield {
  if (i.method) {
    for (j <- i) yield {
      if (j.method) {
        returnSomething()
        // etc

现在,

之间存在很大差异
for (i <- expr1; j <- i) yield ...

for (i <- expr1) yield for (j <- i) yield ...

他们返回不同的东西,有时候你想要的是后者,而不是前者。不过,我会假设你想要前者。现在,在我们继续之前,让我们修复代码。这是丑陋的,难以遵循和没有信息。让我们通过提取方法来重构它。

def resultOrB(j) = if (j.method) returnSomething else returnSomethingElseB
def nonCResults(i) = for (j <- i) yield resultOrB(j)
def resultOrC(i) = if (i.method) nonCResults(i) else returnSomethingC
val result = for (i <- expr1) yield resultOrC(i)

它已经更加清洁,但它并没有像我们期望的那样回归。让我们看看差异:

trait Element
object Unrecognized extends Element
case class Letter(c: Char) extends Element
case class Punct(c: Char) extends Element
val expr1 = "This is a silly example." split "\\b"

def wordOrPunct(j: Char) = if (j.isLetter) Letter(j.toLower) else Punct(j)
def validElements(i: String) = for (j <- i) yield wordOrPunct(j)
def classifyElements(i: String) = if (i.nonEmpty) validElements(i) else Unrecognized
val result = for (i <- expr1) yield classifyElements(i)

result的类型有Array[AnyRef],而使用多个生成器会产生Array[Element]。解决方案的简单部分是:

val result = for {
  i <- expr1
  element <- classifyElements(i)
} yield element

但仅此一点不起作用,因为classifyElements本身返回AnyRef,我们希望它返回一个集合。现在,validElements返回一个集合,因此这不是问题。我们只需修复else部分。由于validElements正在返回IndexedSeq,我们也会在else部分返回该内容。最终结果是:

trait Element
object Unrecognized extends Element
case class Letter(c: Char) extends Element
case class Punct(c: Char) extends Element
val expr1 = "This is a silly example." split "\\b"

def wordOrPunct(j: Char) = if (j.isLetter) Letter(j.toLower) else Punct(j)
def validElements(i: String) = for (j <- i) yield wordOrPunct(j)
def classifyElements(i: String) = if (i.nonEmpty) validElements(i) else IndexedSeq(Unrecognized)
val result = for {
  i <- expr1
  element <- classifyElements(i)
} yield element

这与您所呈现的循环和条件完全相同,但它更易读,更容易更改。

关于收益

我认为重要的是要注意所提出的问题。让我们简化它:

for (i <- expr1) {
  for (j <- i) {
    doSomething
  }
}

现在,这是通过foreach实施的(请参阅here或其他类似的问题和答案)。这意味着上面的代码与此代码完全相同:

for {
  i <- expr1
  j <- i
} doSomething

完全相同的事情。当使用yield时,这根本不是真的。以下表达式不会产生相同的结果:

for (i <- expr1) yield for (j <- i) yield j

for (i <- expr1; j <- i) yield j

第一个代码段将通过两次map次调用实现,而第二个代码段将使用一个flatMap和一个map

因此,只有在yield的上下文中,担心嵌套for循环或使用多个生成器才有意义。而且,实际上,生成器代表了某些东西正在生成的事实,这只适用于真正的for-comprehensions(那些yield某事物)

答案 1 :(得分:4)

部分

for (j <- i) {
   if (j.method) {
     doSomething(j)
   } else {
     doSomethingElse(j)
   }
 }

可以改写为

for(j <- i; e = Either.cond(j.method, j, j)) {
  e.fold(doSomething _, doSomethingElse _)  
}  

(当然,如果你的do ..方法返回一些内容,你可以使用yield)

这里并没有那么糟糕,但如果你有一个更深层次的嵌套结构,它可以......

答案 2 :(得分:3)

import scalaz._; import Scalaz._

val lhs = (_ : List[X]) collect { case j if j.methodJ => doSomething(j) } 
val rhs = (_ : List[X]) map doSomethingElse
lhs <-: (expr1 partition methodI) :-> rhs

答案 3 :(得分:2)

你做不到。 for(expr; if)构造只过滤必须在循环中处理的元素。

答案 4 :(得分:1)

如果订单对doSomething()和doSomethingElse()的调用不重要,那么你可以像这样重新安排代码。

val (tmp, no1) = expr1.partition(_.method)
val (yes, no2) = tmp.partition(_.method)

yes.foreach(doSomething())
no1.foreach(doSomethingElse())
no2.foreach(doSomethingElse())

为了回答你原来的问题,我认为对于具体的用例而言,理解可能会非常好,而你的例子并不适合。

答案 5 :(得分:0)

Scala for operation中指定的条件用于过滤生成器中的元素。不满足条件的元素将被丢弃,不会显示在yield / code块中。

这意味着如果您想基于条件表达式执行备用操作,则需要将测试推迟到yield / code块。

另外请注意,for操作的计算成本相对较高(当前),因此可能更简单的迭代方法可能更合适,可能是这样的:

expr1 foreach {i =>
  if (i.method) {
    i foreach {j =>
      if (j.method)
        doSomething()
      else
        doSomethingElseA()
    }
  }
  else
    doSomethingElseB()
}

更新

如果你必须使用for comprehension并且你可以忍受一些限制,这可能会有效:

for (i <- expr1; j <- i) {
  if (i.method) {if (j.method) doSomething() else doSomethingElseA()} else doSomethingElseB()
}